From ce1f582908e7bc65b0d1a9562b8b47bfb5eae5fd Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 8 Jun 2026 19:57:02 +0800 Subject: [PATCH 01/70] feat(cp0_lvgl): add signal-driven audio support Introduce C++ sigslot events for cp0_lvgl and initialize the audio handler during cp0_lvgl startup. Implement a miniaudio-backed AudioSystem that uses PulseAudio, resolves bundled audio assets through hal_path_audio_dir, preloads switch and enter sounds, and supports playing named wav files through cp0_signal_audio. Update component and HelloWorld build requirements to compile C++ sources and link Sigslot and Miniaudio. --- ext_components/cp0_lvgl/SConstruct | 4 +- ext_components/cp0_lvgl/include/commount.h | 2 +- .../cp0_lvgl/include/hal_lvgl_bsp.h | 12 +- ext_components/cp0_lvgl/src/commount.c | 4 +- ext_components/cp0_lvgl/src/commount.cpp | 14 + ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 1 + ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h | 2 +- .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 255 ++++++++++++++++-- .../cp0_lvgl/src/cp0/cp0_lvgl_file.cpp | 11 + .../cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c | 1 + projects/HelloWorld/main/SConstruct | 2 +- 11 files changed, 280 insertions(+), 28 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/commount.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp diff --git a/ext_components/cp0_lvgl/SConstruct b/ext_components/cp0_lvgl/SConstruct index f7a57c74..29f6f654 100644 --- a/ext_components/cp0_lvgl/SConstruct +++ b/ext_components/cp0_lvgl/SConstruct @@ -9,14 +9,14 @@ if "CONFIG_CP0_LVGL_COMPONENT_ENABLED" in os.environ or "CONFIG_SIGSLOT_COMPONEN SRCS = [] INCLUDE = [ADir('include')] PRIVATE_INCLUDE = [] - REQUIREMENTS = ['lvgl_component'] + REQUIREMENTS = ['lvgl_component', 'Sigslot', 'Miniaudio'] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] DEFINITIONS_PRIVATE = [] LDFLAGS = [] LINK_SEARCH_PATH = [] - SRCS += [AFile('src/commount.c')] + SRCS += Glob('src/*.c*') if 'CONFIG_V9_5_LV_USE_SDL' in os.environ: SRCS += Glob('src/sdl/*.c*') else: diff --git a/ext_components/cp0_lvgl/include/commount.h b/ext_components/cp0_lvgl/include/commount.h index d59d83cb..30494196 100644 --- a/ext_components/cp0_lvgl/include/commount.h +++ b/ext_components/cp0_lvgl/include/commount.h @@ -5,7 +5,7 @@ extern "C" { #endif void init_lvgl_event(); - +void init_lvgl_event_cpp(); #ifdef __cplusplus } diff --git a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h index b50dcc09..247fdabc 100644 --- a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h +++ b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h @@ -26,10 +26,14 @@ typedef struct { extern uint32_t lv_c_event[(2*CP0_C_EVENT_END)]; void cp0_lvgl_init(void); - - - - +const char *hal_path_audio_dir(void); #ifdef __cplusplus } +#include +#include +extern sigslot::signal cp0_signal_audio; +extern sigslot::signal<> cp0_signal_network; +extern sigslot::signal<> cp0_signal_forkexec; +extern sigslot::signal<> cp0_signal_screenshot; +std::string cp0_file_path(std::string file); #endif diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index 0423c2e5..1b8251a4 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -3,10 +3,12 @@ #include "lvgl/lvgl.h" uint32_t lv_c_event[(2*CP0_C_EVENT_END)]; -uint32_t cp0_event[(2*CP0_C_EVENT_END)]; void init_lvgl_event() { + init_lvgl_event_cpp(); for (int i = 0; i < CP0_C_EVENT_END; i++) lv_c_event[i] = lv_event_register_id(); } + + diff --git a/ext_components/cp0_lvgl/src/commount.cpp b/ext_components/cp0_lvgl/src/commount.cpp new file mode 100644 index 00000000..c0d8dff1 --- /dev/null +++ b/ext_components/cp0_lvgl/src/commount.cpp @@ -0,0 +1,14 @@ +#include "hal_lvgl_bsp.h" +#include "commount.h" +#include "lvgl/lvgl.h" + + +sigslot::signal cp0_signal_audio; +sigslot::signal<> cp0_signal_network; +sigslot::signal<> cp0_signal_forkexec; +sigslot::signal<> cp0_signal_screenshot; + +extern "C" void init_lvgl_event_cpp() +{ + +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index 96eb5be3..a466446f 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -7,4 +7,5 @@ void cp0_lvgl_init(void) init_lvgl_event(); init_freambuffer_disp(); init_input(); + init_audio(); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h index c40a446a..b1fc935b 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h @@ -6,7 +6,7 @@ extern "C" { void init_freambuffer_disp(); void init_input(); - +void init_audio(); #ifdef __cplusplus } #endif diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index 5068d7ff..9ba29f57 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -1,23 +1,242 @@ #include "hal_lvgl_bsp.h" -#include "lvgl/lvgl.h" -#include "commount.h" -#include -#include -#include -#include -#include -#include -#include -#include - - -class cp0_lvgl_audio + +#include +#include +#include +#include + +#define MINIAUDIO_IMPLEMENTATION +#include "miniaudio.h" + +extern "C" __attribute__((weak)) const char *hal_path_audio_dir(void) +{ + return nullptr; +} + +namespace { + +#ifdef _WIN32 +constexpr const char *kAudioPathSep = "\\"; +#else +constexpr const char *kAudioPathSep = "/"; +#endif + +std::string audioAssetPath(const std::string &name) +{ + const char *dir = hal_path_audio_dir(); + if (dir == nullptr || name.empty()) { + return {}; + } + + std::string path(dir); + if (!path.empty() && path.back() != '/' && path.back() != '\\') { + path += kAudioPathSep; + } + path += name; + return path; +} + +} // namespace + +class AudioSystem { -private: - /* data */ public: - cp0_lvgl_audio(/* args */); - ~cp0_lvgl_audio(); -}; + AudioSystem() + { + initialize(); + } + + int initialize() + { + std::lock_guard lock(mutex_); + if (initialized_) { + return 0; + } + + ma_backend backends[] = { + ma_backend_pulseaudio + }; + + ma_result result = ma_context_init( + backends, + sizeof(backends) / sizeof(backends[0]), + nullptr, + &context_ + ); + + if (result != MA_SUCCESS) { + std::fprintf(stderr, "[AUDIO] ma_context_init PulseAudio failed: %d\n", result); + return -1; + } + + ma_engine_config engineConfig = ma_engine_config_init(); + engineConfig.pContext = &context_; + engineConfig.pPlaybackDeviceID = nullptr; + engineConfig.channels = 2; + engineConfig.sampleRate = 48000; + + result = ma_engine_init(&engineConfig, &engine_); + if (result != MA_SUCCESS) { + std::fprintf(stderr, "[AUDIO] ma_engine_init PulseAudio failed: %d\n", result); + ma_context_uninit(&context_); + return -1; + } + + initialized_ = true; + std::printf("[AUDIO] audio system initialized with PulseAudio backend\n"); + return 0; + } + + int loadSounds() + { + if (initialize() != 0) { + return -1; + } + + std::lock_guard lock(mutex_); + if (soundsLoaded_) { + return 0; + } + const std::string switchPath = audioAssetPath("switch.wav"); + const std::string enterPath = audioAssetPath("enter.wav"); + if (switchPath.empty() || enterPath.empty()) { + std::fprintf(stderr, "[AUDIO] audio path unavailable\n"); + return -1; + } + ma_result result = ma_sound_init_from_file( + &engine_, + switchPath.c_str(), + MA_SOUND_FLAG_DECODE, + nullptr, + nullptr, + &switchSound_ + ); + + if (result != MA_SUCCESS) { + std::fprintf(stderr, "[AUDIO] load switch.wav failed: %d, path=%s\n", + result, + switchPath.c_str()); + return -1; + } + + result = ma_sound_init_from_file( + &engine_, + enterPath.c_str(), + MA_SOUND_FLAG_DECODE, + nullptr, + nullptr, + &enterSound_ + ); + + if (result != MA_SUCCESS) { + std::fprintf(stderr, "[AUDIO] load enter.wav failed: %d, path=%s\n", + result, + enterPath.c_str()); + ma_sound_uninit(&switchSound_); + return -1; + } + + soundsLoaded_ = true; + std::printf("[AUDIO] sounds loaded\n"); + return 0; + } + + void playSwitch() + { + playLoadedSound(switchSound_); + } + + void playEnter() + { + playLoadedSound(enterSound_); + } + + void play(const std::string &wav) + { + if (wav == "switch.wav" || wav == "switch") { + playSwitch(); + return; + } + + if (wav == "enter.wav" || wav == "enter") { + playEnter(); + return; + } + + std::string path = wav; + if (path.find('/') == std::string::npos && path.find('\\') == std::string::npos) { + path = audioAssetPath(wav); + } + + playFile(path); + } + + void playFile(const std::string &path) + { + if (path.empty() || initialize() != 0) { + return; + } + + std::lock_guard lock(mutex_); + ma_result result = ma_engine_play_sound(&engine_, path.c_str(), nullptr); + if (result != MA_SUCCESS) { + std::fprintf(stderr, "[AUDIO] play_audio failed: %d, path=%s\n", + result, + path.c_str()); + } + } + + void uninitialize() + { + std::lock_guard lock(mutex_); + + if (soundsLoaded_) { + ma_sound_uninit(&switchSound_); + ma_sound_uninit(&enterSound_); + soundsLoaded_ = false; + } + + if (initialized_) { + ma_engine_uninit(&engine_); + ma_context_uninit(&context_); + initialized_ = false; + } + } + ~AudioSystem() + { + uninitialize(); + } +private: + AudioSystem(const AudioSystem &) = delete; + AudioSystem &operator=(const AudioSystem &) = delete; + + void playLoadedSound(ma_sound &sound) + { + if (loadSounds() != 0) { + return; + } + + std::lock_guard lock(mutex_); + ma_sound_stop(&sound); + ma_sound_seek_to_pcm_frame(&sound, 0); + ma_sound_start(&sound); + } + + std::mutex mutex_; + ma_context context_{}; + ma_engine engine_{}; + ma_sound switchSound_{}; + ma_sound enterSound_{}; + bool initialized_ = false; + bool soundsLoaded_ = false; +}; + +extern "C" void init_audio(void) +{ + std::shared_ptr audio = std::make_shared(); + cp0_signal_audio.connect([audio](std::string wav) { + audio->play(wav); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp new file mode 100644 index 00000000..656ae4a8 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp @@ -0,0 +1,11 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include + +std::string cp0_file_path(std::string file) +{ + +} \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c index 9fc5e5b0..1cd9e912 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c @@ -20,6 +20,7 @@ #define CP0_KEY_QUEUE_SIZE 10 #define CP0_EVDEV_KEYCODE_OFFSET 8 +#define LV_C_EVENT_KEYBOARD lv_c_event[CP0_C_EVENT_KEYBOARD] typedef struct { uint32_t code; diff --git a/projects/HelloWorld/main/SConstruct b/projects/HelloWorld/main/SConstruct index 5544e246..1ca2142f 100644 --- a/projects/HelloWorld/main/SConstruct +++ b/projects/HelloWorld/main/SConstruct @@ -7,7 +7,7 @@ with open(env['PROJECT_TOOL_S']) as f: SRCS = Glob('src/*.c*') INCLUDE = [ADir('.'), ADir('include')] PRIVATE_INCLUDE = [] -REQUIREMENTS = ['cp0_lvgl', 'lvgl_component', 'pthread'] +REQUIREMENTS = ['cp0_lvgl', 'lvgl_component', 'pthread', 'Sigslot'] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] #'-std=c++20' From b71bff84de4b4418195d17c5d390572d00d90863 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 9 Jun 2026 11:18:32 +0800 Subject: [PATCH 02/70] feat(cp0_lvgl): add eventpp signal bridge and battery publisher Replace cp0_lvgl signal declarations with eventpp CallbackList definitions generated from a shared registration plan. Add an event queue runner for cp0 tasks so callbacks can be processed asynchronously. Introduce cp0_lvgl battery support that reads the CP0 battery capacity sysfs node and publishes LVGL battery events through cp0_signal_battery_pub during cp0_lvgl initialization. Enable eventpp in HelloWorld default configs and add eventpp to cp0_lvgl and HelloWorld build requirements. --- ext_components/cp0_lvgl/SConstruct | 2 +- .../cp0_lvgl/include/hal_lvgl_bsp.h | 15 +++-- .../cp0_lvgl/include/signal_register_plan.h | 5 ++ ext_components/cp0_lvgl/src/commount.c | 4 +- ext_components/cp0_lvgl/src/commount.cpp | 19 ++++-- ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 1 + ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp | 65 +++++++++++++++++++ projects/HelloWorld/config_defaults.mk | 3 +- .../linux_x86_cross_cp0_config_defaults.mk | 3 +- .../linux_x86_sdl2_config_defaults.mk | 3 +- .../mac_cross_cp0_config_defaults.mk | 1 + projects/HelloWorld/main/SConstruct | 2 +- 14 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 ext_components/cp0_lvgl/include/signal_register_plan.h create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp diff --git a/ext_components/cp0_lvgl/SConstruct b/ext_components/cp0_lvgl/SConstruct index 29f6f654..72ecb855 100644 --- a/ext_components/cp0_lvgl/SConstruct +++ b/ext_components/cp0_lvgl/SConstruct @@ -9,7 +9,7 @@ if "CONFIG_CP0_LVGL_COMPONENT_ENABLED" in os.environ or "CONFIG_SIGSLOT_COMPONEN SRCS = [] INCLUDE = [ADir('include')] PRIVATE_INCLUDE = [] - REQUIREMENTS = ['lvgl_component', 'Sigslot', 'Miniaudio'] + REQUIREMENTS = ['lvgl_component', 'Sigslot', 'Miniaudio', 'eventpp'] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] diff --git a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h index 247fdabc..5071ac20 100644 --- a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h +++ b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h @@ -29,11 +29,16 @@ void cp0_lvgl_init(void); const char *hal_path_audio_dir(void); #ifdef __cplusplus } -#include +// #include +#include "eventpp/callbacklist.h" +#include "eventpp/eventqueue.h" #include -extern sigslot::signal cp0_signal_audio; -extern sigslot::signal<> cp0_signal_network; -extern sigslot::signal<> cp0_signal_forkexec; -extern sigslot::signal<> cp0_signal_screenshot; +#include + +#define def_hal_fun(arg, name) extern eventpp::CallbackList name; +#include "signal_register_plan.h" +#undef def_hal_fun +extern eventpp::EventQueue)> cp0_task_queue; + std::string cp0_file_path(std::string file); #endif diff --git a/ext_components/cp0_lvgl/include/signal_register_plan.h b/ext_components/cp0_lvgl/include/signal_register_plan.h new file mode 100644 index 00000000..50a396e8 --- /dev/null +++ b/ext_components/cp0_lvgl/include/signal_register_plan.h @@ -0,0 +1,5 @@ +def_hal_fun(void(std::string), cp0_signal_audio_play) +def_hal_fun(void(), cp0_signal_network) +def_hal_fun(void(), cp0_signal_forkexec) +def_hal_fun(void(), cp0_signal_screenshot) +def_hal_fun(void(std::function), cp0_signal_battery_pub) diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index 1b8251a4..bfc63f57 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -2,13 +2,13 @@ #include "commount.h" #include "lvgl/lvgl.h" -uint32_t lv_c_event[(2*CP0_C_EVENT_END)]; +uint32_t lv_c_event[(2*CP0_C_EVENT_END)] = {0}; void init_lvgl_event() { - init_lvgl_event_cpp(); for (int i = 0; i < CP0_C_EVENT_END; i++) lv_c_event[i] = lv_event_register_id(); + init_lvgl_event_cpp(); } diff --git a/ext_components/cp0_lvgl/src/commount.cpp b/ext_components/cp0_lvgl/src/commount.cpp index c0d8dff1..9edeb4f7 100644 --- a/ext_components/cp0_lvgl/src/commount.cpp +++ b/ext_components/cp0_lvgl/src/commount.cpp @@ -1,14 +1,21 @@ #include "hal_lvgl_bsp.h" #include "commount.h" #include "lvgl/lvgl.h" +#include +#define def_hal_fun(arg, name) eventpp::CallbackList name; +#include "signal_register_plan.h" +#undef def_hal_fun -sigslot::signal cp0_signal_audio; -sigslot::signal<> cp0_signal_network; -sigslot::signal<> cp0_signal_forkexec; -sigslot::signal<> cp0_signal_screenshot; - +eventpp::EventQueue)> cp0_task_queue; +int queue_run_flage = 1; extern "C" void init_lvgl_event_cpp() { - + cp0_task_queue.appendListener(CP0_C_EVENT_END, [&queue_run_flage](const std::list args) + { queue_run_flage = 0; }); + std::thread t([cp0_task_queue, &queue_run_flage]() + { + while (queue_run_flage) + cp0_task_queue.process(); }); + t.detach(); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index a466446f..f2595a76 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -8,4 +8,5 @@ void cp0_lvgl_init(void) init_freambuffer_disp(); init_input(); init_audio(); + init_battery(); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h index b1fc935b..0bb6e4e0 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h @@ -7,6 +7,7 @@ extern "C" { void init_freambuffer_disp(); void init_input(); void init_audio(); +void init_battery(); #ifdef __cplusplus } #endif diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index 9ba29f57..95323f23 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -236,7 +236,7 @@ class AudioSystem extern "C" void init_audio(void) { std::shared_ptr audio = std::make_shared(); - cp0_signal_audio.connect([audio](std::string wav) { + cp0_signal_audio_play.append([audio](std::string wav) { audio->play(wav); }); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp new file mode 100644 index 00000000..3be8a532 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp @@ -0,0 +1,65 @@ +#include "hal_lvgl_bsp.h" +#include "lvgl/lvgl.h" +#include "cp0_lvgl.h" +#include +#include +#include +#include +#include +#include + +class BatterySystem +{ +private: + /* data */ +public: + BatterySystem() = default; + void pub() + { + if (lv_c_event[CP0_C_EVENT_BATTERY] != 0) + { + int capacity = get_battery_value(); + if (capacity >= 0) + { + // lv_lock(); + lv_obj_t *root = lv_display_get_screen_active(NULL); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_BATTERY], (void *)&capacity); + // lv_unlock(); + } + } + } + ~BatterySystem() = default; + +private: + std::string scanf_battery_path() + { + return "/sys/class/power_supply/bq27220-0/capacity"; + } + int get_battery_value() + { + int capacity; + std::string capacity_path = scanf_battery_path(); + std::ifstream file(capacity_path); + if (!file.is_open()) + { + std::cerr << "Failed to open file: " << capacity_path << std::endl; + return -1; + } + file >> capacity; + if (file.fail()) + { + std::cerr << "Failed to read capacity value" << std::endl; + return -1; + } + file.close(); + return capacity; + } +}; + +extern "C" void init_battery() +{ + std::shared_ptr battery = std::make_shared(); + cp0_signal_battery_pub.append([battery](std::function fun) + { battery->pub(); }); +} \ No newline at end of file diff --git a/projects/HelloWorld/config_defaults.mk b/projects/HelloWorld/config_defaults.mk index a48ab498..30ee3a24 100644 --- a/projects/HelloWorld/config_defaults.mk +++ b/projects/HelloWorld/config_defaults.mk @@ -78,4 +78,5 @@ CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y -CONFIG_CP0_LVGL_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y +CONFIG_EVENTPP_ENABLED=y \ No newline at end of file diff --git a/projects/HelloWorld/linux_x86_cross_cp0_config_defaults.mk b/projects/HelloWorld/linux_x86_cross_cp0_config_defaults.mk index 26d1b51a..8520b428 100644 --- a/projects/HelloWorld/linux_x86_cross_cp0_config_defaults.mk +++ b/projects/HelloWorld/linux_x86_cross_cp0_config_defaults.mk @@ -96,4 +96,5 @@ CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y -CONFIG_CP0_LVGL_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y +CONFIG_EVENTPP_ENABLED=y \ No newline at end of file diff --git a/projects/HelloWorld/linux_x86_sdl2_config_defaults.mk b/projects/HelloWorld/linux_x86_sdl2_config_defaults.mk index d067e4a9..69ddcac6 100644 --- a/projects/HelloWorld/linux_x86_sdl2_config_defaults.mk +++ b/projects/HelloWorld/linux_x86_sdl2_config_defaults.mk @@ -89,4 +89,5 @@ CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y -CONFIG_CP0_LVGL_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y +CONFIG_EVENTPP_ENABLED=y \ No newline at end of file diff --git a/projects/HelloWorld/mac_cross_cp0_config_defaults.mk b/projects/HelloWorld/mac_cross_cp0_config_defaults.mk index 38f83865..d8d3ea96 100644 --- a/projects/HelloWorld/mac_cross_cp0_config_defaults.mk +++ b/projects/HelloWorld/mac_cross_cp0_config_defaults.mk @@ -98,3 +98,4 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y +CONFIG_EVENTPP_ENABLED=y \ No newline at end of file diff --git a/projects/HelloWorld/main/SConstruct b/projects/HelloWorld/main/SConstruct index 1ca2142f..410741b7 100644 --- a/projects/HelloWorld/main/SConstruct +++ b/projects/HelloWorld/main/SConstruct @@ -7,7 +7,7 @@ with open(env['PROJECT_TOOL_S']) as f: SRCS = Glob('src/*.c*') INCLUDE = [ADir('.'), ADir('include')] PRIVATE_INCLUDE = [] -REQUIREMENTS = ['cp0_lvgl', 'lvgl_component', 'pthread', 'Sigslot'] +REQUIREMENTS = ['cp0_lvgl', 'lvgl_component', 'pthread', 'Sigslot', 'eventpp'] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] #'-std=c++20' From 9e64f4bdc6528a38257f7f30d2df88f77c66cbb5 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 9 Jun 2026 13:36:43 +0800 Subject: [PATCH 03/70] refactor(cp0_lvgl): split SDL backend modules Break the SDL LVGL backend out of the monolithic sdl_lvgl.c implementation so display setup, keyboard handling, and file path hooks live in their own translation units. - Move SDL window creation and environment-based sizing/title setup into sdl_lvgl_display.c - Move SDL keyboard input, LVGL custom event registration, key metadata mapping, UTF-8 buffering, and Linux keycode translation into sdl_lvgl_keyboard.c - Add sdl_lvgl.h for shared SDL backend init entry points used by cp0_lvgl_init - Add an SDL cp0_file_path implementation hook and teach the CP0 file resolver to route png, wav, and ttf assets into image, audio, and font share directories --- .../cp0_lvgl/src/cp0/cp0_lvgl_file.cpp | 26 + ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c | 571 +----------------- ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h | 11 + .../cp0_lvgl/src/sdl/sdl_lvgl_display.c | 33 + .../cp0_lvgl/src/sdl/sdl_lvgl_file.cpp | 11 + .../cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c | 558 +++++++++++++++++ 6 files changed, 641 insertions(+), 569 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp index 656ae4a8..a0541a81 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp @@ -4,8 +4,34 @@ #include #include #include +#include std::string cp0_file_path(std::string file) { + std::regex pattern(R"(\.(png|wav|ttf)$)", std::regex::icase); + std::string root_path; + std::smatch m; + + // std::string root_path = "/usr/share/APPLaunch/"; + bool matched = std::regex_search(file, m, pattern); + + if (matched) { + std::string ext = m[1].str(); + + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { + return std::tolower(c); + }); + + if (ext == "png") { + return root_path + "share/images/" + file; + } else if (ext == "wav") { + return root_path + "share/audio/" + file; + } else if (ext == "ttf") { + return root_path + "share/font/" + file; + } + } + + return ""; // 或者 return ""; 或者 throw,根据你的需求 } \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c index 48c0ef53..658d48b0 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c @@ -1,572 +1,13 @@ #include "hal_lvgl_bsp.h" #include "lvgl/lvgl.h" -#if LV_USE_SDL - -#include "lvgl/src/drivers/sdl/lv_sdl_mouse.h" -#include "lvgl/src/drivers/sdl/lv_sdl_private.h" -#include "lvgl/src/drivers/sdl/lv_sdl_window.h" - #include #include #include #include #include - -#ifndef KEYBOARD_BUFFER_SIZE -#define KEYBOARD_BUFFER_SIZE 32 -#endif - -#define CP0_KBD_MOD_SHIFT (1u << 0) -#define CP0_KBD_MOD_CTRL (1u << 1) -#define CP0_KBD_MOD_ALT (1u << 2) -#define CP0_KBD_MOD_LOGO (1u << 3) - -uint32_t LV_C_EVENT_KEYBOARD; -uint32_t LV_C_EVENT_BATTERY; -uint32_t LV_C_EVENT_NETWORK; -uint32_t LV_C_EVENT_DATATIME; - -typedef struct { - char buf[KEYBOARD_BUFFER_SIZE]; - bool dummy_read; - - cp0_key_event_t current; - cp0_key_event_t last; - size_t last_utf8_len; - bool current_valid; -} cp0_sdl_keyboard_t; - -static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data); -static void cp0_sdl_keyboard_delete_cb(lv_event_t *event); - -void init_lvgl_event(void) -{ - LV_C_EVENT_KEYBOARD = lv_event_register_id(); - LV_C_EVENT_BATTERY = lv_event_register_id(); - LV_C_EVENT_NETWORK = lv_event_register_id(); - LV_C_EVENT_DATATIME = lv_event_register_id(); -} - -static const char *getenv_default(const char *name, const char *dflt) -{ - const char *value = getenv(name); - return (value && value[0] != '\0') ? value : dflt; -} - -static uint32_t cp0_utf8_first_codepoint(const char *s) -{ - const unsigned char *p = (const unsigned char *)s; - if (p[0] == '\0') - return 0; - if (p[0] < 0x80) - return p[0]; - if ((p[0] & 0xe0) == 0xc0 && (p[1] & 0xc0) == 0x80) - return ((uint32_t)(p[0] & 0x1f) << 6) | (uint32_t)(p[1] & 0x3f); - if ((p[0] & 0xf0) == 0xe0 && (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80) - return ((uint32_t)(p[0] & 0x0f) << 12) | ((uint32_t)(p[1] & 0x3f) << 6) | - (uint32_t)(p[2] & 0x3f); - if ((p[0] & 0xf8) == 0xf0 && (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80 && - (p[3] & 0xc0) == 0x80) - return ((uint32_t)(p[0] & 0x07) << 18) | ((uint32_t)(p[1] & 0x3f) << 12) | - ((uint32_t)(p[2] & 0x3f) << 6) | (uint32_t)(p[3] & 0x3f); - return 0; -} - -static size_t cp0_utf8_first_len(const char *s) -{ - const unsigned char ch = (unsigned char)s[0]; - if (ch == '\0') - return 0; - if (ch < 0x80) - return 1; - if ((ch & 0xe0) == 0xc0) - return 2; - if ((ch & 0xf0) == 0xe0) - return 3; - if ((ch & 0xf8) == 0xf0) - return 4; - return 1; -} - -static uint32_t cp0_sdl_ctrl_to_lv_key(SDL_Keycode key) -{ - switch (key) { - case SDLK_RIGHT: - case SDLK_KP_PLUS: - return LV_KEY_RIGHT; - case SDLK_LEFT: - case SDLK_KP_MINUS: - return LV_KEY_LEFT; - case SDLK_UP: - return LV_KEY_UP; - case SDLK_DOWN: - return LV_KEY_DOWN; - case SDLK_ESCAPE: - return LV_KEY_ESC; - case SDLK_BACKSPACE: - return LV_KEY_BACKSPACE; - case SDLK_DELETE: - return LV_KEY_DEL; - case SDLK_KP_ENTER: - case SDLK_RETURN: - return LV_KEY_ENTER; - case SDLK_TAB: - return LV_KEY_NEXT; - case SDLK_PAGEDOWN: - return LV_KEY_NEXT; - case SDLK_PAGEUP: - return LV_KEY_PREV; - case SDLK_HOME: - return LV_KEY_HOME; - case SDLK_END: - return LV_KEY_END; - default: - return 0; - } -} - -static const char *cp0_sdl_ctrl_utf8(SDL_Keycode key) -{ - switch (key) { - case SDLK_RETURN: - case SDLK_KP_ENTER: - return "\r"; - case SDLK_BACKSPACE: - return "\x7f"; - case SDLK_TAB: - return "\t"; - case SDLK_ESCAPE: - return "\x1b"; - case SDLK_UP: - return "\033[A"; - case SDLK_DOWN: - return "\033[B"; - case SDLK_RIGHT: - return "\033[C"; - case SDLK_LEFT: - return "\033[D"; - case SDLK_HOME: - return "\033[H"; - case SDLK_END: - return "\033[F"; - case SDLK_DELETE: - return "\033[3~"; - case SDLK_PAGEUP: - return "\033[5~"; - case SDLK_PAGEDOWN: - return "\033[6~"; - case SDLK_F1: - return "\033OP"; - case SDLK_F2: - return "\033OQ"; - case SDLK_F3: - return "\033OR"; - case SDLK_F4: - return "\033OS"; - case SDLK_F5: - return "\033[15~"; - case SDLK_F6: - return "\033[17~"; - case SDLK_F7: - return "\033[18~"; - case SDLK_F8: - return "\033[19~"; - case SDLK_F9: - return "\033[20~"; - case SDLK_F10: - return "\033[21~"; - case SDLK_F11: - return "\033[23~"; - case SDLK_F12: - return "\033[24~"; - default: - return NULL; - } -} - -static uint32_t cp0_sdl_scancode_to_linux_key(SDL_Scancode scancode) -{ - switch (scancode) { - case SDL_SCANCODE_A: - return KEY_A; - case SDL_SCANCODE_B: - return KEY_B; - case SDL_SCANCODE_C: - return KEY_C; - case SDL_SCANCODE_D: - return KEY_D; - case SDL_SCANCODE_E: - return KEY_E; - case SDL_SCANCODE_F: - return KEY_F; - case SDL_SCANCODE_G: - return KEY_G; - case SDL_SCANCODE_H: - return KEY_H; - case SDL_SCANCODE_I: - return KEY_I; - case SDL_SCANCODE_J: - return KEY_J; - case SDL_SCANCODE_K: - return KEY_K; - case SDL_SCANCODE_L: - return KEY_L; - case SDL_SCANCODE_M: - return KEY_M; - case SDL_SCANCODE_N: - return KEY_N; - case SDL_SCANCODE_O: - return KEY_O; - case SDL_SCANCODE_P: - return KEY_P; - case SDL_SCANCODE_Q: - return KEY_Q; - case SDL_SCANCODE_R: - return KEY_R; - case SDL_SCANCODE_S: - return KEY_S; - case SDL_SCANCODE_T: - return KEY_T; - case SDL_SCANCODE_U: - return KEY_U; - case SDL_SCANCODE_V: - return KEY_V; - case SDL_SCANCODE_W: - return KEY_W; - case SDL_SCANCODE_X: - return KEY_X; - case SDL_SCANCODE_Y: - return KEY_Y; - case SDL_SCANCODE_Z: - return KEY_Z; - case SDL_SCANCODE_1: - return KEY_1; - case SDL_SCANCODE_2: - return KEY_2; - case SDL_SCANCODE_3: - return KEY_3; - case SDL_SCANCODE_4: - return KEY_4; - case SDL_SCANCODE_5: - return KEY_5; - case SDL_SCANCODE_6: - return KEY_6; - case SDL_SCANCODE_7: - return KEY_7; - case SDL_SCANCODE_8: - return KEY_8; - case SDL_SCANCODE_9: - return KEY_9; - case SDL_SCANCODE_0: - return KEY_0; - case SDL_SCANCODE_RETURN: - return KEY_ENTER; - case SDL_SCANCODE_ESCAPE: - return KEY_ESC; - case SDL_SCANCODE_BACKSPACE: - return KEY_BACKSPACE; - case SDL_SCANCODE_TAB: - return KEY_TAB; - case SDL_SCANCODE_SPACE: - return KEY_SPACE; - case SDL_SCANCODE_MINUS: - return KEY_MINUS; - case SDL_SCANCODE_EQUALS: - return KEY_EQUAL; - case SDL_SCANCODE_LEFTBRACKET: - return KEY_LEFTBRACE; - case SDL_SCANCODE_RIGHTBRACKET: - return KEY_RIGHTBRACE; - case SDL_SCANCODE_BACKSLASH: - return KEY_BACKSLASH; - case SDL_SCANCODE_SEMICOLON: - return KEY_SEMICOLON; - case SDL_SCANCODE_APOSTROPHE: - return KEY_APOSTROPHE; - case SDL_SCANCODE_GRAVE: - return KEY_GRAVE; - case SDL_SCANCODE_COMMA: - return KEY_COMMA; - case SDL_SCANCODE_PERIOD: - return KEY_DOT; - case SDL_SCANCODE_SLASH: - return KEY_SLASH; - case SDL_SCANCODE_CAPSLOCK: - return KEY_CAPSLOCK; - case SDL_SCANCODE_F1: - return KEY_F1; - case SDL_SCANCODE_F2: - return KEY_F2; - case SDL_SCANCODE_F3: - return KEY_F3; - case SDL_SCANCODE_F4: - return KEY_F4; - case SDL_SCANCODE_F5: - return KEY_F5; - case SDL_SCANCODE_F6: - return KEY_F6; - case SDL_SCANCODE_F7: - return KEY_F7; - case SDL_SCANCODE_F8: - return KEY_F8; - case SDL_SCANCODE_F9: - return KEY_F9; - case SDL_SCANCODE_F10: - return KEY_F10; - case SDL_SCANCODE_F11: - return KEY_F11; - case SDL_SCANCODE_F12: - return KEY_F12; - case SDL_SCANCODE_INSERT: - return KEY_INSERT; - case SDL_SCANCODE_HOME: - return KEY_HOME; - case SDL_SCANCODE_PAGEUP: - return KEY_PAGEUP; - case SDL_SCANCODE_DELETE: - return KEY_DELETE; - case SDL_SCANCODE_END: - return KEY_END; - case SDL_SCANCODE_PAGEDOWN: - return KEY_PAGEDOWN; - case SDL_SCANCODE_RIGHT: - return KEY_RIGHT; - case SDL_SCANCODE_LEFT: - return KEY_LEFT; - case SDL_SCANCODE_DOWN: - return KEY_DOWN; - case SDL_SCANCODE_UP: - return KEY_UP; - case SDL_SCANCODE_LCTRL: - return KEY_LEFTCTRL; - case SDL_SCANCODE_LSHIFT: - return KEY_LEFTSHIFT; - case SDL_SCANCODE_LALT: - return KEY_LEFTALT; - case SDL_SCANCODE_LGUI: - return KEY_LEFTMETA; - case SDL_SCANCODE_RCTRL: - return KEY_RIGHTCTRL; - case SDL_SCANCODE_RSHIFT: - return KEY_RIGHTSHIFT; - case SDL_SCANCODE_RALT: - return KEY_RIGHTALT; - case SDL_SCANCODE_RGUI: - return KEY_RIGHTMETA; - default: - return (uint32_t)scancode; - } -} - -static void cp0_send_keyboard_event(const cp0_key_event_t *event) -{ - lv_obj_t *root = lv_display_get_screen_active(NULL); - if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)LV_C_EVENT_KEYBOARD, (void *)event); -} - -static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEvent *event) -{ - SDL_Keycode sym = event->keysym.sym; - SDL_Scancode scancode = event->keysym.scancode; - SDL_Keymod mods = SDL_GetModState(); - const char *name = SDL_GetKeyName(sym); - - memset(&kbd->current, 0, sizeof(kbd->current)); - kbd->current.key_code = cp0_sdl_scancode_to_linux_key(scancode); - kbd->current.keysym = (uint32_t)sym; - kbd->current.mods = 0; - kbd->current.key_state = LV_INDEV_STATE_PRESSED; - kbd->current.lv_key = cp0_sdl_ctrl_to_lv_key(sym); - if (mods & KMOD_SHIFT) - kbd->current.mods |= CP0_KBD_MOD_SHIFT; - if (mods & KMOD_CTRL) - kbd->current.mods |= CP0_KBD_MOD_CTRL; - if (mods & KMOD_ALT) - kbd->current.mods |= CP0_KBD_MOD_ALT; - if (mods & KMOD_GUI) - kbd->current.mods |= CP0_KBD_MOD_LOGO; - if (name != NULL) - snprintf(kbd->current.sym_name, sizeof(kbd->current.sym_name), "%s", name); - - const char *ctrl_utf8 = cp0_sdl_ctrl_utf8(sym); - if (ctrl_utf8 != NULL) - snprintf(kbd->current.utf8, sizeof(kbd->current.utf8), "%s", ctrl_utf8); - - kbd->current_valid = true; -} - -static void cp0_sdl_set_text_key(cp0_sdl_keyboard_t *kbd, const char *utf8, size_t len) -{ - if (!kbd->current_valid) { - memset(&kbd->current, 0, sizeof(kbd->current)); - kbd->current.key_state = LV_INDEV_STATE_PRESSED; - kbd->current_valid = true; - } - - if (kbd->current.lv_key != 0 && len == 1 && (uint8_t)utf8[0] == (uint8_t)kbd->current.lv_key) { - if (kbd->current.codepoint == 0) - kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); - return; - } - - size_t n = len < sizeof(kbd->current.utf8) - 1 ? len : sizeof(kbd->current.utf8) - 1; - memcpy(kbd->current.utf8, utf8, n); - kbd->current.utf8[n] = '\0'; - kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); - kbd->current.lv_key = kbd->current.codepoint; -} - -static void cp0_sdl_enqueue_text(cp0_sdl_keyboard_t *kbd, const char *text) -{ - size_t used = strlen(kbd->buf); - size_t incoming = strlen(text); - if (used + incoming >= sizeof(kbd->buf)) - incoming = sizeof(kbd->buf) - used - 1; - if (incoming > 0) { - memcpy(kbd->buf + used, text, incoming); - kbd->buf[used + incoming] = '\0'; - } -} - -static lv_indev_t *cp0_sdl_keyboard_create(void) -{ - cp0_sdl_keyboard_t *kbd = calloc(1, sizeof(*kbd)); - if (kbd == NULL) - return NULL; - - lv_indev_t *indev = lv_indev_create(); - if (indev == NULL) { - free(kbd); - return NULL; - } - - lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD); - lv_indev_set_read_cb(indev, cp0_sdl_keyboard_read); - lv_indev_set_driver_data(indev, kbd); - lv_indev_set_mode(indev, LV_INDEV_MODE_EVENT); - lv_indev_add_event_cb(indev, cp0_sdl_keyboard_delete_cb, LV_EVENT_DELETE, indev); - return indev; -} - -static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data) -{ - cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); - size_t len = strlen(kbd->buf); - data->continue_reading = false; - - if (kbd->dummy_read) { - kbd->dummy_read = false; - kbd->last.key_state = LV_INDEV_STATE_RELEASED; - data->key = kbd->last.lv_key; - data->state = LV_INDEV_STATE_RELEASED; - cp0_send_keyboard_event(&kbd->last); - memset(&kbd->last, 0, sizeof(kbd->last)); - kbd->last_utf8_len = 0; - return; - } - - if (len == 0) { - data->state = LV_INDEV_STATE_RELEASED; - return; - } - - size_t char_len = cp0_utf8_first_len(kbd->buf); - if (char_len > len) - char_len = len; - - cp0_sdl_set_text_key(kbd, kbd->buf, char_len); - kbd->current.key_state = LV_INDEV_STATE_PRESSED; - - kbd->last = kbd->current; - kbd->last_utf8_len = char_len; - data->key = kbd->current.lv_key; - data->state = LV_INDEV_STATE_PRESSED; - kbd->dummy_read = true; - cp0_send_keyboard_event(&kbd->current); - - memmove(kbd->buf, kbd->buf + char_len, len - char_len + 1); - kbd->current_valid = false; - data->continue_reading = kbd->buf[0] != '\0'; -} - -static void cp0_sdl_keyboard_delete_cb(lv_event_t *event) -{ - lv_indev_t *indev = (lv_indev_t *)lv_event_get_user_data(event); - cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); - if (kbd != NULL) { - lv_indev_set_driver_data(indev, NULL); - lv_indev_set_read_cb(indev, NULL); - free(kbd); - } -} - -void lv_sdl_keyboard_handler(SDL_Event *event) -{ - uint32_t win_id = UINT32_MAX; - switch (event->type) { - case SDL_KEYDOWN: - win_id = event->key.windowID; - break; - case SDL_TEXTINPUT: - win_id = event->text.windowID; - break; - default: - return; - } - - lv_display_t *disp = lv_sdl_get_disp_from_win_id(win_id); - lv_indev_t *indev = lv_indev_get_next(NULL); - while (indev != NULL) { - if (lv_indev_get_read_cb(indev) == cp0_sdl_keyboard_read) { - if (disp == NULL || lv_indev_get_display(indev) == disp) - break; - } - indev = lv_indev_get_next(indev); - } - if (indev == NULL) - return; - - cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); - if (event->type == SDL_KEYDOWN) { - cp0_sdl_fill_key_meta(kbd, &event->key); - uint32_t ctrl_key = cp0_sdl_ctrl_to_lv_key(event->key.keysym.sym); - if (ctrl_key == 0) - return; - - char ctrl_buf[2] = {(char)ctrl_key, '\0'}; - cp0_sdl_enqueue_text(kbd, ctrl_buf); - } - else if (event->type == SDL_TEXTINPUT) { - cp0_sdl_enqueue_text(kbd, event->text.text); - } - - while (kbd->buf[0] != '\0') { - lv_indev_read(indev); - lv_indev_read(indev); - } -} - -static void init_sdl_disp(void) -{ - int width = atoi(getenv_default("LV_SDL_VIDEO_WIDTH", "320")); - int height = atoi(getenv_default("LV_SDL_VIDEO_HEIGHT", "170")); - lv_display_t *disp = lv_sdl_window_create(width, height); - if (disp == NULL) { - fprintf(stderr, "cp0_lvgl: failed to create SDL display\n"); - return; - } - - lv_sdl_window_set_title(disp, getenv_default("LV_SDL_WINDOW_TITLE", "M5CardputerZero")); -} - -static void init_sdl_input(void) -{ - lv_sdl_mouse_create(); - if (cp0_sdl_keyboard_create() == NULL) - fprintf(stderr, "cp0_lvgl: failed to create SDL keyboard input\n"); -} +#include "sdl_lvgl.h" +#include "commount.h" void cp0_lvgl_init(void) { @@ -574,11 +15,3 @@ void cp0_lvgl_init(void) init_sdl_disp(); init_sdl_input(); } - -#else - -void cp0_lvgl_init(void) -{ -} - -#endif diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h new file mode 100644 index 00000000..303ced96 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h @@ -0,0 +1,11 @@ +#pragma once +#include +#ifdef __cplusplus +extern "C" +{ +#endif +void init_sdl_disp(); +void init_sdl_input(); +#ifdef __cplusplus +} +#endif diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c new file mode 100644 index 00000000..41067c51 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c @@ -0,0 +1,33 @@ +#include "hal_lvgl_bsp.h" +#include "lvgl/lvgl.h" +#include "sdl_lvgl.h" + + +#include "lvgl/src/drivers/sdl/lv_sdl_mouse.h" +#include "lvgl/src/drivers/sdl/lv_sdl_private.h" +#include "lvgl/src/drivers/sdl/lv_sdl_window.h" + +#include +#include +#include +#include +#include + +static const char *getenv_default(const char *name, const char *dflt) +{ + const char *value = getenv(name); + return (value && value[0] != '\0') ? value : dflt; +} + +void init_sdl_disp(void) +{ + int width = atoi(getenv_default("LV_SDL_VIDEO_WIDTH", "320")); + int height = atoi(getenv_default("LV_SDL_VIDEO_HEIGHT", "170")); + lv_display_t *disp = lv_sdl_window_create(width, height); + if (disp == NULL) { + fprintf(stderr, "cp0_lvgl: failed to create SDL display\n"); + return; + } + + lv_sdl_window_set_title(disp, getenv_default("LV_SDL_WINDOW_TITLE", "M5CardputerZero")); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp new file mode 100644 index 00000000..656ae4a8 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp @@ -0,0 +1,11 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include + +std::string cp0_file_path(std::string file) +{ + +} \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c new file mode 100644 index 00000000..bfb65613 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c @@ -0,0 +1,558 @@ +#include "hal_lvgl_bsp.h" +#include "lvgl/lvgl.h" +#include "sdl_lvgl.h" + + +#include "lvgl/src/drivers/sdl/lv_sdl_mouse.h" +#include "lvgl/src/drivers/sdl/lv_sdl_private.h" +#include "lvgl/src/drivers/sdl/lv_sdl_window.h" + +#include +#include +#include +#include +#include + +#ifndef KEYBOARD_BUFFER_SIZE +#define KEYBOARD_BUFFER_SIZE 32 +#endif + +#define CP0_KBD_MOD_SHIFT (1u << 0) +#define CP0_KBD_MOD_CTRL (1u << 1) +#define CP0_KBD_MOD_ALT (1u << 2) +#define CP0_KBD_MOD_LOGO (1u << 3) + +uint32_t LV_C_EVENT_KEYBOARD; +uint32_t LV_C_EVENT_BATTERY; +uint32_t LV_C_EVENT_NETWORK; +uint32_t LV_C_EVENT_DATATIME; + +typedef struct { + char buf[KEYBOARD_BUFFER_SIZE]; + bool dummy_read; + + cp0_key_event_t current; + cp0_key_event_t last; + size_t last_utf8_len; + bool current_valid; +} cp0_sdl_keyboard_t; + +static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data); +static void cp0_sdl_keyboard_delete_cb(lv_event_t *event); + +void init_lvgl_event(void) +{ + LV_C_EVENT_KEYBOARD = lv_event_register_id(); + LV_C_EVENT_BATTERY = lv_event_register_id(); + LV_C_EVENT_NETWORK = lv_event_register_id(); + LV_C_EVENT_DATATIME = lv_event_register_id(); +} + +static const char *getenv_default(const char *name, const char *dflt) +{ + const char *value = getenv(name); + return (value && value[0] != '\0') ? value : dflt; +} + +static uint32_t cp0_utf8_first_codepoint(const char *s) +{ + const unsigned char *p = (const unsigned char *)s; + if (p[0] == '\0') + return 0; + if (p[0] < 0x80) + return p[0]; + if ((p[0] & 0xe0) == 0xc0 && (p[1] & 0xc0) == 0x80) + return ((uint32_t)(p[0] & 0x1f) << 6) | (uint32_t)(p[1] & 0x3f); + if ((p[0] & 0xf0) == 0xe0 && (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80) + return ((uint32_t)(p[0] & 0x0f) << 12) | ((uint32_t)(p[1] & 0x3f) << 6) | + (uint32_t)(p[2] & 0x3f); + if ((p[0] & 0xf8) == 0xf0 && (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80 && + (p[3] & 0xc0) == 0x80) + return ((uint32_t)(p[0] & 0x07) << 18) | ((uint32_t)(p[1] & 0x3f) << 12) | + ((uint32_t)(p[2] & 0x3f) << 6) | (uint32_t)(p[3] & 0x3f); + return 0; +} + +static size_t cp0_utf8_first_len(const char *s) +{ + const unsigned char ch = (unsigned char)s[0]; + if (ch == '\0') + return 0; + if (ch < 0x80) + return 1; + if ((ch & 0xe0) == 0xc0) + return 2; + if ((ch & 0xf0) == 0xe0) + return 3; + if ((ch & 0xf8) == 0xf0) + return 4; + return 1; +} + +static uint32_t cp0_sdl_ctrl_to_lv_key(SDL_Keycode key) +{ + switch (key) { + case SDLK_RIGHT: + case SDLK_KP_PLUS: + return LV_KEY_RIGHT; + case SDLK_LEFT: + case SDLK_KP_MINUS: + return LV_KEY_LEFT; + case SDLK_UP: + return LV_KEY_UP; + case SDLK_DOWN: + return LV_KEY_DOWN; + case SDLK_ESCAPE: + return LV_KEY_ESC; + case SDLK_BACKSPACE: + return LV_KEY_BACKSPACE; + case SDLK_DELETE: + return LV_KEY_DEL; + case SDLK_KP_ENTER: + case SDLK_RETURN: + return LV_KEY_ENTER; + case SDLK_TAB: + return LV_KEY_NEXT; + case SDLK_PAGEDOWN: + return LV_KEY_NEXT; + case SDLK_PAGEUP: + return LV_KEY_PREV; + case SDLK_HOME: + return LV_KEY_HOME; + case SDLK_END: + return LV_KEY_END; + default: + return 0; + } +} + +static const char *cp0_sdl_ctrl_utf8(SDL_Keycode key) +{ + switch (key) { + case SDLK_RETURN: + case SDLK_KP_ENTER: + return "\r"; + case SDLK_BACKSPACE: + return "\x7f"; + case SDLK_TAB: + return "\t"; + case SDLK_ESCAPE: + return "\x1b"; + case SDLK_UP: + return "\033[A"; + case SDLK_DOWN: + return "\033[B"; + case SDLK_RIGHT: + return "\033[C"; + case SDLK_LEFT: + return "\033[D"; + case SDLK_HOME: + return "\033[H"; + case SDLK_END: + return "\033[F"; + case SDLK_DELETE: + return "\033[3~"; + case SDLK_PAGEUP: + return "\033[5~"; + case SDLK_PAGEDOWN: + return "\033[6~"; + case SDLK_F1: + return "\033OP"; + case SDLK_F2: + return "\033OQ"; + case SDLK_F3: + return "\033OR"; + case SDLK_F4: + return "\033OS"; + case SDLK_F5: + return "\033[15~"; + case SDLK_F6: + return "\033[17~"; + case SDLK_F7: + return "\033[18~"; + case SDLK_F8: + return "\033[19~"; + case SDLK_F9: + return "\033[20~"; + case SDLK_F10: + return "\033[21~"; + case SDLK_F11: + return "\033[23~"; + case SDLK_F12: + return "\033[24~"; + default: + return NULL; + } +} + +static uint32_t cp0_sdl_scancode_to_linux_key(SDL_Scancode scancode) +{ + switch (scancode) { + case SDL_SCANCODE_A: + return KEY_A; + case SDL_SCANCODE_B: + return KEY_B; + case SDL_SCANCODE_C: + return KEY_C; + case SDL_SCANCODE_D: + return KEY_D; + case SDL_SCANCODE_E: + return KEY_E; + case SDL_SCANCODE_F: + return KEY_F; + case SDL_SCANCODE_G: + return KEY_G; + case SDL_SCANCODE_H: + return KEY_H; + case SDL_SCANCODE_I: + return KEY_I; + case SDL_SCANCODE_J: + return KEY_J; + case SDL_SCANCODE_K: + return KEY_K; + case SDL_SCANCODE_L: + return KEY_L; + case SDL_SCANCODE_M: + return KEY_M; + case SDL_SCANCODE_N: + return KEY_N; + case SDL_SCANCODE_O: + return KEY_O; + case SDL_SCANCODE_P: + return KEY_P; + case SDL_SCANCODE_Q: + return KEY_Q; + case SDL_SCANCODE_R: + return KEY_R; + case SDL_SCANCODE_S: + return KEY_S; + case SDL_SCANCODE_T: + return KEY_T; + case SDL_SCANCODE_U: + return KEY_U; + case SDL_SCANCODE_V: + return KEY_V; + case SDL_SCANCODE_W: + return KEY_W; + case SDL_SCANCODE_X: + return KEY_X; + case SDL_SCANCODE_Y: + return KEY_Y; + case SDL_SCANCODE_Z: + return KEY_Z; + case SDL_SCANCODE_1: + return KEY_1; + case SDL_SCANCODE_2: + return KEY_2; + case SDL_SCANCODE_3: + return KEY_3; + case SDL_SCANCODE_4: + return KEY_4; + case SDL_SCANCODE_5: + return KEY_5; + case SDL_SCANCODE_6: + return KEY_6; + case SDL_SCANCODE_7: + return KEY_7; + case SDL_SCANCODE_8: + return KEY_8; + case SDL_SCANCODE_9: + return KEY_9; + case SDL_SCANCODE_0: + return KEY_0; + case SDL_SCANCODE_RETURN: + return KEY_ENTER; + case SDL_SCANCODE_ESCAPE: + return KEY_ESC; + case SDL_SCANCODE_BACKSPACE: + return KEY_BACKSPACE; + case SDL_SCANCODE_TAB: + return KEY_TAB; + case SDL_SCANCODE_SPACE: + return KEY_SPACE; + case SDL_SCANCODE_MINUS: + return KEY_MINUS; + case SDL_SCANCODE_EQUALS: + return KEY_EQUAL; + case SDL_SCANCODE_LEFTBRACKET: + return KEY_LEFTBRACE; + case SDL_SCANCODE_RIGHTBRACKET: + return KEY_RIGHTBRACE; + case SDL_SCANCODE_BACKSLASH: + return KEY_BACKSLASH; + case SDL_SCANCODE_SEMICOLON: + return KEY_SEMICOLON; + case SDL_SCANCODE_APOSTROPHE: + return KEY_APOSTROPHE; + case SDL_SCANCODE_GRAVE: + return KEY_GRAVE; + case SDL_SCANCODE_COMMA: + return KEY_COMMA; + case SDL_SCANCODE_PERIOD: + return KEY_DOT; + case SDL_SCANCODE_SLASH: + return KEY_SLASH; + case SDL_SCANCODE_CAPSLOCK: + return KEY_CAPSLOCK; + case SDL_SCANCODE_F1: + return KEY_F1; + case SDL_SCANCODE_F2: + return KEY_F2; + case SDL_SCANCODE_F3: + return KEY_F3; + case SDL_SCANCODE_F4: + return KEY_F4; + case SDL_SCANCODE_F5: + return KEY_F5; + case SDL_SCANCODE_F6: + return KEY_F6; + case SDL_SCANCODE_F7: + return KEY_F7; + case SDL_SCANCODE_F8: + return KEY_F8; + case SDL_SCANCODE_F9: + return KEY_F9; + case SDL_SCANCODE_F10: + return KEY_F10; + case SDL_SCANCODE_F11: + return KEY_F11; + case SDL_SCANCODE_F12: + return KEY_F12; + case SDL_SCANCODE_INSERT: + return KEY_INSERT; + case SDL_SCANCODE_HOME: + return KEY_HOME; + case SDL_SCANCODE_PAGEUP: + return KEY_PAGEUP; + case SDL_SCANCODE_DELETE: + return KEY_DELETE; + case SDL_SCANCODE_END: + return KEY_END; + case SDL_SCANCODE_PAGEDOWN: + return KEY_PAGEDOWN; + case SDL_SCANCODE_RIGHT: + return KEY_RIGHT; + case SDL_SCANCODE_LEFT: + return KEY_LEFT; + case SDL_SCANCODE_DOWN: + return KEY_DOWN; + case SDL_SCANCODE_UP: + return KEY_UP; + case SDL_SCANCODE_LCTRL: + return KEY_LEFTCTRL; + case SDL_SCANCODE_LSHIFT: + return KEY_LEFTSHIFT; + case SDL_SCANCODE_LALT: + return KEY_LEFTALT; + case SDL_SCANCODE_LGUI: + return KEY_LEFTMETA; + case SDL_SCANCODE_RCTRL: + return KEY_RIGHTCTRL; + case SDL_SCANCODE_RSHIFT: + return KEY_RIGHTSHIFT; + case SDL_SCANCODE_RALT: + return KEY_RIGHTALT; + case SDL_SCANCODE_RGUI: + return KEY_RIGHTMETA; + default: + return (uint32_t)scancode; + } +} + +static void cp0_send_keyboard_event(const cp0_key_event_t *event) +{ + lv_obj_t *root = lv_display_get_screen_active(NULL); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)LV_C_EVENT_KEYBOARD, (void *)event); +} + +static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEvent *event) +{ + SDL_Keycode sym = event->keysym.sym; + SDL_Scancode scancode = event->keysym.scancode; + SDL_Keymod mods = SDL_GetModState(); + const char *name = SDL_GetKeyName(sym); + + memset(&kbd->current, 0, sizeof(kbd->current)); + kbd->current.key_code = cp0_sdl_scancode_to_linux_key(scancode); + kbd->current.keysym = (uint32_t)sym; + kbd->current.mods = 0; + kbd->current.key_state = LV_INDEV_STATE_PRESSED; + kbd->current.lv_key = cp0_sdl_ctrl_to_lv_key(sym); + if (mods & KMOD_SHIFT) + kbd->current.mods |= CP0_KBD_MOD_SHIFT; + if (mods & KMOD_CTRL) + kbd->current.mods |= CP0_KBD_MOD_CTRL; + if (mods & KMOD_ALT) + kbd->current.mods |= CP0_KBD_MOD_ALT; + if (mods & KMOD_GUI) + kbd->current.mods |= CP0_KBD_MOD_LOGO; + if (name != NULL) + snprintf(kbd->current.sym_name, sizeof(kbd->current.sym_name), "%s", name); + + const char *ctrl_utf8 = cp0_sdl_ctrl_utf8(sym); + if (ctrl_utf8 != NULL) + snprintf(kbd->current.utf8, sizeof(kbd->current.utf8), "%s", ctrl_utf8); + + kbd->current_valid = true; +} + +static void cp0_sdl_set_text_key(cp0_sdl_keyboard_t *kbd, const char *utf8, size_t len) +{ + if (!kbd->current_valid) { + memset(&kbd->current, 0, sizeof(kbd->current)); + kbd->current.key_state = LV_INDEV_STATE_PRESSED; + kbd->current_valid = true; + } + + if (kbd->current.lv_key != 0 && len == 1 && (uint8_t)utf8[0] == (uint8_t)kbd->current.lv_key) { + if (kbd->current.codepoint == 0) + kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); + return; + } + + size_t n = len < sizeof(kbd->current.utf8) - 1 ? len : sizeof(kbd->current.utf8) - 1; + memcpy(kbd->current.utf8, utf8, n); + kbd->current.utf8[n] = '\0'; + kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); + kbd->current.lv_key = kbd->current.codepoint; +} + +static void cp0_sdl_enqueue_text(cp0_sdl_keyboard_t *kbd, const char *text) +{ + size_t used = strlen(kbd->buf); + size_t incoming = strlen(text); + if (used + incoming >= sizeof(kbd->buf)) + incoming = sizeof(kbd->buf) - used - 1; + if (incoming > 0) { + memcpy(kbd->buf + used, text, incoming); + kbd->buf[used + incoming] = '\0'; + } +} + +static lv_indev_t *cp0_sdl_keyboard_create(void) +{ + cp0_sdl_keyboard_t *kbd = calloc(1, sizeof(*kbd)); + if (kbd == NULL) + return NULL; + + lv_indev_t *indev = lv_indev_create(); + if (indev == NULL) { + free(kbd); + return NULL; + } + + lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(indev, cp0_sdl_keyboard_read); + lv_indev_set_driver_data(indev, kbd); + lv_indev_set_mode(indev, LV_INDEV_MODE_EVENT); + lv_indev_add_event_cb(indev, cp0_sdl_keyboard_delete_cb, LV_EVENT_DELETE, indev); + return indev; +} + +static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data) +{ + cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); + size_t len = strlen(kbd->buf); + data->continue_reading = false; + + if (kbd->dummy_read) { + kbd->dummy_read = false; + kbd->last.key_state = LV_INDEV_STATE_RELEASED; + data->key = kbd->last.lv_key; + data->state = LV_INDEV_STATE_RELEASED; + cp0_send_keyboard_event(&kbd->last); + memset(&kbd->last, 0, sizeof(kbd->last)); + kbd->last_utf8_len = 0; + return; + } + + if (len == 0) { + data->state = LV_INDEV_STATE_RELEASED; + return; + } + + size_t char_len = cp0_utf8_first_len(kbd->buf); + if (char_len > len) + char_len = len; + + cp0_sdl_set_text_key(kbd, kbd->buf, char_len); + kbd->current.key_state = LV_INDEV_STATE_PRESSED; + + kbd->last = kbd->current; + kbd->last_utf8_len = char_len; + data->key = kbd->current.lv_key; + data->state = LV_INDEV_STATE_PRESSED; + kbd->dummy_read = true; + cp0_send_keyboard_event(&kbd->current); + + memmove(kbd->buf, kbd->buf + char_len, len - char_len + 1); + kbd->current_valid = false; + data->continue_reading = kbd->buf[0] != '\0'; +} + +static void cp0_sdl_keyboard_delete_cb(lv_event_t *event) +{ + lv_indev_t *indev = (lv_indev_t *)lv_event_get_user_data(event); + cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); + if (kbd != NULL) { + lv_indev_set_driver_data(indev, NULL); + lv_indev_set_read_cb(indev, NULL); + free(kbd); + } +} + +void lv_sdl_keyboard_handler(SDL_Event *event) +{ + uint32_t win_id = UINT32_MAX; + switch (event->type) { + case SDL_KEYDOWN: + win_id = event->key.windowID; + break; + case SDL_TEXTINPUT: + win_id = event->text.windowID; + break; + default: + return; + } + + lv_display_t *disp = lv_sdl_get_disp_from_win_id(win_id); + lv_indev_t *indev = lv_indev_get_next(NULL); + while (indev != NULL) { + if (lv_indev_get_read_cb(indev) == cp0_sdl_keyboard_read) { + if (disp == NULL || lv_indev_get_display(indev) == disp) + break; + } + indev = lv_indev_get_next(indev); + } + if (indev == NULL) + return; + + cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); + if (event->type == SDL_KEYDOWN) { + cp0_sdl_fill_key_meta(kbd, &event->key); + uint32_t ctrl_key = cp0_sdl_ctrl_to_lv_key(event->key.keysym.sym); + if (ctrl_key == 0) + return; + + char ctrl_buf[2] = {(char)ctrl_key, '\0'}; + cp0_sdl_enqueue_text(kbd, ctrl_buf); + } + else if (event->type == SDL_TEXTINPUT) { + cp0_sdl_enqueue_text(kbd, event->text.text); + } + + while (kbd->buf[0] != '\0') { + lv_indev_read(indev); + lv_indev_read(indev); + } +} + + + +void init_sdl_input(void) +{ + lv_sdl_mouse_create(); + if (cp0_sdl_keyboard_create() == NULL) + fprintf(stderr, "cp0_lvgl: failed to create SDL keyboard input\n"); +} From 45535b0f4a3155af898ecb7f65f191b005891a41 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 00:22:27 +0800 Subject: [PATCH 04/70] Add Compass app UI assets and defaults Introduce a new Compass page for APPLaunch with a 320x170 LVGL layout, including a compass disc panel, IMU level indicator, yaw/ACC/GYR labels, bottom soft-key labels, and keyboard handling for return/calibration actions. Add the SVG icon font and compass disc image assets required by the new UI. Enable the sigslot, cp0_lvgl, and eventpp components across APPLaunch default configuration variants so the staged UI can build consistently on supported targets. --- .../APPLaunch/share/font/svgfont.ttf | Bin 0 -> 3288 bytes .../share/images/compass_disc_transparent.png | Bin 0 -> 2797 bytes projects/APPLaunch/config_defaults.mk | 5 +- projects/APPLaunch/darwin_config_defaults.mk | 5 +- .../linux_x86_cross_cp0_config_defaults.mk | 5 +- .../linux_x86_sdl2_config_defaults.mk | 5 +- .../mac_cross_cp0_config_defaults.mk | 5 +- .../ui/components/page_app/ui_app_compass.hpp | 378 ++++++++++++++++++ 8 files changed, 398 insertions(+), 5 deletions(-) create mode 100644 projects/APPLaunch/APPLaunch/share/font/svgfont.ttf create mode 100644 projects/APPLaunch/APPLaunch/share/images/compass_disc_transparent.png create mode 100644 projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp diff --git a/projects/APPLaunch/APPLaunch/share/font/svgfont.ttf b/projects/APPLaunch/APPLaunch/share/font/svgfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..810921ebb8e4f7a30723fdb2d1e54fe3609f2cd9 GIT binary patch literal 3288 zcmd^BU2Gd!6+UD+D~jzJbI(L9y6fKt%;)JDfY7giV!Y zd1tIM^PTUW`}du5#{&ca6!;oY7(0FL^K;&B4PFNzpGNkDXU~mBpB#VwRrI&ezqnFL zS7MnDZ2(+1av$W1*Rr#g*--%A3m^{UwR9%x&*m`pJ+x#V3Hg%v2I3#14dhGp_3>}- zwy^J?(H}0BSJL7?OfO=86Z+ky^m>K3;T3Gh@xi;6E@=zC!ao7zQ#hwwDc9(bKp{R2H8iyv%S51Gb%+)3@Jck?h&&tMew{ zjQ(sS1#W?;`;3+G2e7OAxI~>9gK6MS%`d(Hwqm-z2FHN^Nc%@IDHd{RVEbe}3C6Kj zf(+5;U@XsXklP?2NODZEt+^W`*a5GHb_I(ckv=W2bd*&rvF6i zJEMQvZHXRK42{2m`u(M&A?qi;*|Cm7Y#mq?m?tz`E6Ez;T|}0xb{L8d!DASoJl`9m5=8raGqAH8#T)*n;npIr0Va zb@B`HklUe0fn%qGKBB*cPr*3=yXtVBAc=S~=Ck|!!6_2(i+#l5Au(TcA~_LHChYMD z3Hbb?N*$8ua=M%o80%EgQIiwN8Ip`e6BD6eh%p$4G?iDW%r#rQOjl*unIIN9V5bd&QU&sk4>&id;zn1ytBi{TQD&G&D z^c}VLi$fy!LFlCK^i=z2j=p1(8OI@FxI5~RtclN<=R_iAvgX&#lp_@7&~--f`mz4; zsNr7CJoi@X0?l(jY%S0SZA;tIxgQd17DP|$i`@5`ueLtRz5Q6QUg`Wz`bV%s01_|{ z=OK?NcDS4!*^;9q&NQ?C4-EG55R^KJ8U`gBQL)n}5ac_OWq|vps+c%JNVs)H^?x$a zE<4p(ug~EU#L&>t;P`*@=%(M^ugFK09yRQ5f7yT3qnNvtZf7{qUJls%EzH3dDu&!W zPbii?zi-fSOb{l+?Tt`xx80(6eEz{McZV6$V|3=2z)yyaywJa5UMQ<)JC+_{R%zen zHrSm$Xm&H(%OQP-z5@<$0d%3FE+;F^WSj|x^(KjfW)hm2$`}7t%umhi1M&>1x3|c{ zuN+`m^LG3F_WS!||56_tmv!jauEQoAnwc(?nx(?@%s%)*C^-AnYfsGvL;HYyc=JFA zi@o{Gfe;p}zZ!lNk==oIcPA+vmn$X`Fc)LOG;@hJeCg4j@f=wLV?g}Jgc9%cG z#izMg6idy43U+2W2agYL%QJmqO&-BPhg9UNTM!~9zCIkVy?hMOiH){ zA(w{19lLe^))AK~%c|>$N0ISTiF{(9S8D%s4EAlhuaRiZY@YBRePNu)NxY87PBW;IzKY;V_7;pNiyc4;|$vH33jY3j_v z@sW|^3ujWz={b~+LE<8J#?H3qC0TWPoT}_l@FA;i)#7yb_xQbMBEy5dy$-9&Jl~m- z;h;OkbCO^)sp03t)-FX5DQ^;}N$8P~VC#}N{_MTvy^W2XDLK)Ve4rf6_?=tQy4b{im4n{6vB#S8M zJ-t{F5g2={$7zgN)8o=OwgwsS!X>zd_s&>RWa~J$7p;opln~7uE75QbMZ{jH=xwXm ze+8o>_4hqOcY`PURy8J zE!lLfzML&rucoURMR%&&)xuhaFBWQbp_0B_)A&j;eNC)tD`gC+rAw8fw#+gi)GC^m L;cNAB1?T-Y(-kW` literal 0 HcmV?d00001 diff --git a/projects/APPLaunch/APPLaunch/share/images/compass_disc_transparent.png b/projects/APPLaunch/APPLaunch/share/images/compass_disc_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..9050b865f222cc0685d9e8c4d860fc248405d30c GIT binary patch literal 2797 zcmV zKWH0S9>>3XsCZe#F@Y^4a1lWY3tU7d0ZZ(`Vs0D578g_*-$G(vvBXDU$#ERGI|nyO zaU{;dZEVBcuCF+4wg{N3(%c~h99Ag|F@ArQ1KcLZQG^yeA-RXyM=OIo$8FvO$AtW55{r`=e(06|N>rYevd@|$C zKoEjj;gz?q008g5SPYy_+C)!{z)DATYps)L~``+GyLRS`v=RgfrwBYQ|a4t%VC0ranXTzN7;@`O%=z_Jg8 z_c%7c+w{MWe-!cH6~MXsjs9!ydpy|uZWCOxKqN)zu?igYA&?Bg@*kG5a&H9yK-<)L z@74d#EY&);0037S7hSUbT8p+EksEIs&NXAqxwm=Hl+B{UxP=}|kfq`Wvo=FvssP3q z-t+g)>+_$_JJ-eOV!!QsG(@1ICcr2P*9y){<(p;anse^_GYkVHLqg@({>^a*W3dYy zMqKN}LSgDmkkYLZ%tI69x$@`_WeDDX^JwIIqsZm@ogvM)P0ZeybzW~CG*Oxf9)X{} zJN*!+$g#TUAmX~+?-g>zxz6Qsn4OtLeZTJg1z7{QULO80Yxt&L->+l-H!iPmz35z< zN2c)aa1*D?j-@2|{h-F121+v}5Yo3OyB;KjfKuvM(|K5z!4MEB3|?v2@7V#E?Z!Fh zXdW~NT>dDcw&ut!SfsR;ZP{%a%;TFfjkcJeqHV zH%XEJ?{ZY;DloFfKvT<)s4Q8zz_RT=x1g1B1WA-Mt&-f+h5%VAl8ZECW%NifDh2yuc)5mdQsWfm+`@>=Z{jL!^JNhcnXC4ss+RiqtjJD7DD z0Q8$gA$x|>1j!I5;4%+`wVi{aMBrhijdOmX)begLBRFZIaJ?|_H}CP0b+rmB4lVGg zOb|k_^z{;eGt0d!b*>k-=dydjNQPkP?$XHn!-^bAV86s`yERl7tJ-phU=MOs7pti4 z);#YGBXVGYMG|FbI*wlbKjZhM>Aw#q+;E7rH;F$dLtEv`wAvyV`DTV4U`{ zV?T@9Zp|5zjEX>Qtt8A;xU9&wkEJcxPh7^B(~+SOr}3sCbf2toNtFg|xyeD)l9krM z=8-AgW1|_wJ?y9gVG9R`?}^0_=p zH%i!hseVD%Wo7T>9!fV#{b@fR2Wo=iX~DYPn;u44abOE>zHREjDh9Ʋy5ZN14s zw0~ik6(?f(sK%QxEtEh4H(rZ%jBC`udpz)b@u4nSyXW+B-c3teunw%!g|xutLA=vG zHGO!G3y&GQx!|IQPJunQJW?}W{Pb|UX`#O8t?#$cJZK`H%cEQ_V|Hd%Pl1gYN-2Ih z_yvtd11;J@X{LmG-`w;4oR9_E*wJ%+zmDZQ%dxD_)%53;Z|!WMa;t()w}XY>E_mMW zwqPlNHR-@DuyBy&wZuXbAF{2O0fD`&*E}?(7Bp+3fp@vH)x{{>I({R{`{f_0>J-Kr)y)`=zDv#6u!2}QauTGzknL`Y!PWdpmgYsO3Spefw5s9p4gBJH9z z=|osy^U%}-4hcYZezbPc6N+}xmxKX<{q|k5U4w2i#gF~WMIz`x(`G59=&%kRZal>5 z{ngNWM`*-hU53^Bt9ZEa5G^V`77PeZe7Dcft=y`hI9-Hc7@;z(2?k>f=AnuDejOx3 zMlM~EUfEy|G_Wp1r`;Jil2sEQ?&FBO27mO@l?lhQ4)zPQhMlIONe-b`__x|EL~7E+9~Xe;n^DZSI(1R-U?zR`4# z$9~Y_WyC%L&yP`hh1zY_UbD2dvxTe1RowmkU7^fqyxIQeHae_>60p&aF#WueS!!r}F-{bHrS1ODd|^xGIjBhK!>cL)9sV@S#l#$t?tWQkth z5)#fiSeJ!9jKfCh$!D5)QF`*3=wXO58-!>Ev5qa_nX96D=+j42N+hR`ihe2UGQFpV z1dK7MIp;EmL`z4}B42J~jSKBIeZ>127oJ5%{7}lsPXHH9gb*^Y{nE2%&w?a1k<5QS zkLqI883GjDmRVL@AmWMdBtyU$Lv^u=+HNh%q*BKZ|M&q*UoS)3Mo?s<;` zJ?}a=sJMUvfdRSZ6JzYsPMl|5CVdP-7;ynbb+Ak(2qBkpJ{P4_x*IpFxWE`z7ZO68 zQhXVNwx<~9ayj9qkV=#Z9Hg{$+MR*FX`5nuXIoX4VrP42`@-Did@dj8yHr>&pxAwFAevGDE4uG3tuisb?ihXE)YW$ zEt2{2&GLo1iev~1*ZXz!sD)P#{`?^F9x_76g+f6ob%Y-_orVx1Jk2IbaRI@@DZ^s_ z2-$ksZa3y~IcLzqI<{b)SeTueMPaHC`4l0NAt+20U>=#cn!Va@?qo^ma~q=;)+W|= zaF`!uTX^AG0kbz|VIG>uPvxC)XVzs(tl2#RMk$4LY@u|kgcfZ%E;udX0%PdtQ1Ro8 ztbzJ|9SdJBI1xAJXTrFT;iGLjv>s*bY*K +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * ============================================================ + * UICompassPage + * + * Compass + IMU dashboard + * Screen: 320 x 170 + * + * 按键: + * F4 预留:校准接口 + * F6/ESC 返回主页 + * ============================================================ + */ +class UICompassPage : public app_ +{ + static constexpr int kScreenW = 320; + static constexpr int kScreenH = 170; + static constexpr int kStatusH = 30; + static constexpr int kBottomH = 25; + static constexpr int kBtnW = kScreenW / 5; + static constexpr int kCompassDia = 116; + static constexpr int kLevelDia = 100; + + static constexpr uint32_t kColorBg = 0x000000; + static constexpr uint32_t kColorText = 0xFFFFFF; + static constexpr uint32_t kColorTextGray = 0xAAAAAA; + static constexpr uint32_t kColorLevelDisc = 0x222222; + static constexpr uint32_t kColorLevelBorder = 0x555555; + static constexpr uint32_t kColorCenterDot = 0x888888; + static constexpr uint32_t kColorLevelMove = 0xE74C3C; + static constexpr uint32_t kColorIconList = 0x33CC33; + static constexpr uint32_t kColorIconExit = 0xFF0000; + + static constexpr const char* ICON_EXIT = "\uEA01"; // .svgfont-exit + static constexpr const char* ICON_LIST = "\uEA04"; // .svgfont-list + +public: + UICompassPage() : app_() + { + app_name = "Compass"; + svg_font_ = lv_freetype_font_create( + cp0_file_path("svgfont.ttf").c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, + LV_FREETYPE_FONT_STYLE_NORMAL); + creat_UI(); + event_handler_init(); + } + + ~UICompassPage() + { + lv_freetype_font_delete(svg_font_); + // TODO(compass): 接入真实 Compass/IMU 接口后,在这里停止传感器轮询/释放资源。 + } + +private: + struct CompassUiState { + std::string statusText = "Compass"; + float yaw = 0.0f; + float accX = 0.0f; + float accY = 0.0f; + float accZ = 0.0f; + float gyrX = 0.0f; + float gyrY = 0.0f; + float gyrZ = 0.0f; + bool sensorReady = false; + }; + + lv_font_t* svg_font_ = nullptr; + + lv_obj_t* lbl_status_text_ = nullptr; + lv_obj_t* compass_disc_ = nullptr; + lv_obj_t* lbl_compass_title_ = nullptr; + lv_obj_t* lbl_yaw_ = nullptr; + lv_obj_t* lbl_imu_title_ = nullptr; + lv_obj_t* level_disc_ = nullptr; + lv_obj_t* center_dot_ = nullptr; + lv_obj_t* level_dot_ = nullptr; + lv_obj_t* lbl_acc_ = nullptr; + lv_obj_t* lbl_gyr_ = nullptr; + std::array lbl_bottom_btns_{}; + std::array lbl_bottom_indicators_{}; + + CompassUiState last_state_{}; + + static lv_color_t color(uint32_t hex) + { + return lv_color_hex(hex); + } + + /* + * ============================================================ + * UI 构建 + * ============================================================ + */ + void creat_UI() + { + lv_obj_set_size(ui_root, kScreenW, kScreenH); + lv_obj_clear_flag(ui_root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(ui_root, color(kColorBg), 0); + lv_obj_set_style_bg_opa(ui_root, LV_OPA_COVER, 0); + + create_status_bar(ui_root); + create_compass_panel(ui_root); + create_imu_panel(ui_root); + create_bottom_bar(ui_root); + + // TODO(compass): 接入真实传感器后,在这里启动数据接口/定时 poll,并调用 update_from_state(state)。 + update_from_state(CompassUiState{}); + } + + void create_status_bar(lv_obj_t* parent) + { + lbl_status_text_ = lv_label_create(parent); + lv_obj_set_pos(lbl_status_text_, 0, 6); + lv_obj_set_width(lbl_status_text_, kScreenW); + lv_obj_set_style_text_font(lbl_status_text_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_status_text_, color(kColorText), 0); + lv_obj_set_style_text_align(lbl_status_text_, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(lbl_status_text_, "Compass"); + } + + void create_compass_panel(lv_obj_t* parent) + { + lbl_compass_title_ = lv_label_create(parent); + lv_label_set_text(lbl_compass_title_, "Compass"); + lv_obj_set_style_text_font(lbl_compass_title_, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_compass_title_, color(kColorText), 0); + lv_obj_set_size(lbl_compass_title_, 160, 16); + lv_obj_set_style_text_align(lbl_compass_title_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_pos(lbl_compass_title_, 0, kStatusH - 20); + + compass_disc_ = lv_img_create(parent); + lv_img_set_src(compass_disc_, cp0_file_path("compass_disc_transparent.png").c_str()); + lv_obj_set_pos(compass_disc_, 12, kStatusH + 1); + lv_obj_set_size(compass_disc_, kCompassDia, kCompassDia); + lv_obj_clear_flag(compass_disc_, LV_OBJ_FLAG_SCROLLABLE); + + lbl_yaw_ = lv_label_create(parent); + lv_label_set_text(lbl_yaw_, "---"); + lv_obj_set_style_text_font(lbl_yaw_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_yaw_, color(kColorText), 0); + lv_obj_set_size(lbl_yaw_, 160, 14); + lv_obj_set_style_text_align(lbl_yaw_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_pos(lbl_yaw_, 0, kScreenH - kBottomH - 24); + } + + void create_imu_panel(lv_obj_t* parent) + { + lbl_imu_title_ = lv_label_create(parent); + lv_label_set_text(lbl_imu_title_, "IMU"); + lv_obj_set_style_text_font(lbl_imu_title_, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_imu_title_, color(kColorText), 0); + lv_obj_set_size(lbl_imu_title_, 160, 16); + lv_obj_set_style_text_align(lbl_imu_title_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_pos(lbl_imu_title_, 160, kStatusH - 20); + + level_disc_ = lv_obj_create(parent); + lv_obj_remove_style_all(level_disc_); + lv_obj_set_size(level_disc_, kLevelDia, kLevelDia); + lv_obj_set_pos(level_disc_, 190, kStatusH + 2); + lv_obj_set_style_radius(level_disc_, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(level_disc_, color(kColorLevelDisc), 0); + lv_obj_set_style_bg_opa(level_disc_, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(level_disc_, 2, 0); + lv_obj_set_style_border_color(level_disc_, color(kColorLevelBorder), 0); + lv_obj_set_style_border_opa(level_disc_, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(level_disc_, 0, 0); + lv_obj_clear_flag(level_disc_, LV_OBJ_FLAG_SCROLLABLE); + + center_dot_ = lv_obj_create(level_disc_); + lv_obj_remove_style_all(center_dot_); + lv_obj_set_size(center_dot_, 8, 8); + lv_obj_set_pos(center_dot_, 46, 46); + lv_obj_set_style_radius(center_dot_, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(center_dot_, color(kColorCenterDot), 0); + lv_obj_set_style_bg_opa(center_dot_, LV_OPA_COVER, 0); + lv_obj_clear_flag(center_dot_, LV_OBJ_FLAG_SCROLLABLE); + + level_dot_ = lv_obj_create(level_disc_); + lv_obj_remove_style_all(level_dot_); + lv_obj_set_size(level_dot_, 16, 16); + lv_obj_set_pos(level_dot_, 42, 42); + lv_obj_set_style_radius(level_dot_, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(level_dot_, color(kColorLevelMove), 0); + lv_obj_set_style_bg_opa(level_dot_, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(level_dot_, 2, 0); + lv_obj_set_style_border_color(level_dot_, color(kColorText), 0); + lv_obj_set_style_border_opa(level_dot_, LV_OPA_COVER, 0); + lv_obj_clear_flag(level_dot_, LV_OBJ_FLAG_SCROLLABLE); + + lbl_acc_ = lv_label_create(parent); + lv_label_set_text(lbl_acc_, "ACC: --- --- ---"); + lv_obj_set_style_text_font(lbl_acc_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_acc_, color(kColorTextGray), 0); + lv_obj_set_size(lbl_acc_, 160, 12); + lv_obj_set_style_text_align(lbl_acc_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_pos(lbl_acc_, 160, kScreenH - kBottomH - 36); + + lbl_gyr_ = lv_label_create(parent); + lv_label_set_text(lbl_gyr_, "GYR: --- --- ---"); + lv_obj_set_style_text_font(lbl_gyr_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_gyr_, color(kColorTextGray), 0); + lv_obj_set_size(lbl_gyr_, 160, 12); + lv_obj_set_style_text_align(lbl_gyr_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_pos(lbl_gyr_, 160, kScreenH - kBottomH - 24); + } + + void create_bottom_bar(lv_obj_t* parent) + { + for (int i = 0; i < 5; i++) { + lbl_bottom_btns_[i] = lv_label_create(parent); + lv_obj_set_pos(lbl_bottom_btns_[i], i * kBtnW, kScreenH - kBottomH - 4); + lv_obj_set_size(lbl_bottom_btns_[i], kBtnW, kBottomH); + lv_obj_set_style_text_font(lbl_bottom_btns_[i], &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_bottom_btns_[i], color(kColorText), 0); + lv_obj_set_style_text_align(lbl_bottom_btns_[i], LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(lbl_bottom_btns_[i], "--"); + lv_obj_set_style_pad_top(lbl_bottom_btns_[i], 0, 0); + lv_obj_add_flag(lbl_bottom_btns_[i], LV_OBJ_FLAG_OVERFLOW_VISIBLE); + } + + for (int i = 0; i < 5; i++) { + lbl_bottom_indicators_[i] = lv_label_create(parent); + lv_obj_set_pos(lbl_bottom_indicators_[i], i * kBtnW, kScreenH - 12); + lv_obj_set_size(lbl_bottom_indicators_[i], kBtnW, 12); + lv_obj_set_style_text_font(lbl_bottom_indicators_[i], &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_bottom_indicators_[i], color(kColorText), 0); + lv_obj_set_style_text_align(lbl_bottom_indicators_[i], LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(lbl_bottom_indicators_[i], "|"); + } + + set_bottom_btn(0, ICON_LIST, true, kColorIconList); + set_bottom_btn(2, ICON_EXIT, true, kColorIconExit); + } + + void set_bottom_btn(int idx, const char* text, bool icon, uint32_t hex) + { + lv_obj_set_style_text_font(lbl_bottom_btns_[idx], + (icon && svg_font_) ? svg_font_ : &lv_font_montserrat_12, + 0); + lv_obj_set_style_text_color(lbl_bottom_btns_[idx], color(hex), 0); + lv_label_set_text(lbl_bottom_btns_[idx], text); + } + + /* + * ============================================================ + * UI 状态刷新 + * ============================================================ + */ + void update_from_state(const CompassUiState& state) + { + char buf[128]; + + if (lbl_status_text_) { + lv_label_set_text(lbl_status_text_, state.statusText.c_str()); + } + + if (lbl_yaw_) { + std::snprintf(buf, sizeof(buf), "%.0f deg %s", state.yaw, direction_text(state.yaw)); + lv_label_set_text(lbl_yaw_, state.sensorReady ? buf : "---"); + } + + if (lbl_acc_) { + std::snprintf(buf, sizeof(buf), "ACC:%6.2f %6.2f %6.2f", + state.accX, state.accY, state.accZ); + lv_label_set_text(lbl_acc_, state.sensorReady ? buf : "ACC: --- --- ---"); + } + + if (lbl_gyr_) { + std::snprintf(buf, sizeof(buf), "GYR:%6.1f %6.1f %6.1f", + state.gyrX, state.gyrY, state.gyrZ); + lv_label_set_text(lbl_gyr_, state.sensorReady ? buf : "GYR: --- --- ---"); + } + + update_level_dot(state); + last_state_ = state; + } + + const char* direction_text(float yaw) const + { + float y = yaw; + while (y < 0.0f) y += 360.0f; + while (y >= 360.0f) y -= 360.0f; + + if (y >= 337.5f || y < 22.5f) return "N"; + if (y < 67.5f) return "NE"; + if (y < 112.5f) return "E"; + if (y < 157.5f) return "SE"; + if (y < 202.5f) return "S"; + if (y < 247.5f) return "SW"; + if (y < 292.5f) return "W"; + return "NW"; + } + + void update_level_dot(const CompassUiState& state) + { + if (!level_dot_) return; + + constexpr float maxOff = 30.0f; + float dx = state.sensorReady ? (state.accY / 9.80665f * maxOff) : 0.0f; + float dy = state.sensorReady ? (state.accX / 9.80665f * maxOff) : 0.0f; + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > maxOff) { + dx = dx / dist * maxOff; + dy = dy / dist * maxOff; + } + + lv_obj_set_pos(level_dot_, 50 + static_cast(dx) - 8, 50 + static_cast(dy) - 8); + + bool stable = false; + if (state.sensorReady && last_state_.sensorReady) { + stable = (std::fabs(state.accX - last_state_.accX) < 0.2f) && + (std::fabs(state.accY - last_state_.accY) < 0.2f) && + (std::fabs(state.accZ - last_state_.accZ) < 0.2f); + } + lv_obj_set_style_bg_color(level_dot_, color(stable ? 0x2ECC71 : kColorLevelMove), 0); + } + +private: + /* + * ============================================================ + * 按键事件 + * ============================================================ + */ + void event_handler_init() + { + lv_obj_add_event_cb(ui_root, UICompassPage::static_lvgl_handler, LV_EVENT_ALL, this); + } + + static void static_lvgl_handler(lv_event_t* e) + { + UICompassPage* self = static_cast(lv_event_get_user_data(e)); + if (self) { + self->event_handler(e); + } + } + + void event_handler(lv_event_t* e) + { + if (IS_KEY_RELEASED(e)) { + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + handle_key(key); + } + } + + void handle_key(uint32_t key) + { + switch (key) { + case KEY_F4: + // TODO(compass): 接入接口后触发磁力计/IMU 校准。 + break; + + case KEY_F6: + case KEY_ESC: + if (go_back_home) { + go_back_home(); + } + break; + + default: + break; + } + } +}; From 0b105ccb685a94d5b99713cecaa732e79596e3ba Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 00:26:14 +0800 Subject: [PATCH 05/70] Expand cp0_lvgl audio capture and playback APIs Replace the simple UI sound playback helper with a command-driven AudioSystem that supports file playback, playback pause/resume/stop, recording capture, capture pause/resume/stop, and saving captured WAV output from a temporary recording file. Add audio status callbacks and optional recording waveform generation so LVGL pages can receive play-complete notifications, command results, and live capture waveform samples through cp0_signal_audio_setup and cp0_signal_audio_api. Wire the expanded audio interface into the HAL signal plan and SDL startup path, add an SDL audio source shim, and update cp0_file_path handling for image, audio, and font assets across cp0 and SDL builds. Also remove the unused Sigslot dependency from the cp0_lvgl SConstruct requirements. --- ext_components/cp0_lvgl/SConstruct | 2 +- .../cp0_lvgl/include/signal_register_plan.h | 3 + .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 629 +++++++++++++----- .../cp0_lvgl/src/cp0/cp0_lvgl_file.cpp | 6 +- .../cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp | 1 + ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c | 1 + ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h | 1 + .../cp0_lvgl/src/sdl/sdl_lvgl_file.cpp | 46 +- .../share/images/compass_needle_80.png | Bin 0 -> 514 bytes 9 files changed, 531 insertions(+), 158 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp create mode 100644 projects/APPLaunch/APPLaunch/share/images/compass_needle_80.png diff --git a/ext_components/cp0_lvgl/SConstruct b/ext_components/cp0_lvgl/SConstruct index 72ecb855..0d61bd4d 100644 --- a/ext_components/cp0_lvgl/SConstruct +++ b/ext_components/cp0_lvgl/SConstruct @@ -9,7 +9,7 @@ if "CONFIG_CP0_LVGL_COMPONENT_ENABLED" in os.environ or "CONFIG_SIGSLOT_COMPONEN SRCS = [] INCLUDE = [ADir('include')] PRIVATE_INCLUDE = [] - REQUIREMENTS = ['lvgl_component', 'Sigslot', 'Miniaudio', 'eventpp'] + REQUIREMENTS = ['lvgl_component', 'eventpp', 'Miniaudio'] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] diff --git a/ext_components/cp0_lvgl/include/signal_register_plan.h b/ext_components/cp0_lvgl/include/signal_register_plan.h index 50a396e8..826ee1a8 100644 --- a/ext_components/cp0_lvgl/include/signal_register_plan.h +++ b/ext_components/cp0_lvgl/include/signal_register_plan.h @@ -1,4 +1,7 @@ def_hal_fun(void(std::string), cp0_signal_audio_play) +def_hal_fun(void(bool), cp0_signal_audio_cap) +def_hal_fun(void(std::list, std::function), cp0_signal_audio_setup) +def_hal_fun(void(std::list, std::function), cp0_signal_audio_api) def_hal_fun(void(), cp0_signal_network) def_hal_fun(void(), cp0_signal_forkexec) def_hal_fun(void(), cp0_signal_screenshot) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index 95323f23..ce8b4a09 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -1,242 +1,567 @@ #include "hal_lvgl_bsp.h" +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include #include #include +#include -#define MINIAUDIO_IMPLEMENTATION +// #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" -extern "C" __attribute__((weak)) const char *hal_path_audio_dir(void) -{ - return nullptr; -} - -namespace { - -#ifdef _WIN32 -constexpr const char *kAudioPathSep = "\\"; -#else -constexpr const char *kAudioPathSep = "/"; -#endif -std::string audioAssetPath(const std::string &name) +class AudioSystem { - const char *dir = hal_path_audio_dir(); - if (dir == nullptr || name.empty()) { - return {}; - } +public: + static constexpr const char* kCapTmpFile = "/tmp/rec.tmp.wav"; + typedef std::function callback_t; + typedef std::list arg_t; - std::string path(dir); - if (!path.empty() && path.back() != '/' && path.back() != '\\') { - path += kAudioPathSep; + AudioSystem() + { + // initialize(); } - path += name; - return path; -} -} // namespace - -class AudioSystem -{ public: - AudioSystem() + std::function _cap_status_callback; + std::unique_ptr ma_cp0_cap_device; + std::unique_ptr ma_cp0_cap_encoder; + + std::unique_ptr ma_cp0_play_device; + std::unique_ptr ma_cp0_play_decoder; + std::atomic play_finished_reported_{false}; + + static constexpr int kRecWaveformSize = 128; + std::array rec_waveform_{}; + size_t rec_waveform_index_ = 0; + std::mutex rec_waveform_mutex_; + std::atomic rec_waveform_enabled_{false}; + + static void cap_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { - initialize(); + AudioSystem* self = (AudioSystem*)pDevice->pUserData; + if (self) self->on_cap_data(pInput, frameCount); + (void)pOutput; } - int initialize() + static void play_data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { - std::lock_guard lock(mutex_); - if (initialized_) { - return 0; + AudioSystem* self = (AudioSystem*)pDevice->pUserData; + // ma_decoder* pDecoder = (ma_decoder*)pDevice->pUserData; + if (self == NULL || self->ma_cp0_play_decoder.get() == NULL) { + ma_silence_pcm_frames(pOutput, frameCount, pDevice->playback.format, pDevice->playback.channels); + return; } + ma_uint64 framesRead ; + ma_result result = ma_decoder_read_pcm_frames(self->ma_cp0_play_decoder.get(), pOutput, frameCount, &framesRead); + if (framesRead < frameCount) { + void* silence = ma_offset_pcm_frames_ptr(pOutput, framesRead, pDevice->playback.format, pDevice->playback.channels); + ma_silence_pcm_frames(silence, frameCount - framesRead, pDevice->playback.format, pDevice->playback.channels); + } + bool finished = (result == MA_AT_END || framesRead < frameCount); + if(finished && !self->play_finished_reported_.exchange(true) && self->_cap_status_callback) + self->_cap_status_callback(0, "play over\n"); + (void)pInput; + } - ma_backend backends[] = { - ma_backend_pulseaudio - }; - ma_result result = ma_context_init( - backends, - sizeof(backends) / sizeof(backends[0]), - nullptr, - &context_ - ); + int play(std::string wav) + { + ma_result result; + ma_device_config deviceConfig; + stop_play_device(false); + play_finished_reported_.store(false); + ma_cp0_play_device = std::make_unique(); + ma_cp0_play_decoder = std::make_unique(); + result = ma_decoder_init_file(wav.c_str(), NULL, ma_cp0_play_decoder.get()); if (result != MA_SUCCESS) { - std::fprintf(stderr, "[AUDIO] ma_context_init PulseAudio failed: %d\n", result); - return -1; + if(_cap_status_callback)_cap_status_callback(-2, "Could not load file\n"); + ma_cp0_play_decoder.reset(); + ma_cp0_play_device.reset(); + return -2; + } + + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.format = ma_cp0_play_decoder.get()->outputFormat; + deviceConfig.playback.channels = ma_cp0_play_decoder.get()->outputChannels; + deviceConfig.sampleRate = ma_cp0_play_decoder.get()->outputSampleRate; + deviceConfig.dataCallback = play_data_callback; + deviceConfig.pUserData = this; + + if (ma_device_init(NULL, &deviceConfig, ma_cp0_play_device.get()) != MA_SUCCESS) { + ma_decoder_uninit(ma_cp0_play_decoder.get()); + if(_cap_status_callback)_cap_status_callback(-3, "Failed to open playback device.\n"); + ma_cp0_play_decoder.reset(); + ma_cp0_play_device.reset(); + return -3; } - ma_engine_config engineConfig = ma_engine_config_init(); - engineConfig.pContext = &context_; - engineConfig.pPlaybackDeviceID = nullptr; - engineConfig.channels = 2; - engineConfig.sampleRate = 48000; - - result = ma_engine_init(&engineConfig, &engine_); - if (result != MA_SUCCESS) { - std::fprintf(stderr, "[AUDIO] ma_engine_init PulseAudio failed: %d\n", result); - ma_context_uninit(&context_); - return -1; + if (ma_device_start(ma_cp0_play_device.get()) != MA_SUCCESS) { + ma_device_uninit(ma_cp0_play_device.get()); + ma_decoder_uninit(ma_cp0_play_decoder.get()); + if(_cap_status_callback)_cap_status_callback(-4, "Failed to start playback device.\n"); + ma_cp0_play_decoder.reset(); + ma_cp0_play_device.reset(); + return -4; } - initialized_ = true; - std::printf("[AUDIO] audio system initialized with PulseAudio backend\n"); return 0; } - int loadSounds() + + + + + + + + +private: + void report(callback_t callback, int code, const std::string& data) { - if (initialize() != 0) { - return -1; + if(callback) callback(code, data); + else if(_cap_status_callback) _cap_status_callback(code, data); + } + + static std::string first_arg_after_command(const arg_t& arg) + { + if(arg.size() < 2) return ""; + return *std::next(arg.begin()); + } + + static bool has_path_separator(const std::string& path) + { + return path.find('/') != std::string::npos || path.find('\\') != std::string::npos; + } + + static std::string resolve_play_file(const std::string& file, bool asset) + { + if(file.empty()) return ""; + if(!asset || has_path_separator(file)) return file; + + std::string path = cp0_file_path(file); + return path.empty() ? file : path; + } + + void stop_play_device(bool report_state) + { + play_finished_reported_.store(false); + if(ma_cp0_play_device) + { + ma_device_uninit(ma_cp0_play_device.get()); + ma_cp0_play_device.reset(); + } + if(ma_cp0_play_decoder) + { + ma_decoder_uninit(ma_cp0_play_decoder.get()); + ma_cp0_play_decoder.reset(); + } + if(report_state && _cap_status_callback) _cap_status_callback(0, "play stop\n"); + } + + static int copy_file(const std::string& src_path, const std::string& dst_path) + { + FILE* src = std::fopen(src_path.c_str(), "rb"); + if(!src) return -1; + + FILE* dst = std::fopen(dst_path.c_str(), "wb"); + if(!dst) + { + std::fclose(src); + return -2; } - std::lock_guard lock(mutex_); - if (soundsLoaded_) { + char buf[4096]; + size_t n = 0; + int ret = 0; + while((n = std::fread(buf, 1, sizeof(buf), src)) > 0) + { + if(std::fwrite(buf, 1, n, dst) != n) + { + ret = -3; + break; + } + } + if(std::ferror(src)) ret = -4; + + std::fclose(dst); + std::fclose(src); + return ret; + } + + int save_cap_file(const std::string& dst_path) + { + if(dst_path.empty()) return -1; + if(std::rename(kCapTmpFile, dst_path.c_str()) == 0) return 0; + + int saved_errno = errno; + int ret = copy_file(kCapTmpFile, dst_path); + if(ret == 0) + { + std::remove(kCapTmpFile); return 0; } + errno = saved_errno; + return ret; + } + + void on_cap_data(const void* input, ma_uint32 frameCount) + { + if(ma_cp0_cap_encoder) + { + ma_encoder_write_pcm_frames(ma_cp0_cap_encoder.get(), input, frameCount, NULL); + } - const std::string switchPath = audioAssetPath("switch.wav"); - const std::string enterPath = audioAssetPath("enter.wav"); - if (switchPath.empty() || enterPath.empty()) { - std::fprintf(stderr, "[AUDIO] audio path unavailable\n"); - return -1; + if(!rec_waveform_enabled_.load() || !_cap_status_callback || input == NULL || frameCount == 0) + { + return; } - ma_result result = ma_sound_init_from_file( - &engine_, - switchPath.c_str(), - MA_SOUND_FLAG_DECODE, - nullptr, - nullptr, - &switchSound_ - ); + std::string waveform = build_rec_waveform(input, frameCount); + if(!waveform.empty()) + { + _cap_status_callback(1, waveform); + } + } - if (result != MA_SUCCESS) { - std::fprintf(stderr, "[AUDIO] load switch.wav failed: %d, path=%s\n", - result, - switchPath.c_str()); - return -1; + std::string build_rec_waveform(const void* input, ma_uint32 frameCount) + { + const int16_t* samples = static_cast(input); + ma_uint32 channels = 1; + if(ma_cp0_cap_encoder && ma_cp0_cap_encoder.get()->config.channels > 0) + { + channels = ma_cp0_cap_encoder.get()->config.channels; } - result = ma_sound_init_from_file( - &engine_, - enterPath.c_str(), - MA_SOUND_FLAG_DECODE, - nullptr, - nullptr, - &enterSound_ - ); + ma_uint32 sampleCount = frameCount * channels; + int16_t peak = 0; + double sumSq = 0.0; + for(ma_uint32 i = 0; i < sampleCount; i++) + { + if(std::abs(samples[i]) > std::abs(peak)) + { + peak = samples[i]; + } + double s = static_cast(samples[i]) / 32768.0; + sumSq += s * s; + } - if (result != MA_SUCCESS) { - std::fprintf(stderr, "[AUDIO] load enter.wav failed: %d, path=%s\n", - result, - enterPath.c_str()); - ma_sound_uninit(&switchSound_); - return -1; + float rms = (sampleCount > 0) ? static_cast(std::sqrt(sumSq / sampleCount)) : 0.0f; + float db = 20.0f * std::log10(rms + 1e-6f); + if(db < -36.0f) db = -36.0f; + float dbNorm = (db + 36.0f) / 36.0f; + if(peak < 0) dbNorm = -dbNorm; + + std::array waveform{}; + { + std::lock_guard lock(rec_waveform_mutex_); + rec_waveform_[rec_waveform_index_] = std::max(-1.0f, std::min(1.0f, dbNorm)); + rec_waveform_index_ = (rec_waveform_index_ + 1) % kRecWaveformSize; + + for(int i = 0; i < kRecWaveformSize; i++) + { + size_t idx = (rec_waveform_index_ + kRecWaveformSize - kRecWaveformSize + i) % kRecWaveformSize; + waveform[i] = rec_waveform_[idx]; + } } - soundsLoaded_ = true; - std::printf("[AUDIO] sounds loaded\n"); - return 0; + std::string out(sizeof(float) * kRecWaveformSize, '\0'); + std::memcpy(&out[0], waveform.data(), out.size()); + return out; } - void playSwitch() + static bool arg_is_enable(const std::string& arg) { - playLoadedSound(switchSound_); + return arg == "1" || arg == "on" || arg == "true" || arg == "enable" || arg == "enabled"; } - void playEnter() + static bool arg_is_disable(const std::string& arg) + { + return arg == "0" || arg == "off" || arg == "false" || arg == "disable" || arg == "disabled"; + } + + int start_cap_device() + { + ma_result result; + ma_encoder_config encoderConfig; + ma_device_config deviceConfig; + if(!ma_cp0_cap_encoder) + { + ma_cp0_cap_encoder = std::make_unique(); + ma_cp0_cap_device = std::make_unique(); + + encoderConfig = ma_encoder_config_init(ma_encoding_format_wav, ma_format_s16, 2, 48000); + if (ma_encoder_init_file(kCapTmpFile, &encoderConfig, ma_cp0_cap_encoder.get()) != MA_SUCCESS) { + if(_cap_status_callback)_cap_status_callback(-1, "Failed to initialize output file.\n"); + ma_cp0_cap_encoder.reset(); + ma_cp0_cap_device.reset(); + return -1; + } + deviceConfig = ma_device_config_init(ma_device_type_capture); + deviceConfig.capture.format = ma_cp0_cap_encoder.get()->config.format; + deviceConfig.capture.channels = ma_cp0_cap_encoder.get()->config.channels; + deviceConfig.sampleRate = ma_cp0_cap_encoder.get()->config.sampleRate; + deviceConfig.dataCallback = cap_data_callback; + deviceConfig.pUserData = this; + result = ma_device_init(NULL, &deviceConfig, ma_cp0_cap_device.get()); + if (result != MA_SUCCESS) { + if(_cap_status_callback)_cap_status_callback(-3, "Failed to initialize capture device.\n"); + ma_encoder_uninit(ma_cp0_cap_encoder.get()); + ma_cp0_cap_encoder.reset(); + ma_cp0_cap_device.reset(); + return -2; + } + result = ma_device_start(ma_cp0_cap_device.get()); + if (result != MA_SUCCESS) { + ma_device_uninit(ma_cp0_cap_device.get()); + ma_encoder_uninit(ma_cp0_cap_encoder.get()); + ma_cp0_cap_encoder.reset(); + ma_cp0_cap_device.reset(); + if(_cap_status_callback)_cap_status_callback(-3, "Failed to start device.\n"); + return -3; + } + } + else + { + if(_cap_status_callback)_cap_status_callback(-4, "working"); + } + return 0; + } + void stop_cap_device() + { + if(ma_cp0_cap_device) + { + ma_device_uninit(ma_cp0_cap_device.get()); + if(ma_cp0_cap_encoder) ma_encoder_uninit(ma_cp0_cap_encoder.get()); + ma_cp0_cap_device.reset(); + ma_cp0_cap_encoder.reset(); + } + else + { + if(_cap_status_callback)_cap_status_callback(-5, "stop"); + } + } +public: + void cap(bool enable) { - playLoadedSound(enterSound_); + if(enable) + { + start_cap_device(); + }else{ + stop_cap_device(); + } + } + void setup(std::list arg, std::function callback) + { + if(arg.empty()) return; + auto arg1 = arg.begin(); + if(*arg1 == "set_callback") + { + _cap_status_callback = callback; + } + else if(*arg1 == "set_waveform" || *arg1 == "waveform") + { + auto arg2 = std::next(arg1); + if(arg2 != arg.end()) + { + if(arg_is_enable(*arg2)) + { + rec_waveform_enabled_.store(true); + } + else if(arg_is_disable(*arg2)) + { + rec_waveform_enabled_.store(false); + } + } + else + { + rec_waveform_enabled_.store(true); + } + } + else if(*arg1 == "stop_play") + { + stop_play_device(false); + } + } + // 录音的过程控制:开始,暂停,恢复播放,结束保存。 + // 播放的过程控制:开始,暂停,恢复播放,播放结束。 + void PlayFile(arg_t arg, callback_t callback) + { + std::string file = resolve_play_file(first_arg_after_command(arg), false); + if(file.empty()) + { + report(callback, -1, "PlayFile need file\n"); + return; + } + int ret = play(file); + report(callback, ret, ret == 0 ? "play start\n" : "play failed\n"); } - void play(const std::string &wav) + void Play(arg_t arg, callback_t callback) { - if (wav == "switch.wav" || wav == "switch") { - playSwitch(); + std::string file = resolve_play_file(first_arg_after_command(arg), true); + if(file.empty()) + { + report(callback, -1, "Play need file\n"); return; } + int ret = play(file); + report(callback, ret, ret == 0 ? "play start\n" : "play failed\n"); + } - if (wav == "enter.wav" || wav == "enter") { - playEnter(); + void PlayPause(arg_t arg, callback_t callback) + { + (void)arg; + if(!ma_cp0_play_device) + { + report(callback, -1, "play not started\n"); return; } + ma_result ret = ma_device_stop(ma_cp0_play_device.get()); + report(callback, ret == MA_SUCCESS ? 0 : -2, ret == MA_SUCCESS ? "play pause\n" : "play pause failed\n"); + } - std::string path = wav; - if (path.find('/') == std::string::npos && path.find('\\') == std::string::npos) { - path = audioAssetPath(wav); + void PlayContinue(arg_t arg, callback_t callback) + { + (void)arg; + if(!ma_cp0_play_device) + { + report(callback, -1, "play not started\n"); + return; } + ma_result ret = ma_device_start(ma_cp0_play_device.get()); + report(callback, ret == MA_SUCCESS ? 0 : -2, ret == MA_SUCCESS ? "play continue\n" : "play continue failed\n"); + } - playFile(path); + void PlayEnd(arg_t arg, callback_t callback) + { + (void)arg; + stop_play_device(false); + report(callback, 0, "play stop\n"); + } + + void Cap(arg_t arg, callback_t callback) + { + (void)arg; + int ret = start_cap_device(); + report(callback, ret, ret == 0 ? "cap start\n" : "cap failed\n"); } - void playFile(const std::string &path) + void CapPause(arg_t arg, callback_t callback) { - if (path.empty() || initialize() != 0) { + (void)arg; + if(!ma_cp0_cap_device) + { + report(callback, -1, "cap not started\n"); return; } + ma_result ret = ma_device_stop(ma_cp0_cap_device.get()); + report(callback, ret == MA_SUCCESS ? 0 : -2, ret == MA_SUCCESS ? "cap pause\n" : "cap pause failed\n"); + } - std::lock_guard lock(mutex_); - ma_result result = ma_engine_play_sound(&engine_, path.c_str(), nullptr); - if (result != MA_SUCCESS) { - std::fprintf(stderr, "[AUDIO] play_audio failed: %d, path=%s\n", - result, - path.c_str()); + void CapContinue(arg_t arg, callback_t callback) + { + (void)arg; + if(!ma_cp0_cap_device) + { + report(callback, -1, "cap not started\n"); + return; } + ma_result ret = ma_device_start(ma_cp0_cap_device.get()); + report(callback, ret == MA_SUCCESS ? 0 : -2, ret == MA_SUCCESS ? "cap continue\n" : "cap continue failed\n"); } - void uninitialize() + void CapEnd(arg_t arg, callback_t callback) { - std::lock_guard lock(mutex_); + (void)arg; + stop_cap_device(); + report(callback, 0, "cap stop\n"); + } - if (soundsLoaded_) { - ma_sound_uninit(&switchSound_); - ma_sound_uninit(&enterSound_); - soundsLoaded_ = false; + void CapFileSave(arg_t arg, callback_t callback) + { + std::string file = first_arg_after_command(arg); + if(file.empty()) + { + report(callback, -1, "CapFileSave need file\n"); + return; } - - if (initialized_) { - ma_engine_uninit(&engine_); - ma_context_uninit(&context_); - initialized_ = false; + if(ma_cp0_cap_device) + { + stop_cap_device(); } + int ret = save_cap_file(file); + report(callback, ret, ret == 0 ? "cap file saved\n" : "cap file save failed\n"); } - ~AudioSystem() + + void SetCallback(arg_t arg, callback_t callback) { - uninitialize(); + (void)arg; + _cap_status_callback = callback; } -private: - AudioSystem(const AudioSystem &) = delete; - AudioSystem &operator=(const AudioSystem &) = delete; - void playLoadedSound(ma_sound &sound) + void api_call(arg_t arg, callback_t callback) { - if (loadSounds() != 0) { + if(arg.empty()) + { + report(callback, -1, "empty audio api\n"); return; } +#define map_fun(name) {#name, std::bind(&AudioSystem::name, this, std::placeholders::_1, std::placeholders::_2)} + + std::list>> cmd_map = { + map_fun(PlayFile), + map_fun(Play), + map_fun(PlayPause), + map_fun(PlayContinue), + map_fun(PlayEnd), + map_fun(Cap), + map_fun(CapPause), + map_fun(CapContinue), + map_fun(CapEnd), + map_fun(CapFileSave), + map_fun(SetCallback) + }; - std::lock_guard lock(mutex_); - ma_sound_stop(&sound); - ma_sound_seek_to_pcm_frame(&sound, 0); - ma_sound_start(&sound); - } +#undef map_fun - std::mutex mutex_; - ma_context context_{}; - ma_engine engine_{}; - ma_sound switchSound_{}; - ma_sound enterSound_{}; - bool initialized_ = false; - bool soundsLoaded_ = false; + for (const auto& it : cmd_map) + { + if (it.first == arg.front()) + { + it.second(arg, callback); + return; + } + } + report(callback, -1, "unknown audio api\n"); + } }; extern "C" void init_audio(void) { std::shared_ptr audio = std::make_shared(); - cp0_signal_audio_play.append([audio](std::string wav) { - audio->play(wav); - }); + cp0_signal_audio_play.append([audio](std::string wav) + { audio->play(wav); }); + + cp0_signal_audio_cap.append([audio](bool enable) + { audio->cap(enable); }); + + cp0_signal_audio_setup.append([audio](std::list arg, std::function callback) + { audio->setup(arg, callback); }); + + cp0_signal_audio_api.append([audio](std::list arg, std::function callback) + { audio->api_call(arg, callback); }); + } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp index a0541a81..d3affe33 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp @@ -9,11 +9,9 @@ std::string cp0_file_path(std::string file) { std::regex pattern(R"(\.(png|wav|ttf)$)", std::regex::icase); - - std::string root_path; std::smatch m; - // std::string root_path = "/usr/share/APPLaunch/"; + std::string root_path = "/usr/share/APPLaunch/"; bool matched = std::regex_search(file, m, pattern); if (matched) { @@ -25,7 +23,7 @@ std::string cp0_file_path(std::string file) }); if (ext == "png") { - return root_path + "share/images/" + file; + return "share/images/" + file; } else if (ext == "wav") { return root_path + "share/audio/" + file; } else if (ext == "ttf") { diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp new file mode 100644 index 00000000..ebfa3d7a --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp @@ -0,0 +1 @@ +#include "../cp0/cp0_lvgl_audio.cpp" \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c index 658d48b0..12b500e0 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c @@ -14,4 +14,5 @@ void cp0_lvgl_init(void) init_lvgl_event(); init_sdl_disp(); init_sdl_input(); + init_audio(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h index 303ced96..a4882740 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h @@ -6,6 +6,7 @@ extern "C" #endif void init_sdl_disp(); void init_sdl_input(); +void init_audio(); #ifdef __cplusplus } #endif diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp index 656ae4a8..e4c8567f 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp @@ -4,8 +4,52 @@ #include #include #include +#include +#include +#include +#include +#include + +static std::string get_app_root_path() +{ + char exe_path[PATH_MAX] = {0}; + ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len <= 0) { + return "APPLaunch/"; + } + + exe_path[len] = '\0'; + std::string path(exe_path); + size_t slash = path.find_last_of('/'); + std::string exe_dir = (slash == std::string::npos) ? "." : path.substr(0, slash); + + return exe_dir + "/APPLaunch/"; +} std::string cp0_file_path(std::string file) { + std::regex pattern(R"(\.(png|wav|ttf)$)", std::regex::icase); + std::smatch m; + + std::string root_path = get_app_root_path(); + bool matched = std::regex_search(file, m, pattern); + + if (matched) { + std::string ext = m[1].str(); + + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { + return std::tolower(c); + }); + + if (ext == "png") { + return "share/images/" + file; + } else if (ext == "wav") { + return root_path + "share/audio/" + file; + } else if (ext == "ttf") { + return root_path + "share/font/" + file; + } + } -} \ No newline at end of file + return ""; // 或者 return ""; 或者 throw,根据你的需求 +} diff --git a/projects/APPLaunch/APPLaunch/share/images/compass_needle_80.png b/projects/APPLaunch/APPLaunch/share/images/compass_needle_80.png new file mode 100644 index 0000000000000000000000000000000000000000..fa4e828111f17e87f49a6993aa3a6b1c7cbcfb0e GIT binary patch literal 514 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U_9yR;uumf=k2YHflP@4Z4Yga zGjV7feBh<9fQ89>fr+;PkoHp8z`|^G;J`)4J=X8uC8yoL`o8ms&2Or&z#|T zy*Bs!&2))R6KWe?U0~dwC;wAy5p&ps1|@cpMCQ2nwy)%lu&p`7=*p+z#P;TP`4QU= zo`_E7t^&sVUd`N|LdMmF4ilNX0vM0KGruI~!n)=Jqw9=@;Nz2wj?HKYwOjz?b~F@! zmrUeWVhVc(GIc3?$oGhEaz6YKbD6t>nD1r(F*<(3JmAZW+6Kkbhadia{{5=fN0q-D z%s&{#T~F>(d)@l`+qKIdZ_M#qesAu!2%T9ApWl03lf1O|L)pz^b^oni^Ome;l;nAFaFsNHwLD#=OD)_GM&4deDk+Hlh6hub`i@3 z9Kr>!?3CZ^=Lk5kN+zI%%c4w2^RYA|cLXpDfPC(X?EMqJ#j_YBn1aJgI{4NPHwWgh yN3h_ue)O-KfgK#4Kdw*wC%v^hf5FVZ>GCO`Dq=V1ImrRzmci52&t;ucLK6VPu-{ey literal 0 HcmV?d00001 From f6ffb25382da668e8f7bc5fc788ba6a640c02c7d Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 10:23:20 +0800 Subject: [PATCH 06/70] feat: add camera service and recorder UI flow Add CP0 camera integration backed by libcamera and JPEG output, including build requirements, signal registration, initialization hooks, and photo capture/status API handling. Refactor APPLaunch recorder UI into multi-page home/save/file/playback flows with button/key bindings, waveform updates, file scanning, save confirmation, and playback state management. Update app launch wiring for Compass and camera visibility, move miniaudio implementation ownership to the CP0 audio component, adjust build defines, ignore generated/local artifacts, and refresh APPLaunch deploy settings. --- .gitignore | 5 + ext_components/cp0_lvgl/SConstruct | 8 + .../cp0_lvgl/include/signal_register_plan.h | 1 + ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 3 +- ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp | 651 +++++++ projects/APPLaunch/main/SConstruct | 59 +- projects/APPLaunch/main/src/main.cpp | 18 +- .../ui/components/page_app/ui_app_compass.hpp | 305 +++- .../ui/components/page_app/ui_app_rec.hpp | 1599 +++++++++++++---- .../main/ui/components/ui_app_launch.cpp | 8 +- .../main/ui/components/ui_app_page.hpp | 1 + projects/APPLaunch/main/ui/ui.h | 1 + projects/APPLaunch/main/ui/ui_events.c | 2 +- projects/APPLaunch/setup.ini | 4 +- 16 files changed, 2231 insertions(+), 437 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp diff --git a/.gitignore b/.gitignore index ea506462..2cc256dd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ tools setup.ini pi projects/APPLaunch/main/ui/components/page_app.h +dist.bak +.agents +data +skills-lock.json +docs/ui_app_rec_flow.html diff --git a/ext_components/cp0_lvgl/SConstruct b/ext_components/cp0_lvgl/SConstruct index 0d61bd4d..6c90d2d5 100644 --- a/ext_components/cp0_lvgl/SConstruct +++ b/ext_components/cp0_lvgl/SConstruct @@ -1,6 +1,7 @@ # component/SConscript Import("env") import os +import subprocess with open(env["PROJECT_TOOL_S"]) as f: exec(f.read()) @@ -22,6 +23,13 @@ if "CONFIG_CP0_LVGL_COMPONENT_ENABLED" in os.environ or "CONFIG_SIGSLOT_COMPONEN else: SRCS += Glob('src/cp0/*.c*') REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'pthread'] + REQUIREMENTS += ['camera', 'camera-base', 'jpeg'] + try: + DEFINITIONS += subprocess.check_output( + ['pkg-config', '--cflags', 'libcamera'], text=True).split() + except Exception: + _sr = os.environ.get('CONFIG_TOOLCHAIN_SYSROOT', '') + DEFINITIONS += [f'-I{_sr}/usr/include/libcamera'] env["COMPONENTS"].append( { diff --git a/ext_components/cp0_lvgl/include/signal_register_plan.h b/ext_components/cp0_lvgl/include/signal_register_plan.h index 826ee1a8..d9e62b9a 100644 --- a/ext_components/cp0_lvgl/include/signal_register_plan.h +++ b/ext_components/cp0_lvgl/include/signal_register_plan.h @@ -6,3 +6,4 @@ def_hal_fun(void(), cp0_signal_network) def_hal_fun(void(), cp0_signal_forkexec) def_hal_fun(void(), cp0_signal_screenshot) def_hal_fun(void(std::function), cp0_signal_battery_pub) +def_hal_fun(void(std::list, std::function), cp0_signal_camera_api) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index f2595a76..e87806c6 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -6,7 +6,8 @@ void cp0_lvgl_init(void) { init_lvgl_event(); init_freambuffer_disp(); - init_input(); + // init_input(); init_audio(); init_battery(); + init_camera(); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h index 0bb6e4e0..6fe9d025 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h @@ -8,6 +8,7 @@ void init_freambuffer_disp(); void init_input(); void init_audio(); void init_battery(); +void init_camera(void); #ifdef __cplusplus } #endif diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index ce8b4a09..24d61e49 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -17,7 +17,7 @@ #include #include -// #define MINIAUDIO_IMPLEMENTATION +#define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp new file mode 100644 index 00000000..54d3b068 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp @@ -0,0 +1,651 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() && __has_include() +#define CP0_CAMERA_HAS_LIBCAMERA 1 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#else +#define CP0_CAMERA_HAS_LIBCAMERA 0 +#endif + +/* + * Libcamera API extracted from UICameraPage: + * - CameraManager start/stop and cameras() enumeration + * - properties::Model filtering for IMX219 + * - Camera acquire/release, generateConfiguration(), configure(), start()/stop() + * - StreamRole::Viewfinder with RGB565 request, validated to RGB888/BGR888/XRGB8888/XBGR8888/RGB565 + * - FrameBufferAllocator allocate()/buffers(), mmap() of the first plane fd + * - Request createRequest(), addBuffer(), queueRequest(), reuse(ReuseBuffers) + * - requestCompleted signal to consume frame metadata bytesused and re-queue buffers + */ +class CameraSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + CameraSystem() = default; + ~CameraSystem() + { + close_camera(); + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) + { + report(callback, -1, "empty camera api\n"); + return; + } + +#define map_fun(name) {#name, std::bind(&CameraSystem::name, this, std::placeholders::_1, std::placeholders::_2)} + std::list>> cmd_map = { + map_fun(Open), + map_fun(Start), + map_fun(Close), + map_fun(Stop), + map_fun(Capture), + map_fun(Photo), + map_fun(Status), + map_fun(SetCallback), + }; +#undef map_fun + + for (const auto &it : cmd_map) + { + if (it.first == arg.front()) + { + it.second(arg, callback); + return; + } + } + + report(callback, -1, "unknown camera api\n"); + } + +private: + callback_t status_callback_; + std::mutex mutex_; + +#if CP0_CAMERA_HAS_LIBCAMERA + struct MappedBuffer + { + void *addr = nullptr; + size_t size = 0; + int fd = -1; + }; + + std::unique_ptr cm_; + std::shared_ptr camera_; + std::unique_ptr config_; + std::unique_ptr allocator_; + libcamera::Stream *stream_ = nullptr; + std::vector> requests_; + std::unordered_map mapped_buffers_; + + bool streaming_ = false; + int stream_w_ = 320; + int stream_h_ = 150; + int stream_stride_ = 320 * 2; + libcamera::PixelFormat stream_format_ = libcamera::formats::RGB565; + + std::atomic capture_requested_{false}; + std::string pending_capture_path_; + callback_t pending_capture_callback_; + int capture_counter_ = 0; +#endif + + void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + else if (status_callback_) + status_callback_(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t n) + { + if (arg.size() <= n) + return ""; + auto it = arg.begin(); + std::advance(it, n); + return *it; + } + + static int to_int(const std::string &value, int fallback) + { + if (value.empty()) + return fallback; + char *end = nullptr; + long ret = std::strtol(value.c_str(), &end, 10); + return end && *end == '\0' ? static_cast(ret) : fallback; + } + + void SetCallback(arg_t arg, callback_t callback) + { + (void)arg; + status_callback_ = callback; + report(callback, 0, "camera callback set\n"); + } + + void Open(arg_t arg, callback_t callback) + { + const int width = to_int(nth_arg(arg, 1), 320); + const int height = to_int(nth_arg(arg, 2), 150); + const int ret = open_camera(width, height); + report(callback, ret, ret == 0 ? "camera open\n" : "camera open failed\n"); + } + + void Start(arg_t arg, callback_t callback) + { + Open(arg, callback); + } + + void Close(arg_t arg, callback_t callback) + { + (void)arg; + close_camera(); + report(callback, 0, "camera close\n"); + } + + void Stop(arg_t arg, callback_t callback) + { + Close(arg, callback); + } + + void Capture(arg_t arg, callback_t callback) + { + std::string path = nth_arg(arg, 1); + const int width = to_int(nth_arg(arg, 2), 320); + const int height = to_int(nth_arg(arg, 3), 150); + + const int ret = capture(path, width, height, callback); + if (ret != 0) + report(callback, ret, "camera capture failed\n"); + } + + void Photo(arg_t arg, callback_t callback) + { + Capture(arg, callback); + } + + void Status(arg_t arg, callback_t callback) + { + (void)arg; +#if CP0_CAMERA_HAS_LIBCAMERA + bool streaming = false; + { + std::lock_guard lock(mutex_); + streaming = streaming_; + } + report(callback, streaming ? 0 : 1, streaming ? "camera streaming\n" : "camera stopped\n"); +#else + report(callback, -10, "camera unavailable: libcamera/jpeg headers not found\n"); +#endif + } + +#if CP0_CAMERA_HAS_LIBCAMERA + static std::string lower_string(std::string s) + { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; + } + + static void ensure_picture_dir() + { + const char *dir = "/home/pi/Pictures"; + struct stat st; + if (stat(dir, &st) != 0) + mkdir(dir, 0777); + chmod(dir, 0777); + } + + std::string make_photo_path() + { + ensure_picture_dir(); + time_t now = time(nullptr); + struct tm tm_now; + localtime_r(&now, &tm_now); + + char time_buf[64]; + strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", &tm_now); + + char path[256]; + std::snprintf(path, sizeof(path), "/home/pi/Pictures/IMX219_%s_%03d.jpg", time_buf, ++capture_counter_); + return std::string(path); + } + + static bool is_supported_preview_format(const libcamera::PixelFormat &format) + { + return format == libcamera::formats::RGB888 || + format == libcamera::formats::BGR888 || + format == libcamera::formats::XRGB8888 || + format == libcamera::formats::XBGR8888 || + format == libcamera::formats::RGB565; + } + + static void dma_buf_sync(int fd, uint64_t flags) + { + if (fd < 0) + return; + struct dma_buf_sync sync = {flags}; + ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync); + } + + static void rgb565_to_rgb888(uint16_t p, uint8_t &r, uint8_t &g, uint8_t &b) + { + r = ((p >> 11) & 0x1F) << 3; + g = ((p >> 5) & 0x3F) << 2; + b = (p & 0x1F) << 3; + r |= r >> 5; + g |= g >> 6; + b |= b >> 5; + } + + bool convert_to_rgb888(const uint8_t *src, size_t bytes_used, std::vector &rgb) + { + const bool is_rgb888 = stream_format_ == libcamera::formats::RGB888; + const bool is_bgr888 = stream_format_ == libcamera::formats::BGR888; + const bool is_xrgb8888 = stream_format_ == libcamera::formats::XRGB8888; + const bool is_xbgr8888 = stream_format_ == libcamera::formats::XBGR8888; + const bool is_rgb565 = stream_format_ == libcamera::formats::RGB565; + const int bytes_per_pixel = is_rgb888 || is_bgr888 ? 3 : is_rgb565 ? 2 : 4; + const int min_stride = stream_w_ * bytes_per_pixel; + const int row_stride = stream_stride_ > 0 ? stream_stride_ : min_stride; + + if (row_stride < min_stride) + return false; + + rgb.assign(stream_w_ * stream_h_ * 3, 0); + for (int y = 0; y < stream_h_; ++y) + { + const size_t row_offset = static_cast(y) * row_stride; + if (row_offset + min_stride > bytes_used) + return y > 0; + + const uint8_t *line = src + row_offset; + const int dst_y = stream_h_ - 1 - y; + for (int x = 0; x < stream_w_; ++x) + { + const int dst_x = stream_w_ - 1 - x; + const int dst = (dst_y * stream_w_ + dst_x) * 3; + uint8_t r = 0, g = 0, b = 0; + + if (is_rgb888) + { + const uint8_t *p = line + x * 3; + r = p[0]; g = p[1]; b = p[2]; + } + else if (is_bgr888) + { + const uint8_t *p = line + x * 3; + b = p[0]; g = p[1]; r = p[2]; + } + else if (is_xrgb8888) + { + const uint8_t *p = line + x * 4; + b = p[0]; g = p[1]; r = p[2]; + } + else if (is_xbgr8888) + { + const uint8_t *p = line + x * 4; + r = p[0]; g = p[1]; b = p[2]; + } + else if (is_rgb565) + { + const uint8_t *p = line + x * 2; + rgb565_to_rgb888(static_cast(p[0] | (p[1] << 8)), r, g, b); + } + + rgb[dst + 0] = r; + rgb[dst + 1] = g; + rgb[dst + 2] = b; + } + } + return true; + } + + static bool save_jpeg_rgb888(const std::string &path, const uint8_t *rgb, int width, int height, int quality = 90) + { + FILE *fp = std::fopen(path.c_str(), "wb"); + if (!fp) + return false; + + jpeg_compress_struct cinfo; + jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + jpeg_stdio_dest(&cinfo, fp); + cinfo.image_width = width; + cinfo.image_height = height; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + jpeg_start_compress(&cinfo, TRUE); + + while (cinfo.next_scanline < cinfo.image_height) + { + JSAMPROW row_pointer[1]; + row_pointer[0] = const_cast(&rgb[cinfo.next_scanline * width * 3]); + jpeg_write_scanlines(&cinfo, row_pointer, 1); + } + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + std::fclose(fp); + chmod(path.c_str(), 0666); + return true; + } + + int open_camera_impl(int width, int height) + { + std::lock_guard lock(mutex_); + if (streaming_) + return 0; + if (cm_ || camera_) + close_camera_locked(); + + cm_ = std::make_unique(); + if (cm_->start()) + { + cm_.reset(); + return -2; + } + + std::shared_ptr selected; + for (const std::shared_ptr &cam : cm_->cameras()) + { + std::string model_text; + const auto &props = cam->properties(); + auto model = props.get(libcamera::properties::Model); + model_text = model ? *model : cam->id(); + if (lower_string(model_text).find("imx219") != std::string::npos) + { + selected = cam; + break; + } + } + if (!selected) + { + close_camera_locked(); + return -3; + } + + camera_ = selected; + if (camera_->acquire()) + { + close_camera_locked(); + return -4; + } + + config_ = camera_->generateConfiguration({libcamera::StreamRole::Viewfinder}); + if (!config_ || config_->empty()) + { + close_camera_locked(); + return -5; + } + + libcamera::StreamConfiguration &cfg = config_->at(0); + cfg.size.width = width > 0 ? width : 320; + cfg.size.height = height > 0 ? height : 150; + cfg.pixelFormat = libcamera::formats::RGB565; + cfg.bufferCount = 4; + + if (config_->validate() == libcamera::CameraConfiguration::Invalid) + { + close_camera_locked(); + return -6; + } + + if (camera_->configure(config_.get())) + { + close_camera_locked(); + return -7; + } + + cfg = config_->at(0); + if (!is_supported_preview_format(cfg.pixelFormat)) + { + close_camera_locked(); + return -8; + } + + stream_ = cfg.stream(); + stream_w_ = cfg.size.width; + stream_h_ = cfg.size.height; + stream_stride_ = cfg.stride; + stream_format_ = cfg.pixelFormat; + + allocator_ = std::make_unique(camera_); + if (allocator_->allocate(stream_) < 0) + { + close_camera_locked(); + return -9; + } + + const std::vector> &buffers = allocator_->buffers(stream_); + for (const std::unique_ptr &buffer : buffers) + { + auto planes = buffer->planes(); + if (planes.empty()) + continue; + + const libcamera::FrameBuffer::Plane &plane = planes[0]; + void *memory = mmap(nullptr, plane.length, PROT_READ | PROT_WRITE, MAP_SHARED, plane.fd.get(), plane.offset); + if (memory == MAP_FAILED) + continue; + + mapped_buffers_[buffer.get()] = {memory, plane.length, plane.fd.get()}; + + std::unique_ptr request = camera_->createRequest(); + if (!request || request->addBuffer(stream_, buffer.get()) < 0) + continue; + + requests_.push_back(std::move(request)); + } + + if (requests_.empty()) + { + close_camera_locked(); + return -10; + } + + camera_->requestCompleted.connect(this, &CameraSystem::request_complete); + if (camera_->start()) + { + close_camera_locked(); + return -11; + } + + for (std::unique_ptr &request : requests_) + camera_->queueRequest(request.get()); + + streaming_ = true; + return 0; + } + + int capture_impl(const std::string &path_arg, int width, int height, callback_t callback) + { + int ret = open_camera_impl(width, height); + if (ret != 0) + return ret; + + std::lock_guard lock(mutex_); + pending_capture_path_ = path_arg.empty() ? make_photo_path() : path_arg; + pending_capture_callback_ = callback; + capture_requested_.store(true); + return 0; + } + + void request_complete(libcamera::Request *request) + { + if (request->status() == libcamera::Request::RequestCancelled) + return; + + libcamera::FrameBuffer *buffer = nullptr; + { + std::lock_guard lock(mutex_); + auto it = request->buffers().find(stream_); + if (it == request->buffers().end()) + return; + buffer = it->second; + } + + std::string save_path; + callback_t callback; + std::vector rgb; + int save_w = 0; + int save_h = 0; + bool should_capture = capture_requested_.exchange(false); + + if (should_capture) + { + std::lock_guard lock(mutex_); + auto map_it = mapped_buffers_.find(buffer); + if (map_it != mapped_buffers_.end()) + { + const uint8_t *src = reinterpret_cast(map_it->second.addr); + size_t bytes_used = map_it->second.size; + const auto &metadata = buffer->metadata(); + if (!metadata.planes().empty() && metadata.planes()[0].bytesused > 0) + bytes_used = std::min(bytes_used, static_cast(metadata.planes()[0].bytesused)); + + dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); + bool converted = convert_to_rgb888(src, bytes_used, rgb); + dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); + + if (converted) + { + save_path = pending_capture_path_; + callback = pending_capture_callback_; + save_w = stream_w_; + save_h = stream_h_; + } + } + } + + if (!save_path.empty()) + { + bool ok = save_jpeg_rgb888(save_path, rgb.data(), save_w, save_h, 90); + report(callback, ok ? 0 : -12, ok ? save_path + "\n" : "camera save failed\n"); + } + + { + std::lock_guard lock(mutex_); + if (camera_ && streaming_) + { + request->reuse(libcamera::Request::ReuseBuffers); + camera_->queueRequest(request); + } + } + } + + void close_camera_locked() + { + const bool was_streaming = streaming_; + streaming_ = false; + capture_requested_.store(false); + + if (camera_) + { + camera_->requestCompleted.disconnect(this); + if (was_streaming) + camera_->stop(); + } + + requests_.clear(); + for (auto &it : mapped_buffers_) + { + if (it.second.addr && it.second.addr != MAP_FAILED) + munmap(it.second.addr, it.second.size); + } + mapped_buffers_.clear(); + allocator_.reset(); + + if (camera_) + { + camera_->release(); + camera_.reset(); + } + if (cm_) + { + cm_->stop(); + cm_.reset(); + } + config_.reset(); + stream_ = nullptr; + } +#endif + + int open_camera(int width, int height) + { + (void)width; + (void)height; +#if CP0_CAMERA_HAS_LIBCAMERA + return open_camera_impl(width, height); +#else + return -10; +#endif + } + + int capture(const std::string &path_arg, int width, int height, callback_t callback) + { + (void)path_arg; + (void)width; + (void)height; + (void)callback; +#if CP0_CAMERA_HAS_LIBCAMERA + return capture_impl(path_arg, width, height, callback); +#else + return -10; +#endif + } + + void close_camera() + { +#if CP0_CAMERA_HAS_LIBCAMERA + std::lock_guard lock(mutex_); + close_camera_locked(); +#endif + } +}; + +extern "C" void init_camera(void) +{ + std::shared_ptr camera = std::make_shared(); + + cp0_signal_camera_api.append([camera](std::list arg, std::function callback) + { camera->api_call(arg, callback); }); +} diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 55659ce3..11aede18 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -11,35 +11,12 @@ with open(env['PROJECT_TOOL_S']) as f: rootfs_path=os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "") -def wget_github(url): - repo = _parse.parse("{}://{}/{}/{}.git", url) - repo_name = repo[3] - local_path = os.path.join(os.environ['GIT_REPO_PATH'], repo_name) - if not os.path.exists(local_path): - github_url = url[:-4] if url.endswith('.git') else url - down_url = github_url + "/archive/refs/heads/master.zip" - zip_file_name = '{}-master.zip'.format(repo_name) - file_path = wget_zip(down_url, zip_file_name) - extracted_dir = os.path.join(file_path, '{}-master'.format(repo_name)) - if os.path.exists(extracted_dir): - shutil.move(extracted_dir, local_path) - shutil.rmtree(file_path) - else: - shutil.rmtree(file_path, ignore_errors=True) - down_url = github_url + "/archive/refs/heads/main.zip" - zip_file_name = '{}-main.zip'.format(repo_name) - file_path = wget_zip(down_url, zip_file_name) - extracted_dir = os.path.join(file_path, '{}-main'.format(repo_name)) - if os.path.exists(extracted_dir): - shutil.move(extracted_dir, local_path) - shutil.rmtree(file_path) - return local_path - # define the project's environment SRCS = [] INCLUDE = [] PRIVATE_INCLUDE = [] -REQUIREMENTS = [] +REQUIREMENTS = ['cp0_lvgl', 'eventpp'] +# REQUIREMENTS = [] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] @@ -105,7 +82,7 @@ lvgl_component['SRCS'] = list(filter( )) SRCS += Glob('hal/*.c*') -# x86 +# # x86 if 'linux_x86_sdl2_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") lvgl_component['REQUIREMENTS'] += pkg_config_ldflags("freetype2") @@ -148,25 +125,25 @@ if 'mac_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ' LDFLAGS += [f'{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/lib/aarch64-linux-gnu/libstdc++.so.6',f'-Wl,-rpath-link,{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/lib/aarch64-linux-gnu',f'-B{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/lib/aarch64-linux-gnu'] DEFINITIONS += ['-Wno-format-truncation'] -# macOS -if 'darwin_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): - # macOS 使用 SDL 后端 - SRCS += Glob('hal/sdl/*.c*') +# # macOS +# if 'darwin_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): +# # macOS 使用 SDL 后端 +# SRCS += Glob('hal/sdl/*.c*') - # nlohmann_json - DEFINITIONS += pkg_config_cflags("nlohmann_json") +# # nlohmann_json +# DEFINITIONS += pkg_config_cflags("nlohmann_json") - # SDL2 - DEFINITIONS += pkg_config_cflags("sdl2") - REQUIREMENTS += pkg_config_ldflags("sdl2") +# # SDL2 +# DEFINITIONS += pkg_config_cflags("sdl2") +# REQUIREMENTS += pkg_config_ldflags("sdl2") - # LVGL SDL driver 也需要 SDL2 头文件/定义 - lvgl_component['DEFINITIONS'] += pkg_config_cflags("sdl2") - lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] +# # LVGL SDL driver 也需要 SDL2 头文件/定义 +# lvgl_component['DEFINITIONS'] += pkg_config_cflags("sdl2") +# lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] - # FreeType - lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") - REQUIREMENTS += pkg_config_ldflags("freetype2") +# # FreeType +# lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") +# REQUIREMENTS += pkg_config_ldflags("freetype2") # add RadioLib diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index 9c87ed9d..3d47f4f1 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -32,7 +32,7 @@ extern "C" { static const char* lock_file = NULL; volatile uint32_t LV_EVENT_BATTERY; volatile uint32_t LV_EVENT_WIFI_INFO; - +volatile uint32_t LV_EVENT_DELL_CPP_DATA; @@ -69,10 +69,13 @@ int get_st7789v_fbdev(char *dev_path, size_t buf_size) if (fb_num < 0) { fprintf(stderr, "fb_st7789v not found in /proc/fb\n"); - return -1; } - - snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); + if (access("/dev/fb_lcd", F_OK) == 0) { + snprintf(dev_path, buf_size, "/dev/fb_lcd"); + } else { + fb_num = 0; + snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); + } return 0; } @@ -308,7 +311,8 @@ void APPLaunch_lock() } } - +extern "C" void init_audio(void); +extern "C" void init_camera(void); int main(void) { setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1); @@ -331,6 +335,7 @@ int main(void) LV_EVENT_KEYBOARD = lv_event_register_id(); LV_EVENT_BATTERY = lv_event_register_id(); + LV_EVENT_DELL_CPP_DATA = lv_event_register_id(); lv_timer_create(battery_timer_cb, 3000, NULL); // Restore saved brightness @@ -346,7 +351,8 @@ int main(void) if (saved_vol >= 0) hal_volume_write(saved_vol); } - + init_audio(); + init_camera(); ui_init(); // Force full-screen refresh immediately after init diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp index e127b3e9..a950ba40 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp @@ -5,13 +5,21 @@ #include "compat/input_keys.h" #include "hal_lvgl_bsp.h" #include +#include #include #include #include #include +#include +#include #include +#include +#include #include +#include #include +#include +#include #include /* @@ -45,6 +53,7 @@ class UICompassPage : public app_ static constexpr uint32_t kColorLevelMove = 0xE74C3C; static constexpr uint32_t kColorIconList = 0x33CC33; static constexpr uint32_t kColorIconExit = 0xFF0000; + static constexpr uint32_t kColorSensorWarn = 0xFF0000; static constexpr const char* ICON_EXIT = "\uEA01"; // .svgfont-exit static constexpr const char* ICON_LIST = "\uEA04"; // .svgfont-list @@ -62,20 +71,40 @@ class UICompassPage : public app_ ~UICompassPage() { + stop_sensor_thread(); + if (sensor_timer_) { + lv_timer_delete(sensor_timer_); + sensor_timer_ = nullptr; + } lv_freetype_font_delete(svg_font_); - // TODO(compass): 接入真实 Compass/IMU 接口后,在这里停止传感器轮询/释放资源。 } private: + struct IioDevicePaths { + std::string accel; + std::string magn; + bool hasGyro = false; + + bool ready() const + { + return !accel.empty() && !magn.empty(); + } + }; + struct CompassUiState { std::string statusText = "Compass"; float yaw = 0.0f; + float pitch = 0.0f; + float roll = 0.0f; float accX = 0.0f; float accY = 0.0f; float accZ = 0.0f; float gyrX = 0.0f; float gyrY = 0.0f; float gyrZ = 0.0f; + float magX = 0.0f; + float magY = 0.0f; + float magZ = 0.0f; bool sensorReady = false; }; @@ -91,9 +120,17 @@ class UICompassPage : public app_ lv_obj_t* level_dot_ = nullptr; lv_obj_t* lbl_acc_ = nullptr; lv_obj_t* lbl_gyr_ = nullptr; + lv_obj_t* sensor_missing_box_ = nullptr; + lv_obj_t* sensor_missing_label_ = nullptr; std::array lbl_bottom_btns_{}; std::array lbl_bottom_indicators_{}; + lv_timer_t* sensor_timer_ = nullptr; + std::thread sensor_thread_; + std::atomic sensor_running_{false}; + std::mutex sensor_mutex_; + CompassUiState sensor_state_{}; + bool sensor_state_dirty_ = false; CompassUiState last_state_{}; static lv_color_t color(uint32_t hex) @@ -117,9 +154,16 @@ class UICompassPage : public app_ create_compass_panel(ui_root); create_imu_panel(ui_root); create_bottom_bar(ui_root); - - // TODO(compass): 接入真实传感器后,在这里启动数据接口/定时 poll,并调用 update_from_state(state)。 - update_from_state(CompassUiState{}); + create_sensor_missing_overlay(ui_root); + + CompassUiState initial_state; + IioDevicePaths initial_paths = enumerate_iio_devices(); + initial_state.statusText = initial_paths.ready() ? "Sensor starting" : "IIO sensor missing"; + initial_state.sensorReady = initial_paths.ready(); + update_from_state(initial_state); + start_sensor_thread(); + sensor_timer_ = lv_timer_create(&UICompassPage::sensor_timer_cb, 50, this); + poll_sensor_once(); } void create_status_bar(lv_obj_t* parent) @@ -147,6 +191,7 @@ class UICompassPage : public app_ lv_img_set_src(compass_disc_, cp0_file_path("compass_disc_transparent.png").c_str()); lv_obj_set_pos(compass_disc_, 12, kStatusH + 1); lv_obj_set_size(compass_disc_, kCompassDia, kCompassDia); + lv_img_set_pivot(compass_disc_, kCompassDia / 2, kCompassDia / 2); lv_obj_clear_flag(compass_disc_, LV_OBJ_FLAG_SCROLLABLE); lbl_yaw_ = lv_label_create(parent); @@ -247,6 +292,31 @@ class UICompassPage : public app_ set_bottom_btn(2, ICON_EXIT, true, kColorIconExit); } + void create_sensor_missing_overlay(lv_obj_t* parent) + { + sensor_missing_box_ = lv_obj_create(parent); + lv_obj_remove_style_all(sensor_missing_box_); + lv_obj_set_size(sensor_missing_box_, 210, 32); + lv_obj_set_pos(sensor_missing_box_, 55, 4); + lv_obj_set_style_bg_color(sensor_missing_box_, color(0x220000), 0); + lv_obj_set_style_bg_opa(sensor_missing_box_, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sensor_missing_box_, 2, 0); + lv_obj_set_style_border_color(sensor_missing_box_, color(kColorSensorWarn), 0); + lv_obj_set_style_radius(sensor_missing_box_, 0, 0); + lv_obj_clear_flag(sensor_missing_box_, LV_OBJ_FLAG_SCROLLABLE); + + sensor_missing_label_ = lv_label_create(sensor_missing_box_); + lv_label_set_text(sensor_missing_label_, "No sensor device found"); + lv_obj_set_size(sensor_missing_label_, 206, 28); + lv_obj_set_pos(sensor_missing_label_, 2, 6); + lv_obj_set_style_text_font(sensor_missing_label_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(sensor_missing_label_, color(kColorSensorWarn), 0); + lv_obj_set_style_text_align(sensor_missing_label_, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_add_flag(sensor_missing_box_, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sensor_missing_box_); + } + void set_bottom_btn(int idx, const char* text, bool icon, uint32_t hex) { lv_obj_set_style_text_font(lbl_bottom_btns_[idx], @@ -256,6 +326,220 @@ class UICompassPage : public app_ lv_label_set_text(lbl_bottom_btns_[idx], text); } + /* + * ============================================================ + * IIO 驱动枚举与数据读取 + * ============================================================ + */ + static bool file_exists(const std::string& path) + { + struct stat st; + return stat(path.c_str(), &st) == 0; + } + + static bool read_text_file(const std::string& path, std::string& out) + { + std::ifstream ifs(path); + if (!ifs.is_open()) return false; + std::getline(ifs, out); + return true; + } + + static bool read_float_file(const std::string& path, float& out) + { + std::ifstream ifs(path); + if (!ifs.is_open()) return false; + ifs >> out; + return !ifs.fail(); + } + + static float read_float_file_or(const std::string& path, float fallback) + { + float v = fallback; + return read_float_file(path, v) ? v : fallback; + } + + static bool has_accel_files(const std::string& dir) + { + return file_exists(dir + "/in_accel_x_raw") && + file_exists(dir + "/in_accel_y_raw") && + file_exists(dir + "/in_accel_z_raw"); + } + + static bool has_magn_files(const std::string& dir) + { + return file_exists(dir + "/in_magn_x_raw") && + file_exists(dir + "/in_magn_y_raw") && + file_exists(dir + "/in_magn_z_raw"); + } + + static bool has_gyro_files(const std::string& dir) + { + return file_exists(dir + "/in_anglvel_x_raw") && + file_exists(dir + "/in_anglvel_y_raw") && + file_exists(dir + "/in_anglvel_z_raw"); + } + + static IioDevicePaths enumerate_iio_devices() + { + static constexpr const char* kIioRoot = "/sys/bus/iio/devices"; + IioDevicePaths paths; + + DIR* dp = opendir(kIioRoot); + if (!dp) return paths; + + while (dirent* ent = readdir(dp)) { + if (std::strncmp(ent->d_name, "iio:device", 10) != 0) continue; + + std::string dir = std::string(kIioRoot) + "/" + ent->d_name; + if (paths.accel.empty() && has_accel_files(dir)) { + paths.accel = dir; + paths.hasGyro = has_gyro_files(dir); + } + if (paths.magn.empty() && has_magn_files(dir)) { + paths.magn = dir; + } + } + + closedir(dp); + return paths; + } + + bool read_axis_triplet(const std::string& dir, const char* prefix, + float scale, float& x, float& y, float& z) const + { + float rx = 0.0f; + float ry = 0.0f; + float rz = 0.0f; + if (!read_float_file(dir + "/" + prefix + "_x_raw", rx)) return false; + if (!read_float_file(dir + "/" + prefix + "_y_raw", ry)) return false; + if (!read_float_file(dir + "/" + prefix + "_z_raw", rz)) return false; + + x = rx * scale; + y = ry * scale; + z = rz * scale; + return true; + } + + bool read_iio_state(IioDevicePaths& paths, CompassUiState& state) + { + if (!paths.ready()) { + paths = enumerate_iio_devices(); + } + + if (!paths.ready()) { + state.statusText = "IIO sensor missing"; + state.sensorReady = false; + return false; + } + + const float acc_scale = read_float_file_or(paths.accel + "/in_accel_scale", 1.0f); + const float gyr_scale = read_float_file_or(paths.accel + "/in_anglvel_scale", 1.0f); + const float mag_scale = read_float_file_or(paths.magn + "/in_magn_scale", 1.0f); + + float acc_x = 0.0f, acc_y = 0.0f, acc_z = 0.0f; + float mag_x = 0.0f, mag_y = 0.0f, mag_z = 0.0f; + float gyr_x = 0.0f, gyr_y = 0.0f, gyr_z = 0.0f; + + if (!read_axis_triplet(paths.accel, "in_accel", acc_scale, acc_x, acc_y, acc_z) || + !read_axis_triplet(paths.magn, "in_magn", mag_scale, mag_x, mag_y, mag_z)) { + state.statusText = "IIO read failed"; + state.sensorReady = false; + return false; + } + + if (paths.hasGyro) { + read_axis_triplet(paths.accel, "in_anglvel", gyr_scale, gyr_x, gyr_y, gyr_z); + } + + float pitch = std::atan2(-acc_x, std::sqrt(acc_y * acc_y + acc_z * acc_z)); + float roll = std::atan2(acc_y, acc_z); + float sin_p = std::sin(pitch); + float cos_p = std::cos(pitch); + float sin_r = std::sin(roll); + float cos_r = std::cos(roll); + + float mag_x_h = mag_x * cos_p + mag_z * sin_p; + float mag_y_h = mag_x * sin_r * sin_p + mag_y * cos_r - mag_z * sin_r * cos_p; + float yaw = std::atan2(-mag_y_h, mag_x_h) * 180.0f / 3.1415926f; + if (yaw < 0.0f) yaw += 360.0f; + + state.statusText = "Sensor OK"; + state.sensorReady = true; + state.yaw = yaw; + state.pitch = pitch * 180.0f / 3.1415926f; + state.roll = roll * 180.0f / 3.1415926f; + state.accX = acc_x; + state.accY = acc_y; + state.accZ = acc_z; + state.gyrX = gyr_x; + state.gyrY = gyr_y; + state.gyrZ = gyr_z; + state.magX = mag_x; + state.magY = mag_y; + state.magZ = mag_z; + return true; + } + + void start_sensor_thread() + { + if (sensor_running_.load()) return; + sensor_running_ = true; + sensor_thread_ = std::thread(&UICompassPage::sensor_thread_func, this); + } + + void stop_sensor_thread() + { + sensor_running_ = false; + if (sensor_thread_.joinable()) { + sensor_thread_.join(); + } + } + + void publish_sensor_state(const CompassUiState& state) + { + std::lock_guard lock(sensor_mutex_); + sensor_state_ = state; + sensor_state_dirty_ = true; + } + + void sensor_thread_func() + { + IioDevicePaths paths = enumerate_iio_devices(); + CompassUiState state; + + while (sensor_running_.load()) { + read_iio_state(paths, state); + publish_sensor_state(state); + usleep(50000); /* 50 ms => 20 Hz, matching the original Compass app */ + } + } + + void poll_sensor_once() + { + CompassUiState state; + bool dirty = false; + { + std::lock_guard lock(sensor_mutex_); + dirty = sensor_state_dirty_; + if (dirty) { + state = sensor_state_; + sensor_state_dirty_ = false; + } + } + + if (dirty) { + update_from_state(state); + } + } + + static void sensor_timer_cb(lv_timer_t* t) + { + auto* self = static_cast(lv_timer_get_user_data(t)); + if (!self) return; + self->poll_sensor_once(); + } + /* * ============================================================ * UI 状态刷新 @@ -269,6 +553,15 @@ class UICompassPage : public app_ lv_label_set_text(lbl_status_text_, state.statusText.c_str()); } + if (sensor_missing_box_) { + if (state.sensorReady) { + lv_obj_add_flag(sensor_missing_box_, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_clear_flag(sensor_missing_box_, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sensor_missing_box_); + } + } + if (lbl_yaw_) { std::snprintf(buf, sizeof(buf), "%.0f deg %s", state.yaw, direction_text(state.yaw)); lv_label_set_text(lbl_yaw_, state.sensorReady ? buf : "---"); @@ -286,6 +579,10 @@ class UICompassPage : public app_ lv_label_set_text(lbl_gyr_, state.sensorReady ? buf : "GYR: --- --- ---"); } + if (compass_disc_) { + lv_img_set_angle(compass_disc_, state.sensorReady ? -(int16_t)(state.yaw * 10.0f) : 0); + } + update_level_dot(state); last_state_ = state; } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp index 60fc7bf1..55bc41e8 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp @@ -1,503 +1,1344 @@ #pragma once + #include "../ui_app_page.hpp" -#include -#include -#include + +#include +#include +#include +#include +#include +#include #include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include "hal/hal_process.h" +#include +#include + #include "compat/input_keys.h" +#include "hal_lvgl_bsp.h" -// ============================================================ -// Audio Recorder UIRecPage -// Screen: 320 x 170 (top bar 20px, ui_APP_Container 320x150) -// -// States: IDLE -> RECORDING -> IDLE, IDLE -> PLAYING -> IDLE -// -// Keys: -// R Start recording (arecord) -// S Stop recording / playback -// P Play last recording (aplay) -// D Delete selected recording -// UP/DOWN Navigate recording list -// ESC Stop active process, go back home -// ============================================================ -class UIRecPage : public app_base +class rec_page : public app_base { - enum class RecState { IDLE, RECORDING, PLAYING }; +public: + lv_font_t *svg_font = NULL; + static constexpr uint32_t kColorText = 0xFFFFFF; + static constexpr uint32_t kColorIconStop = 0xFF0000; + static constexpr uint32_t kColorIconRecord = 0xFF3399; + static constexpr uint32_t kColorIconSampleRate = 0xFFCC00; + static constexpr uint32_t kColorIconList = 0x33CC33; + + typedef enum + { + ICON_EXIT = 0, + ICON_FAST_FORWARD , + ICON_FAST_REWIND, + ICON_LIST, + ICON_PAUSE, + ICON_PLAY, + ICON_RECORD, + ICON_SAMPLE_RATE, + ICON_SPEED, + ICON_STOP, + ICON_COUNT, + } ICON_t; + + std::array, ICON_COUNT> icon_map = {{ + {ICON_EXIT, "\uEA01"}, + {ICON_FAST_FORWARD, "\uEA02"}, + {ICON_FAST_REWIND, "\uEA03"}, + {ICON_LIST, "\uEA04"}, + {ICON_PAUSE, "\uEA05"}, + {ICON_PLAY, "\uEA06"}, + {ICON_RECORD, "\uEA07"}, + {ICON_SAMPLE_RATE, "\uEA08"}, + {ICON_SPEED, "\uEA09"}, + {ICON_STOP, "\uEA0A"}, + }}; public: - UIRecPage() : app_base() + lv_obj_t *ui_BOTTOM_Container; + lv_obj_t *but[5]; + +public: + rec_page() : app_base() + { + app_name = "Recorder"; + set_page_title(app_name); + svg_font = lv_freetype_font_create( + cp0_file_path("svgfont.ttf").c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, + LV_FREETYPE_FONT_STYLE_NORMAL); + init_APP_UI(); + creat_BOTTOM_UI(); + } + ~rec_page() { - set_page_title("REC"); - creat_UI(); - event_handler_init(); + if (svg_font) + lv_freetype_font_delete(svg_font); } - ~UIRecPage() + struct lvgl_call_d_t + { + void *p1; + std::function callback; + }; + + static void lvgl_event_handler(lv_event_t *e) { - stop_process(); - if (elapsed_timer_) lv_timer_delete(elapsed_timer_); - if (blink_timer_) lv_timer_delete(blink_timer_); + lvgl_call_d_t *t = static_cast(lv_event_get_user_data(e)); + if (!t) + return; + lv_event_code_t c = lv_event_get_code(e); + if (c == LV_EVENT_DELETE) + { + delete t; + return; + } + + auto callback = t->callback; + void *event_param = lv_event_get_param(e); + void *user_data = t->p1; + if (callback) + { + try + { + callback(c, event_param, user_data); + } + catch (...) + { + fprintf(stderr, "[LVGL] C++ event callback threw\n"); + } + } } + void lvgl_add_call(lv_obj_t *obj, std::function callback, void *d) + { + if (!obj || !callback) + return; + { + uint32_t event_cnt = lv_obj_get_event_count(obj); + int32_t i; + if (event_cnt != 0) + for (i = event_cnt - 1; i >= 0; i--) + { + lv_event_dsc_t *dsc = lv_obj_get_event_dsc(obj, i); + if (dsc && (lv_event_dsc_get_cb(dsc) == rec_page::lvgl_event_handler)) + { + lvgl_call_d_t *data = static_cast(lv_event_dsc_get_user_data(dsc)); + lv_obj_remove_event(obj, i); + delete data; + } + } + } + lvgl_call_d_t *t = new lvgl_call_d_t; + t->p1 = d; + t->callback = std::move(callback); + lv_obj_add_event_cb(obj, rec_page::lvgl_event_handler, LV_EVENT_ALL, t); + } + const char *icon_text(ICON_t icon) + { + const size_t index = static_cast(icon); + return index < icon_map.size() ? icon_map[index].second.c_str() : ""; + } private: - // ==================== data members ==================== - std::unordered_map ui_obj_; - std::vector recordings_; - int selected_idx_ = 0; - int rec_counter_ = 0; - RecState state_ = RecState::IDLE; - hal_pid_t active_pid_ = -1; - std::string current_file_; - int elapsed_sec_ = 0; - lv_timer_t *elapsed_timer_ = nullptr; - lv_timer_t *blink_timer_ = nullptr; - bool blink_visible_ = true; - - // ==================== UI construction ==================== - void creat_UI() - { - // background + static uint8_t idle_wave_height(int index) + { + const float phase = static_cast(index) * 0.72f; + const float envelope = 0.55f + 0.45f * std::fabs(std::sin(phase * 0.37f)); + const float pulse = 0.5f + 0.5f * std::sin(phase); + return static_cast(4.0f + envelope * pulse * 34.0f); + } + + void init_APP_UI() + { + lv_obj_set_height(ui_APP_Container, 120); + lv_obj_set_y(ui_APP_Container, 5); + lv_obj_clear_flag(ui_APP_Container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t *bg = lv_obj_create(ui_APP_Container); - lv_obj_set_size(bg, 320, 150); + lv_obj_remove_style_all(bg); + lv_obj_set_size(bg, 320, 120); lv_obj_set_pos(bg, 0, 0); - lv_obj_set_style_radius(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(bg, lv_color_hex(0x0D1117), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(bg, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["bg"] = bg; - - // title bar - lv_obj_t *title_bar = lv_obj_create(bg); - lv_obj_set_size(title_bar, 320, 22); - lv_obj_set_pos(title_bar, 0, 0); - lv_obj_set_style_radius(title_bar, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(title_bar, lv_color_hex(0x1F3A5F), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(title_bar, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(title_bar, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(title_bar, 8, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *lbl_title = lv_label_create(title_bar); - lv_label_set_text(lbl_title, "Recorder"); - lv_obj_set_align(lbl_title, LV_ALIGN_LEFT_MID); - lv_obj_set_style_text_color(lbl_title, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lbl_hint = lv_label_create(title_bar); - lv_label_set_text(lbl_hint, "R:Rec S:Stop P:Play ESC:Back"); - lv_obj_set_align(lbl_hint, LV_ALIGN_RIGHT_MID); - lv_obj_set_x(lbl_hint, -4); - lv_obj_set_style_text_color(lbl_hint, lv_color_hex(0x7EA8D8), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - - // content area - lv_obj_t *content = lv_obj_create(bg); - lv_obj_set_size(content, 320, 128); - lv_obj_set_pos(content, 0, 22); - lv_obj_set_style_radius(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(content, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["content"] = content; - - // red dot (for blinking during recording) - lv_obj_t *red_dot = lv_obj_create(content); - lv_obj_set_size(red_dot, 10, 10); - lv_obj_set_pos(red_dot, 10, 10); - lv_obj_set_style_radius(red_dot, 5, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(red_dot, lv_color_hex(0xE74C3C), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(red_dot, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(red_dot, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(red_dot, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(red_dot, LV_OBJ_FLAG_HIDDEN); - ui_obj_["red_dot"] = red_dot; - - // status label (READY / RECORDING / PLAYING) - lv_obj_t *lbl_status = lv_label_create(content); - lv_label_set_text(lbl_status, "READY"); - lv_obj_set_pos(lbl_status, 26, 4); - lv_obj_set_style_text_color(lbl_status, lv_color_hex(0xE6EDF3), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["lbl_status"] = lbl_status; - - // elapsed time label - lv_obj_t *lbl_time = lv_label_create(content); - lv_label_set_text(lbl_time, "00:00"); - lv_obj_set_pos(lbl_time, 120, 4); - lv_obj_set_style_text_color(lbl_time, lv_color_hex(0xE6EDF3), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_time, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["lbl_time"] = lbl_time; - - // file info label - lv_obj_t *lbl_file = lv_label_create(content); - lv_label_set_text(lbl_file, "File: (none)"); - lv_obj_set_pos(lbl_file, 10, 24); - lv_obj_set_width(lbl_file, 300); - lv_label_set_long_mode(lbl_file, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_set_style_text_color(lbl_file, lv_color_hex(0x7EA8D8), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_file, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["lbl_file"] = lbl_file; - - // separator line - lv_obj_t *sep = lv_obj_create(content); - lv_obj_set_size(sep, 300, 1); - lv_obj_set_pos(sep, 10, 38); - lv_obj_set_style_radius(sep, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(sep, lv_color_hex(0x555555), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(sep, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(sep, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(sep, LV_OBJ_FLAG_SCROLLABLE); - - // recordings list label - lv_obj_t *lbl_list_title = lv_label_create(content); - lv_label_set_text(lbl_list_title, "Recordings:"); - lv_obj_set_pos(lbl_list_title, 10, 42); - lv_obj_set_style_text_color(lbl_list_title, lv_color_hex(0xE6EDF3), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_list_title, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - - // recordings list container (shows up to 5 items) - lv_obj_t *list_cont = lv_obj_create(content); - lv_obj_set_size(list_cont, 300, 70); - lv_obj_set_pos(list_cont, 10, 56); - lv_obj_set_style_radius(list_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(list_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(list_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(list_cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(list_cont, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["list_cont"] = list_cont; - - build_rec_list(); - } - - // ==================== recording list rows ==================== - void build_rec_list() - { - lv_obj_t *list_cont = ui_obj_["list_cont"]; - lv_obj_clean(list_cont); + lv_obj_set_style_bg_color(bg, lv_color_hex(0x0D1117), 0); + lv_obj_set_style_bg_opa(bg, LV_OPA_COVER, 0); + lv_obj_clear_flag(bg, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - if (recordings_.empty()) + lv_obj_t *file = lv_label_create(bg); + lv_obj_set_pos(file, 8, 6); + lv_obj_set_width(file, 304); + lv_label_set_long_mode(file, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_font(file, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(file, lv_color_hex(0xAAB6C4), 0); + lv_label_set_text(file, "rec_0001.wav"); + + lv_obj_t *wave_bg = lv_obj_create(bg); + lv_obj_remove_style_all(wave_bg); + lv_obj_set_size(wave_bg, 304, 56); + lv_obj_set_pos(wave_bg, 8, 25); + lv_obj_set_style_radius(wave_bg, 4, 0); + lv_obj_set_style_bg_color(wave_bg, lv_color_hex(0x161B22), 0); + lv_obj_set_style_bg_opa(wave_bg, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(wave_bg, lv_color_hex(0x30363D), 0); + lv_obj_set_style_border_width(wave_bg, 1, 0); + lv_obj_clear_flag(wave_bg, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + + constexpr int bar_count = 40; + const int bar_w = 4; + const int gap = 3; + const int start_x = 9; + const int mid_y = 28; + for (int i = 0; i < bar_count; ++i) { - lv_obj_t *lbl = lv_label_create(list_cont); - lv_label_set_text(lbl, "(no recordings yet)"); - lv_obj_set_pos(lbl, 0, 0); - lv_obj_set_style_text_color(lbl, lv_color_hex(0x555555), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - return; + int h = idle_wave_height(i); + lv_obj_t *bar = lv_obj_create(wave_bg); + lv_obj_remove_style_all(bar); + lv_obj_set_size(bar, bar_w, h); + lv_obj_set_pos(bar, start_x + i * (bar_w + gap), mid_y - h / 2); + lv_obj_set_style_radius(bar, 2, 0); + lv_obj_set_style_bg_color(bar, lv_color_hex(0xFF8800), 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_clear_flag(bar, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } - static constexpr int ROW_H = 14; - static constexpr int MAX_VISIBLE = 5; + lv_obj_t *timer = lv_label_create(bg); + lv_obj_set_pos(timer, 0, 85); + lv_obj_set_width(timer, 320); + lv_obj_set_style_text_font(timer, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(timer, lv_color_hex(kColorText), 0); + lv_obj_set_style_text_align(timer, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(timer, "00:00"); - int count = (int)recordings_.size(); - int visible = (count < MAX_VISIBLE) ? count : MAX_VISIBLE; - int offset = selected_idx_ - visible / 2; - if (offset < 0) offset = 0; - if (offset > count - visible) offset = count - visible; - if (offset < 0) offset = 0; + lv_obj_t *sample_rate = lv_label_create(bg); + lv_obj_set_pos(sample_rate, 8, 101); + lv_obj_set_width(sample_rate, 90); + lv_obj_set_style_text_font(sample_rate, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(sample_rate, lv_color_hex(0x8B949E), 0); + lv_label_set_text(sample_rate, "44.1kHz"); - for (int vi = 0; vi < visible; ++vi) + lv_obj_t *hint = lv_label_create(bg); + lv_obj_set_pos(hint, 218, 101); + lv_obj_set_width(hint, 94); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(hint, lv_color_hex(0x8B949E), 0); + lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_RIGHT, 0); + lv_label_set_text(hint, "REC / LIST"); + } + + + + void creat_BOTTOM_UI() + { + ui_BOTTOM_Container = lv_obj_create(ui_root); + lv_obj_remove_style_all(ui_BOTTOM_Container); + lv_obj_set_width(ui_BOTTOM_Container, 320); + lv_obj_set_height(ui_BOTTOM_Container, 25); + lv_obj_set_x(ui_BOTTOM_Container, 0); + lv_obj_set_y(ui_BOTTOM_Container, 145); + lv_obj_set_align(ui_BOTTOM_Container, LV_ALIGN_TOP_LEFT); + lv_obj_clear_flag(ui_BOTTOM_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + + const char *icons[5] = { + icon_text(ICON_EXIT), + icon_text(ICON_STOP), + icon_text(ICON_RECORD), + icon_text(ICON_SAMPLE_RATE), + icon_text(ICON_LIST), + }; + const uint32_t colors[5] = { + kColorText, + kColorIconStop, + kColorIconRecord, + kColorIconSampleRate, + kColorIconList, + }; + for (int i = 0; i < 5; i++) { - int ri = vi + offset; - bool is_sel = (ri == selected_idx_); - - lv_obj_t *lbl = lv_label_create(list_cont); - // extract just the filename for display - const std::string &path = recordings_[ri]; - std::string display = path; - size_t slash = path.rfind('/'); - if (slash != std::string::npos) - display = path.substr(slash + 1); - - char buf[64]; - snprintf(buf, sizeof(buf), "%s %s", is_sel ? ">" : " ", display.c_str()); - lv_label_set_text(lbl, buf); - lv_obj_set_pos(lbl, 0, vi * ROW_H); - lv_obj_set_width(lbl, 300); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); - lv_obj_set_style_text_color(lbl, - is_sel ? lv_color_hex(0x1F6FEB) : lv_color_hex(0xE6EDF3), - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); + but[i] = lv_btn_create(ui_BOTTOM_Container); + lv_obj_remove_style_all(but[i]); + lv_obj_clear_flag(but[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(but[i], LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(but[i], 0, 0); + lv_obj_set_style_shadow_width(but[i], 0, 0); + lv_obj_set_style_pad_all(but[i], 0, 0); + + lv_obj_t *label = lv_label_create(but[i]); + lv_obj_set_style_text_font(label, svg_font ? svg_font : &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(label, lv_color_hex(colors[i]), 0); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(label, icons[i]); + lv_obj_update_layout(label); + + lv_coord_t label_w = lv_obj_get_width(label); + lv_coord_t label_h = lv_obj_get_height(label); + lv_obj_set_size(but[i], label_w, label_h); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_pos(but[i], i * 64 + (64 - label_w) / 2, (25 - label_h) / 2); + } + + for (int i = 0; i < 5; i++) + { + lv_obj_add_flag(but[i], LV_OBJ_FLAG_CLICKABLE); } + // lvgl_add_call(but[0], [](lv_event_code_t c, void *d){ + // if(c == LV_EVENT_CLICKED) + // { + // printf("butt will be clicked\n"); + // } + // }, NULL); } +}; - // ==================== state update UI ==================== - void update_status_ui() +namespace rec_ui2 +{ +static constexpr int kScreenW = 320; +static constexpr int kContentH = 120; +static constexpr int kBtnCount = 5; +static constexpr int kWaveBarCount = 40; +static constexpr uint32_t kBg = 0x0D1117; +static constexpr uint32_t kPanel = 0x161B22; +static constexpr uint32_t kBorder = 0x30363D; +static constexpr uint32_t kText = 0xFFFFFF; +static constexpr uint32_t kMuted = 0x8B949E; +static constexpr uint32_t kAccent = 0x1F9DFF; +static constexpr uint32_t kWave = 0xFF8800; + +static inline lv_color_t color(uint32_t hex) +{ + return lv_color_hex(hex); +} + +static inline void prep_page(lv_obj_t *obj) +{ + lv_obj_remove_style_all(obj); + lv_obj_set_size(obj, kScreenW, kContentH); + lv_obj_set_pos(obj, 0, 0); + lv_obj_set_style_bg_color(obj, color(kBg), 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); +} +} + +class RecHomeView +{ +public: + static constexpr size_t kWavePointCount = 128; + + void create(lv_obj_t *page) { - lv_obj_t *lbl_status = ui_obj_["lbl_status"]; - lv_obj_t *red_dot = ui_obj_["red_dot"]; + lbl_file_ = lv_label_create(page); + lv_obj_set_pos(lbl_file_, 8, 6); + lv_obj_set_width(lbl_file_, 304); + lv_label_set_long_mode(lbl_file_, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_font(lbl_file_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_file_, rec_ui2::color(0xAAB6C4), 0); - switch (state_) + wave_bg_ = lv_obj_create(page); + lv_obj_remove_style_all(wave_bg_); + lv_obj_set_size(wave_bg_, 304, 56); + lv_obj_set_pos(wave_bg_, 8, 25); + lv_obj_set_style_radius(wave_bg_, 4, 0); + lv_obj_set_style_bg_color(wave_bg_, rec_ui2::color(rec_ui2::kPanel), 0); + lv_obj_set_style_bg_opa(wave_bg_, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(wave_bg_, rec_ui2::color(rec_ui2::kBorder), 0); + lv_obj_set_style_border_width(wave_bg_, 1, 0); + lv_obj_clear_flag(wave_bg_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + + for (int i = 0; i < rec_ui2::kWaveBarCount; ++i) { - case RecState::IDLE: - lv_label_set_text(lbl_status, "READY"); - lv_obj_set_style_text_color(lbl_status, lv_color_hex(0xE6EDF3), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_flag(red_dot, LV_OBJ_FLAG_HIDDEN); - stop_blink_timer(); - break; - case RecState::RECORDING: - lv_label_set_text(lbl_status, "RECORDING"); - lv_obj_set_style_text_color(lbl_status, lv_color_hex(0xE74C3C), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(red_dot, LV_OBJ_FLAG_HIDDEN); - start_blink_timer(); - break; - case RecState::PLAYING: - lv_label_set_text(lbl_status, "PLAYING"); - lv_obj_set_style_text_color(lbl_status, lv_color_hex(0x2ECC71), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_flag(red_dot, LV_OBJ_FLAG_HIDDEN); - stop_blink_timer(); - break; + bars_[i] = lv_obj_create(wave_bg_); + lv_obj_remove_style_all(bars_[i]); + lv_obj_set_size(bars_[i], 4, 1); + lv_obj_set_style_radius(bars_[i], 2, 0); + lv_obj_set_style_bg_color(bars_[i], rec_ui2::color(rec_ui2::kWave), 0); + lv_obj_set_style_bg_opa(bars_[i], LV_OPA_COVER, 0); + lv_obj_clear_flag(bars_[i], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } + + lbl_time_ = lv_label_create(page); + lv_obj_set_pos(lbl_time_, 0, 85); + lv_obj_set_width(lbl_time_, 320); + lv_obj_set_style_text_font(lbl_time_, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(lbl_time_, rec_ui2::color(rec_ui2::kText), 0); + lv_obj_set_style_text_align(lbl_time_, LV_TEXT_ALIGN_CENTER, 0); + + lbl_rate_ = lv_label_create(page); + lv_obj_set_pos(lbl_rate_, 8, 101); + lv_obj_set_width(lbl_rate_, 90); + lv_obj_set_style_text_font(lbl_rate_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(lbl_rate_, rec_ui2::color(rec_ui2::kMuted), 0); + + lbl_hint_ = lv_label_create(page); + lv_obj_set_pos(lbl_hint_, 178, 101); + lv_obj_set_width(lbl_hint_, 134); + lv_obj_set_style_text_font(lbl_hint_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(lbl_hint_, rec_ui2::color(rec_ui2::kMuted), 0); + lv_obj_set_style_text_align(lbl_hint_, LV_TEXT_ALIGN_RIGHT, 0); } - void update_time_label() + void refresh(bool recording, + const std::string &file, + const std::string &time, + const std::string &rate) { - char buf[16]; - int mins = elapsed_sec_ / 60; - int secs = elapsed_sec_ % 60; - snprintf(buf, sizeof(buf), "%02d:%02d", mins, secs); - lv_label_set_text(ui_obj_["lbl_time"], buf); + if (!lbl_file_) + return; + + lv_label_set_text(lbl_file_, file.empty() ? "No recordings" : file.c_str()); + lv_label_set_text(lbl_time_, recording ? time.c_str() : "00:00"); + lv_label_set_text(lbl_rate_, rate.c_str()); + lv_label_set_text(lbl_hint_, recording ? "RECORDING" : "REC / LIST"); + refresh_bars(recording); } - void update_file_label() + void set_waveform(const std::string &data) { - if (current_file_.empty()) - lv_label_set_text(ui_obj_["lbl_file"], "File: (none)"); - else + if (data.size() < sizeof(float) * waveform_.size()) + return; + std::lock_guard lock(waveform_mutex_); + std::memcpy(waveform_.data(), data.data(), sizeof(float) * waveform_.size()); + } + + void reset_waveform() + { + std::lock_guard lock(waveform_mutex_); + waveform_.fill(0.0f); + } + + size_t waveform_byte_size() const + { + return sizeof(float) * waveform_.size(); + } + +private: + void refresh_bars(bool recording) + { + std::array snapshot{}; + if (recording) + { + std::lock_guard lock(waveform_mutex_); + snapshot = waveform_; + } + + constexpr int bar_w = 4; + constexpr int gap = 3; + constexpr int start_x = 9; + constexpr int mid_y = 28; + constexpr int max_h = 48; + for (int i = 0; i < rec_ui2::kWaveBarCount; ++i) { - char buf[128]; - snprintf(buf, sizeof(buf), "File: %s", current_file_.c_str()); - lv_label_set_text(ui_obj_["lbl_file"], buf); + float max_val = 0.02f; + if (recording) + { + int start = i * static_cast(kWavePointCount) / rec_ui2::kWaveBarCount; + int end = (i + 1) * static_cast(kWavePointCount) / rec_ui2::kWaveBarCount; + for (int j = start; j < end; ++j) + { + max_val = std::max(max_val, std::min(1.0f, std::fabs(snapshot[j]))); + } + } + else + { + max_val = 0.05f + static_cast((i * 7) % 9) / 28.0f; + } + int h = std::max(2, static_cast(max_val * max_h)); + lv_obj_set_size(bars_[i], bar_w, h); + lv_obj_set_pos(bars_[i], start_x + i * (bar_w + gap), mid_y - h / 2); } } - // ==================== timers ==================== - // elapsed time timer (1 second interval) - static void elapsed_timer_cb(lv_timer_t *t) + lv_obj_t *lbl_file_ = nullptr; + lv_obj_t *wave_bg_ = nullptr; + std::array bars_{}; + lv_obj_t *lbl_time_ = nullptr; + lv_obj_t *lbl_rate_ = nullptr; + lv_obj_t *lbl_hint_ = nullptr; + std::array waveform_{}; + std::mutex waveform_mutex_; +}; + +class RecSaveConfirmView +{ +public: + void create(lv_obj_t *page) { - UIRecPage *self = static_cast(lv_timer_get_user_data(t)); - if (self) self->on_elapsed_tick(); + lbl_title_ = lv_label_create(page); + lv_obj_set_pos(lbl_title_, 8, 10); + lv_obj_set_width(lbl_title_, 304); + lv_obj_set_style_text_font(lbl_title_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_title_, rec_ui2::color(rec_ui2::kMuted), 0); + lv_label_set_text(lbl_title_, "Save recording as"); + + ta_name_ = lv_textarea_create(page); + lv_obj_set_pos(ta_name_, 8, 34); + lv_obj_set_size(ta_name_, 304, 34); + lv_obj_set_style_text_font(ta_name_, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(ta_name_, rec_ui2::color(rec_ui2::kText), 0); + lv_obj_set_style_bg_color(ta_name_, rec_ui2::color(rec_ui2::kPanel), 0); + lv_obj_set_style_bg_opa(ta_name_, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(ta_name_, rec_ui2::color(rec_ui2::kAccent), 0); + lv_obj_set_style_border_width(ta_name_, 1, 0); + lv_obj_set_style_radius(ta_name_, 4, 0); + lv_textarea_set_one_line(ta_name_, true); + + lbl_hint_ = lv_label_create(page); + lv_obj_set_pos(lbl_hint_, 8, 84); + lv_obj_set_width(lbl_hint_, 304); + lv_obj_set_style_text_font(lbl_hint_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(lbl_hint_, rec_ui2::color(rec_ui2::kMuted), 0); + lv_label_set_text(lbl_hint_, "Back: discard Prev/Next: rename Rec: reset List: save"); } - void on_elapsed_tick() + void set_name(const std::string &name) { - if (state_ == RecState::RECORDING || state_ == RecState::PLAYING) + if (ta_name_) + lv_textarea_set_text(ta_name_, name.c_str()); + } + + std::string name() const + { + return ta_name_ ? lv_textarea_get_text(ta_name_) : ""; + } + +private: + lv_obj_t *lbl_title_ = nullptr; + lv_obj_t *ta_name_ = nullptr; + lv_obj_t *lbl_hint_ = nullptr; +}; + +class RecFileListView +{ +public: + void create(lv_obj_t *page) + { + for (int i = 0; i < 5; ++i) { - elapsed_sec_++; - update_time_label(); + items_[i] = lv_label_create(page); + lv_obj_set_pos(items_[i], 8, 8 + i * 21); + lv_obj_set_width(items_[i], 304); + lv_obj_set_style_text_font(items_[i], &lv_font_montserrat_12, 0); + lv_label_set_long_mode(items_[i], LV_LABEL_LONG_CLIP); } + + empty_ = lv_label_create(page); + lv_obj_set_width(empty_, 320); + lv_obj_set_style_text_font(empty_, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(empty_, rec_ui2::color(rec_ui2::kMuted), 0); + lv_obj_set_style_text_align(empty_, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(empty_, "Empty"); + lv_obj_center(empty_); } - void start_elapsed_timer() + void refresh(const std::vector &files, int selected) { - elapsed_sec_ = 0; - update_time_label(); - if (!elapsed_timer_) - elapsed_timer_ = lv_timer_create(elapsed_timer_cb, 1000, this); - else - lv_timer_reset(elapsed_timer_); + if (files.empty()) + { + lv_obj_clear_flag(empty_, LV_OBJ_FLAG_HIDDEN); + for (auto *item : items_) + lv_obj_add_flag(item, LV_OBJ_FLAG_HIDDEN); + offset_ = 0; + return; + } + + lv_obj_add_flag(empty_, LV_OBJ_FLAG_HIDDEN); + if (selected < offset_) + offset_ = selected; + if (selected >= offset_ + 5) + offset_ = selected - 4; + if (offset_ < 0) + offset_ = 0; + + for (int i = 0; i < 5; ++i) + { + int idx = offset_ + i; + if (idx >= static_cast(files.size())) + { + lv_obj_add_flag(items_[i], LV_OBJ_FLAG_HIDDEN); + continue; + } + + std::string text = (idx == selected ? "> " : " ") + files[idx]; + lv_label_set_text(items_[i], text.c_str()); + lv_obj_set_style_text_color(items_[i], + rec_ui2::color(idx == selected ? rec_ui2::kAccent : rec_ui2::kText), + 0); + lv_obj_clear_flag(items_[i], LV_OBJ_FLAG_HIDDEN); + } } - void stop_elapsed_timer() +private: + lv_obj_t *empty_ = nullptr; + std::array items_{}; + int offset_ = 0; +}; + +class RecPlaybackView +{ +public: + void create(lv_obj_t *page) { - if (elapsed_timer_) + lbl_file_ = lv_label_create(page); + lv_obj_set_pos(lbl_file_, 8, 6); + lv_obj_set_width(lbl_file_, 304); + lv_obj_set_style_text_font(lbl_file_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_file_, rec_ui2::color(rec_ui2::kText), 0); + lv_label_set_long_mode(lbl_file_, LV_LABEL_LONG_SCROLL_CIRCULAR); + + for (int i = 0; i < rec_ui2::kWaveBarCount; ++i) { - lv_timer_delete(elapsed_timer_); - elapsed_timer_ = nullptr; + bars_[i] = lv_obj_create(page); + lv_obj_remove_style_all(bars_[i]); + lv_obj_set_style_bg_color(bars_[i], rec_ui2::color(0x888888), 0); + lv_obj_set_style_bg_opa(bars_[i], LV_OPA_COVER, 0); + lv_obj_set_style_radius(bars_[i], 1, 0); + lv_obj_clear_flag(bars_[i], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } + + progress_ = lv_obj_create(page); + lv_obj_remove_style_all(progress_); + lv_obj_set_size(progress_, 2, 50); + lv_obj_set_style_bg_color(progress_, rec_ui2::color(0xFF3333), 0); + lv_obj_set_style_bg_opa(progress_, LV_OPA_COVER, 0); + + lbl_time_ = lv_label_create(page); + lv_obj_set_pos(lbl_time_, 0, 96); + lv_obj_set_width(lbl_time_, 320); + lv_obj_set_style_text_font(lbl_time_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl_time_, rec_ui2::color(rec_ui2::kMuted), 0); + lv_obj_set_style_text_align(lbl_time_, LV_TEXT_ALIGN_CENTER, 0); } - // blink timer (500ms interval for red dot) - static void blink_timer_cb(lv_timer_t *t) + void refresh(const std::string &file, const std::string &time, float progress, bool paused) { - UIRecPage *self = static_cast(lv_timer_get_user_data(t)); - if (self) self->on_blink_tick(); + lv_label_set_text(lbl_file_, file.c_str()); + std::string time_text = paused ? time + " paused" : time; + lv_label_set_text(lbl_time_, time_text.c_str()); + + constexpr int bar_w = 5; + constexpr int gap = 2; + constexpr int start_x = 20; + constexpr int base_y = 82; + constexpr int max_h = 42; + for (int i = 0; i < rec_ui2::kWaveBarCount; ++i) + { + int h = 4 + ((i * 13 + 7) % max_h); + lv_obj_set_size(bars_[i], bar_w, h); + lv_obj_set_pos(bars_[i], start_x + i * (bar_w + gap), base_y - h); + } + + if (progress < 0.0f) + { + lv_obj_add_flag(progress_, LV_OBJ_FLAG_HIDDEN); + return; + } + + lv_obj_clear_flag(progress_, LV_OBJ_FLAG_HIDDEN); + progress = std::max(0.0f, std::min(1.0f, progress)); + int x = start_x + static_cast(progress * (rec_ui2::kWaveBarCount * (bar_w + gap) - gap)); + lv_obj_set_pos(progress_, x, 35); } - void on_blink_tick() +private: + lv_obj_t *lbl_file_ = nullptr; + std::array bars_{}; + lv_obj_t *progress_ = nullptr; + lv_obj_t *lbl_time_ = nullptr; +}; + +class RecorderAudioClient +{ +public: + using Callback = std::function; + + void set_status_callback(Callback callback) { - lv_obj_t *red_dot = ui_obj_["red_dot"]; - blink_visible_ = !blink_visible_; - if (blink_visible_) - lv_obj_clear_flag(red_dot, LV_OBJ_FLAG_HIDDEN); - else - lv_obj_add_flag(red_dot, LV_OBJ_FLAG_HIDDEN); + request({"SetCallback"}, std::move(callback), false); } - void start_blink_timer() + void clear_status_callback() { - blink_visible_ = true; - if (!blink_timer_) - blink_timer_ = lv_timer_create(blink_timer_cb, 500, this); - else - lv_timer_reset(blink_timer_); + request({"SetCallback"}, nullptr, false); } - void stop_blink_timer() + void set_waveform_enabled(bool enabled) { - if (blink_timer_) - { - lv_timer_delete(blink_timer_); - blink_timer_ = nullptr; - } - blink_visible_ = true; + cp0_signal_audio_setup({"set_waveform", enabled ? "on" : "off"}, nullptr); + } + + void start_capture(Callback callback) + { + request({"Cap"}, std::move(callback)); } - // ==================== recording actions ==================== - void start_recording() + void stop_capture(Callback callback = nullptr) { - if (state_ != RecState::IDLE) return; + request({"CapEnd"}, std::move(callback)); + } + + void save_capture_file(const std::string &path, Callback callback) + { + request({"CapFileSave", path}, std::move(callback)); + } - rec_counter_++; - char fname[64]; - snprintf(fname, sizeof(fname), "/tmp/rec_%03d.wav", rec_counter_); - current_file_ = fname; + void play_file(const std::string &path, Callback callback) + { + request({"PlayFile", path}, std::move(callback)); + } - char cmd[256]; - snprintf(cmd, sizeof(cmd), "arecord -f cd -t wav %s", fname); - active_pid_ = hal_process_spawn(cmd, 0); + void pause_playback() + { + request({"PlayPause"}); + } - state_ = RecState::RECORDING; - start_elapsed_timer(); - update_status_ui(); - update_file_label(); - printf("[Rec] Start recording: %s pid=%d\n", fname, active_pid_); + void continue_playback() + { + request({"PlayContinue"}); } - void stop_process() + void stop_playback() { - if (active_pid_ > 0) - { - hal_process_stop(active_pid_); - active_pid_ = -1; - } + request({"PlayEnd"}); } - void stop_action() +private: + void request(std::initializer_list args, + Callback callback = nullptr, + bool swallow_result = true) { - if (state_ == RecState::RECORDING) + if (!callback && swallow_result) + callback = [](int, std::string) {}; + cp0_signal_audio_api(std::list(args), std::move(callback)); + } +}; + +class RecorderFileStore +{ +public: + std::string recordings_dir() const + { + const char *home = getenv("HOME"); + std::string base = home ? std::string(home) : std::string("/tmp"); + std::string music = base + "/Music"; + std::string dir = music + "/Recorder"; + + struct stat st; + if (stat(music.c_str(), &st) != 0) + mkdir(music.c_str(), 0755); + if (stat(dir.c_str(), &st) != 0) + mkdir(dir.c_str(), 0755); + return dir; + } + + std::vector scan() const + { + std::vector recordings; + std::string dir = recordings_dir(); + DIR *d = opendir(dir.c_str()); + if (!d) + return recordings; + + struct dirent *entry = nullptr; + while ((entry = readdir(d)) != nullptr) { - stop_process(); - // add the recording to the list - recordings_.push_back(current_file_); - selected_idx_ = (int)recordings_.size() - 1; - build_rec_list(); - printf("[Rec] Stopped recording: %s\n", current_file_.c_str()); + size_t len = std::strlen(entry->d_name); + if (len >= 5 && std::strcmp(entry->d_name + len - 4, ".wav") == 0) + recordings.push_back(entry->d_name); } - else if (state_ == RecState::PLAYING) + closedir(d); + + std::sort(recordings.begin(), recordings.end(), std::greater()); + return recordings; + } + + std::string generate_filename() const + { + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; + localtime_r(&tt, &tm); + + std::ostringstream oss; + oss << "rec_" + << (tm.tm_year + 1900) + << std::setfill('0') << std::setw(2) << (tm.tm_mon + 1) + << std::setw(2) << tm.tm_mday << "_" + << std::setw(2) << tm.tm_hour + << std::setw(2) << tm.tm_min + << std::setw(2) << tm.tm_sec + << ".wav"; + return recordings_dir() + "/" + oss.str(); + } + + std::string make_serial_name(int serial) const + { + std::ostringstream oss; + oss << "rec_take_" << std::setfill('0') << std::setw(3) << serial << ".wav"; + return oss.str(); + } + + std::string normalized_wav_name(std::string name) const + { + while (!name.empty() && (name.front() == ' ' || name.front() == '/')) + name.erase(name.begin()); + if (name.empty()) + return ""; + if (name.size() < 4 || name.substr(name.size() - 4) != ".wav") + name += ".wav"; + return filename_only(name); + } + + static std::string filename_only(const std::string &path) + { + size_t slash = path.find_last_of('/'); + return slash == std::string::npos ? path : path.substr(slash + 1); + } +}; + +class UIRecPage : public rec_page +{ +public: + UIRecPage() + { + build_pages(); + scan_files(); + bind_audio_callback(); + bind_keyboard_shortcuts(); + switch_page(UiPage::Home); + refresh_view(); + } + + ~UIRecPage() + { + alive_->store(false); + stop_process(false); + audio_.clear_status_callback(); + if (elapsed_timer_) + lv_timer_delete(elapsed_timer_); + if (poll_timer_) + lv_timer_delete(poll_timer_); + } + +private: + enum class RecState + { + Idle, + Recording, + Playing, + }; + + enum class UiPage + { + Home, + SaveConfirm, + FileList, + Playback, + Count, + }; + + using AudioCallback = RecorderAudioClient::Callback; + + void build_pages() + { + lv_obj_clean(ui_APP_Container); + lv_obj_set_height(ui_APP_Container, rec_ui2::kContentH); + lv_obj_set_y(ui_APP_Container, 5); + lv_obj_clear_flag(ui_APP_Container, LV_OBJ_FLAG_SCROLLABLE); + + for (size_t i = 0; i < pages_.size(); ++i) { - stop_process(); - printf("[Rec] Stopped playback\n"); + pages_[i] = lv_obj_create(ui_APP_Container); + rec_ui2::prep_page(pages_[i]); + lv_obj_add_flag(pages_[i], LV_OBJ_FLAG_HIDDEN); } - state_ = RecState::IDLE; - stop_elapsed_timer(); - update_status_ui(); + + page_home_.create(pages_[static_cast(UiPage::Home)]); + page_save_.create(pages_[static_cast(UiPage::SaveConfirm)]); + page_files_.create(pages_[static_cast(UiPage::FileList)]); + page_playback_.create(pages_[static_cast(UiPage::Playback)]); + + poll_timer_ = lv_timer_create(&UIRecPage::poll_timer_cb, 200, this); } - void play_selected() + void bind_audio_callback() { - if (state_ != RecState::IDLE) return; - if (recordings_.empty()) return; + audio_.set_status_callback(make_audio_callback([this](int code, std::string data) { + on_audio_status(code, data); + })); + } - const std::string &file = recordings_[selected_idx_]; - char cmd[256]; - snprintf(cmd, sizeof(cmd), "aplay %s", file.c_str()); - active_pid_ = hal_process_spawn(cmd, 0); + AudioCallback make_audio_callback(AudioCallback callback) + { + auto alive = alive_; + return [alive, callback = std::move(callback)](int code, std::string data) mutable { + if (!alive->load() || !callback) + return; + callback(code, std::move(data)); + }; + } - current_file_ = file; - state_ = RecState::PLAYING; - start_elapsed_timer(); - update_status_ui(); - update_file_label(); - printf("[Rec] Playing: %s pid=%d\n", file.c_str(), active_pid_); + void bind_button(int index, std::function callback) + { + if (index < 0 || index >= rec_ui2::kBtnCount) + return; + button_actions_[index] = callback; + lvgl_add_call(but[index], [callback](lv_event_code_t c, void *event_param, void *user_data) { + (void)user_data; + if (!callback) + return; + if (c == LV_EVENT_CLICKED) + { + callback(); + return; + } + if (c == static_cast(LV_EVENT_KEYBOARD)) + { + struct key_item *key = static_cast(event_param); + if (key && key->key_state == 0) + callback(); + } + }, NULL); } - void delete_selected() + void bind_keyboard_shortcuts() { - if (recordings_.empty()) return; - if (state_ != RecState::IDLE) return; + lvgl_add_call(ui_root, [this](lv_event_code_t c, void *event_param, void *user_data) { + (void)user_data; + if (c != static_cast(LV_EVENT_KEYBOARD)) + return; + + struct key_item *key = static_cast(event_param); + if (!key || key->key_state != 0) + return; - const std::string &file = recordings_[selected_idx_]; - ::unlink(file.c_str()); - printf("[Rec] Deleted: %s\n", file.c_str()); + int index = -1; + switch (key->key_code) + { + case KEY_1: + index = 0; + break; + case KEY_2: + index = 1; + break; + case KEY_3: + index = 2; + break; + case KEY_4: + index = 3; + break; + case KEY_5: + index = 4; + break; + default: + break; + } - recordings_.erase(recordings_.begin() + selected_idx_); - if (selected_idx_ >= (int)recordings_.size() && selected_idx_ > 0) - selected_idx_--; - build_rec_list(); + if (index >= 0 && index < rec_ui2::kBtnCount && button_actions_[index]) + button_actions_[index](); + }, NULL); } - // ==================== event handling ==================== - void event_handler_init() + lv_obj_t *button_label(int index) { - lv_obj_add_event_cb(ui_root, UIRecPage::static_lvgl_handler, LV_EVENT_ALL, this); + if (index < 0 || index >= rec_ui2::kBtnCount) + return nullptr; + return but[index] ? lv_obj_get_child(but[index], 0) : nullptr; } - static void static_lvgl_handler(lv_event_t *e) + void set_button_icon(int index, ICON_t icon, lv_opa_t opa = LV_OPA_COVER) { - UIRecPage *self = static_cast(lv_event_get_user_data(e)); - if (self) self->event_handler(e); + lv_obj_t *label = button_label(index); + if (!label) + return; + lv_label_set_text(label, icon_text(icon)); + lv_obj_set_style_text_opa(label, opa, 0); } - void event_handler(lv_event_t *e) + void switch_page(UiPage page) { - if (IS_KEY_RELEASED(e)) + for (size_t i = 0; i < pages_.size(); ++i) { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); - handle_key(key); + if (!pages_[i]) + continue; + if (i == static_cast(page)) + lv_obj_clear_flag(pages_[i], LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(pages_[i], LV_OBJ_FLAG_HIDDEN); } + current_page_ = page; + bind_buttons_for_page(); + refresh_view(); } - void handle_key(uint32_t key) + void bind_buttons_for_page() { - int count = (int)recordings_.size(); - switch (key) + switch (current_page_) { - case KEY_R: - start_recording(); + case UiPage::Home: + set_button_icon(0, ICON_EXIT); + set_button_icon(1, ICON_STOP, state_ == RecState::Recording ? LV_OPA_COVER : LV_OPA_40); + set_button_icon(2, state_ == RecState::Recording ? ICON_SAMPLE_RATE : ICON_RECORD); + set_button_icon(3, ICON_SAMPLE_RATE, LV_OPA_40); + set_button_icon(4, ICON_LIST, state_ == RecState::Recording ? LV_OPA_40 : LV_OPA_COVER); + bind_button(0, [this]() { on_home_back(); }); + bind_button(1, [this]() { if (state_ == RecState::Recording) stop_recording_to_save(); }); + bind_button(2, [this]() { toggle_record(); }); + bind_button(3, []() {}); + bind_button(4, [this]() { if (state_ == RecState::Idle) switch_page(UiPage::FileList); }); break; - case KEY_S: - stop_action(); + case UiPage::SaveConfirm: + set_button_icon(0, ICON_EXIT); + set_button_icon(1, ICON_FAST_REWIND); + set_button_icon(2, ICON_RECORD); + set_button_icon(3, ICON_FAST_FORWARD); + set_button_icon(4, ICON_LIST); + bind_button(0, [this]() { discard_pending_recording(); }); + bind_button(1, [this]() { bump_save_name(-1); }); + bind_button(2, [this]() { reset_save_name(); }); + bind_button(3, [this]() { bump_save_name(1); }); + bind_button(4, [this]() { save_pending_recording(); }); break; - case KEY_P: - play_selected(); + case UiPage::FileList: + set_button_icon(0, ICON_EXIT); + set_button_icon(1, ICON_FAST_REWIND); + set_button_icon(2, ICON_PLAY); + set_button_icon(3, ICON_FAST_FORWARD); + set_button_icon(4, ICON_LIST); + bind_button(0, [this]() { switch_page(UiPage::Home); }); + bind_button(1, [this]() { prev_file(); }); + bind_button(2, [this]() { start_playback(); }); + bind_button(3, [this]() { next_file(); }); + bind_button(4, [this]() { switch_page(UiPage::Home); }); break; - case KEY_D: - delete_selected(); + case UiPage::Playback: + set_button_icon(0, ICON_EXIT); + set_button_icon(1, ICON_SPEED, LV_OPA_40); + set_button_icon(2, playback_paused_ ? ICON_PLAY : ICON_PAUSE); + set_button_icon(3, ICON_FAST_REWIND, LV_OPA_40); + set_button_icon(4, ICON_FAST_FORWARD, LV_OPA_40); + bind_button(0, [this]() { stop_playback(UiPage::FileList); }); + bind_button(1, []() {}); + bind_button(2, [this]() { toggle_play_pause(); }); + bind_button(3, []() {}); + bind_button(4, []() {}); break; - case KEY_UP: - if (count > 0 && selected_idx_ > 0) + case UiPage::Count: + break; + } + } + + void on_home_back() + { + if (state_ == RecState::Recording) + { + stop_recording_to_save(); + return; + } + if (go_back_home) + go_back_home(); + } + + void toggle_record() + { + if (state_ == RecState::Recording) + { + stop_recording_to_save(); + return; + } + + stop_process(false); + current_file_ = files_.generate_filename(); + pending_save_name_ = RecorderFileStore::filename_only(current_file_); + page_home_.reset_waveform(); + elapsed_sec_ = 0; + audio_.set_waveform_enabled(true); + audio_.start_capture(make_audio_callback([this](int code, std::string data) { + (void)data; + if (code != 0) { - selected_idx_--; - build_rec_list(); + state_ = RecState::Idle; + refresh_view(); + bind_buttons_for_page(); + return; } - break; - case KEY_DOWN: - if (count > 0 && selected_idx_ < count - 1) + state_ = RecState::Recording; + start_elapsed_timer(); + switch_page(UiPage::Home); + })); + } + + void stop_recording_to_save() + { + if (state_ != RecState::Recording || audio_requesting_) + return; + + audio_requesting_ = true; + audio_.set_waveform_enabled(false); + audio_.stop_capture(make_audio_callback([this](int code, std::string data) { + (void)data; + audio_requesting_ = false; + if (code != 0) + return; + state_ = RecState::Idle; + stop_elapsed_timer(); + page_save_.set_name(pending_save_name_); + switch_page(UiPage::SaveConfirm); + })); + } + + void discard_pending_recording() + { + std::remove("/tmp/rec.tmp.wav"); + current_file_.clear(); + pending_save_name_.clear(); + switch_page(UiPage::Home); + } + + void save_pending_recording() + { + std::string name = files_.normalized_wav_name(page_save_.name()); + if (name.empty()) + name = pending_save_name_; + + std::string path = files_.recordings_dir() + "/" + name; + current_file_ = path; + audio_.save_capture_file(path, make_audio_callback([this, name](int code, std::string data) { + (void)data; + if (code == 0) { - selected_idx_++; - build_rec_list(); + scan_files(); + select_file(name); + pending_save_name_.clear(); + switch_page(UiPage::FileList); } - break; - case KEY_ESC: - stop_process(); - if (state_ != RecState::IDLE) + })); + } + + void bump_save_name(int delta) + { + save_name_serial_ = std::max(0, save_name_serial_ + delta); + page_save_.set_name(files_.make_serial_name(save_name_serial_)); + } + + void reset_save_name() + { + page_save_.set_name(pending_save_name_); + } + + void start_playback() + { + if (recordings_.empty()) + return; + + stop_process(false); + current_file_ = files_.recordings_dir() + "/" + recordings_[selected_idx_]; + playback_paused_ = false; + playback_finished_.store(false); + elapsed_sec_ = 0; + audio_.play_file(current_file_, make_audio_callback([this](int code, std::string data) { + (void)data; + if (code != 0) + return; + state_ = RecState::Playing; + start_elapsed_timer(); + switch_page(UiPage::Playback); + })); + } + + void toggle_play_pause() + { + if (state_ != RecState::Playing) + return; + if (playback_paused_) + { + audio_.continue_playback(); + playback_paused_ = false; + start_elapsed_timer(); + } + else + { + audio_.pause_playback(); + playback_paused_ = true; + stop_elapsed_timer(); + } + bind_buttons_for_page(); + refresh_view(); + } + + void stop_playback(UiPage next) + { + if (state_ == RecState::Playing) + audio_.stop_playback(); + state_ = RecState::Idle; + playback_paused_ = false; + stop_elapsed_timer(); + scan_files(); + switch_page(next); + } + + void stop_process(bool refresh) + { + if (state_ == RecState::Recording) + { + audio_.set_waveform_enabled(false); + audio_.stop_capture(); + } + else if (state_ == RecState::Playing) + { + audio_.stop_playback(); + } + + state_ = RecState::Idle; + playback_paused_ = false; + stop_elapsed_timer(); + if (refresh) + refresh_view(); + } + + void prev_file() + { + if (recordings_.empty()) + return; + selected_idx_ = selected_idx_ > 0 ? selected_idx_ - 1 : static_cast(recordings_.size()) - 1; + refresh_view(); + } + + void next_file() + { + if (recordings_.empty()) + return; + selected_idx_ = selected_idx_ < static_cast(recordings_.size()) - 1 ? selected_idx_ + 1 : 0; + refresh_view(); + } + + void refresh_view() + { + page_home_.refresh(state_ == RecState::Recording, + state_ == RecState::Recording ? RecorderFileStore::filename_only(current_file_) : current_home_file(), + format_time(elapsed_sec_), + sample_rate_text()); + page_files_.refresh(recordings_, selected_idx_); + float progress = -1.0f; + page_playback_.refresh(RecorderFileStore::filename_only(current_file_), format_time(elapsed_sec_), progress, playback_paused_); + } + + void on_audio_status(int code, const std::string &data) + { + if (code == 0 && data.find("play over") != std::string::npos) + { + playback_finished_.store(true); + return; + } + if (code == 1 && data.size() >= page_home_.waveform_byte_size()) + page_home_.set_waveform(data); + } + + static void elapsed_timer_cb(lv_timer_t *t) + { + auto *self = static_cast(lv_timer_get_user_data(t)); + if (!self) + return; + self->elapsed_sec_++; + self->refresh_view(); + } + + static void poll_timer_cb(lv_timer_t *t) + { + auto *self = static_cast(lv_timer_get_user_data(t)); + if (!self) + return; + if (self->playback_finished_.exchange(false) && self->state_ == RecState::Playing) + { + self->stop_playback(UiPage::FileList); + return; + } + if (self->current_page_ == UiPage::Home || self->current_page_ == UiPage::Playback) + self->refresh_view(); + } + + void start_elapsed_timer() + { + stop_elapsed_timer(); + elapsed_timer_ = lv_timer_create(&UIRecPage::elapsed_timer_cb, 1000, this); + } + + void stop_elapsed_timer() + { + if (elapsed_timer_) + { + lv_timer_delete(elapsed_timer_); + elapsed_timer_ = nullptr; + } + } + + std::string current_home_file() const + { + if (!recordings_.empty() && selected_idx_ >= 0 && selected_idx_ < static_cast(recordings_.size())) + return recordings_[selected_idx_]; + return ""; + } + + std::string sample_rate_text() const + { + return "48kHz"; + } + + std::string format_time(int seconds) const + { + char buf[16]; + snprintf(buf, sizeof(buf), "%02d:%02d", seconds / 60, seconds % 60); + return buf; + } + + void scan_files() + { + recordings_ = files_.scan(); + if (selected_idx_ >= static_cast(recordings_.size())) + selected_idx_ = recordings_.empty() ? 0 : static_cast(recordings_.size()) - 1; + } + + void select_file(const std::string &name) + { + for (size_t i = 0; i < recordings_.size(); ++i) + { + if (recordings_[i] == name) { - state_ = RecState::IDLE; - stop_elapsed_timer(); - update_status_ui(); + selected_idx_ = static_cast(i); + return; } - if (go_back_home) go_back_home(); - break; - default: - break; } } + + std::array(UiPage::Count)> pages_{}; + RecorderAudioClient audio_; + RecorderFileStore files_; + std::array, rec_ui2::kBtnCount> button_actions_{}; + RecHomeView page_home_; + RecSaveConfirmView page_save_; + RecFileListView page_files_; + RecPlaybackView page_playback_; + + UiPage current_page_ = UiPage::Home; + RecState state_ = RecState::Idle; + std::vector recordings_; + int selected_idx_ = 0; + std::string current_file_; + std::string pending_save_name_; + int save_name_serial_ = 1; + int elapsed_sec_ = 0; + bool playback_paused_ = false; + bool audio_requesting_ = false; + lv_timer_t *elapsed_timer_ = nullptr; + lv_timer_t *poll_timer_ = nullptr; + std::atomic playback_finished_{false}; + std::shared_ptr> alive_ = std::make_shared>(true); }; diff --git a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp index 38c76d2d..69edd36c 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp +++ b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp @@ -196,6 +196,10 @@ class app_launch_S img_path("math_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); + app_list.emplace_back("Compass", + img_path("compass_needle_80.png"), page_v); + + #if defined(__linux__) && !defined(HAL_PLATFORM_SDL) if (APP_ENABLED("IP_Panel")) app_list.emplace_back("IP_PANEL", @@ -224,8 +228,8 @@ class app_launch_S app_list.emplace_back("REC", img_path("rec_100.png"), page_v); if (APP_ENABLED("Camera")) - app_list.emplace_back("CAMERA", - img_path("camera_100.png"), page_v); + // app_list.emplace_back("CAMERA", + // img_path("camera_100.png"), page_v); if (APP_ENABLED("UnitEnv")) app_list.emplace_back("UnitEnv", img_path("unitenv_100.png"), page_v); diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 6fae11d6..b59c627f 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -66,6 +66,7 @@ class app_ { key_group = lv_group_create(); lv_group_add_obj(key_group, ui_root); + // lv_group_focus_obj(ui_root); } // static void static_event_handler(lv_event_t * e) diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index e644ac48..fad5896a 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -64,6 +64,7 @@ void ui_info_bind(); #define IS_KEY_RELEASED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD)&&(LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) extern volatile uint32_t LV_EVENT_BATTERY; +extern volatile uint32_t LV_EVENT_DELL_CPP_DATA; typedef struct { hal_battery_info_t info; diff --git a/projects/APPLaunch/main/ui/ui_events.c b/projects/APPLaunch/main/ui/ui_events.c index 7c5e5541..9354da35 100644 --- a/projects/APPLaunch/main/ui/ui_events.c +++ b/projects/APPLaunch/main/ui/ui_events.c @@ -10,7 +10,7 @@ #include "compat/input_keys.h" -#define MINIAUDIO_IMPLEMENTATION +// #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" #include "thpool.h" diff --git a/projects/APPLaunch/setup.ini b/projects/APPLaunch/setup.ini index 54d7a470..5d0cae7b 100644 --- a/projects/APPLaunch/setup.ini +++ b/projects/APPLaunch/setup.ini @@ -1,9 +1,9 @@ [ssh] local_file_path = dist remote_file_path = /home/pi/dist -remote_host = 192.168.50.150 +remote_host = 192.168.28.177 remote_port = 22 username = pi password = pi ; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' -after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +; after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' From 2821b5bd83e99eb262234dacbae6f98374b2d2dc Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 10:24:42 +0800 Subject: [PATCH 07/70] feat: rework camera app preview controls Replace the page-local libcamera implementation with a camera API client that consumes CP0 frame callbacks, renders RGB565 previews in LVGL, and separates hardware access from the APPLaunch camera UI. Extend the CP0 camera service with frame streaming callbacks, RGB565 frame payloads, ScalerCrop-based zoom and pan commands, and per-request crop application while preserving JPEG capture support. Add SDL camera init stubs so the component link path remains consistent across hardware and SDL builds, and wire init_camera into the SDL LVGL startup path. Redesign the camera page around preview, gallery, detail, and dialog flows with keyboard/button actions for capture, zoom, pan, save feedback, file browsing, and delete confirmation. --- .../cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp | 147 +- .../cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp | 10 + ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c | 1 + ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h | 1 + .../ui/components/page_app/ui_app_camera.hpp | 1673 +++++++++-------- 5 files changed, 1040 insertions(+), 792 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp index 54d3b068..9365ae36 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -79,6 +80,10 @@ class CameraSystem map_fun(Photo), map_fun(Status), map_fun(SetCallback), + map_fun(SetFrameCallback), + map_fun(ZoomIn), + map_fun(ZoomOut), + map_fun(Pan), }; #undef map_fun @@ -96,6 +101,7 @@ class CameraSystem private: callback_t status_callback_; + callback_t frame_callback_; std::mutex mutex_; #if CP0_CAMERA_HAS_LIBCAMERA @@ -119,6 +125,10 @@ class CameraSystem int stream_h_ = 150; int stream_stride_ = 320 * 2; libcamera::PixelFormat stream_format_ = libcamera::formats::RGB565; + libcamera::Rectangle scaler_crop_max_{}; + int zoom_percent_ = 100; + int view_x_percent_ = 50; + int view_y_percent_ = 50; std::atomic capture_requested_{false}; std::string pending_capture_path_; @@ -159,6 +169,13 @@ class CameraSystem report(callback, 0, "camera callback set\n"); } + void SetFrameCallback(arg_t arg, callback_t callback) + { + (void)arg; + frame_callback_ = callback; + report(callback, 0, "camera frame callback set\n"); + } + void Open(arg_t arg, callback_t callback) { const int width = to_int(nth_arg(arg, 1), 320); @@ -215,6 +232,51 @@ class CameraSystem #endif } + void ZoomIn(arg_t arg, callback_t callback) + { + (void)arg; +#if CP0_CAMERA_HAS_LIBCAMERA + std::lock_guard lock(mutex_); + zoom_percent_ = zoom_percent_ < 250 ? 250 : 500; + report(callback, 0, zoom_status_text_locked()); +#else + report(callback, -10, "camera unavailable: libcamera/jpeg headers not found\n"); +#endif + } + + void ZoomOut(arg_t arg, callback_t callback) + { + (void)arg; +#if CP0_CAMERA_HAS_LIBCAMERA + std::lock_guard lock(mutex_); + zoom_percent_ = zoom_percent_ > 250 ? 250 : 100; + if (zoom_percent_ == 100) { + view_x_percent_ = 50; + view_y_percent_ = 50; + } + report(callback, 0, zoom_status_text_locked()); +#else + report(callback, -10, "camera unavailable: libcamera/jpeg headers not found\n"); +#endif + } + + void Pan(arg_t arg, callback_t callback) + { +#if CP0_CAMERA_HAS_LIBCAMERA + const int dx = to_int(nth_arg(arg, 1), 0); + const int dy = to_int(nth_arg(arg, 2), 0); + std::lock_guard lock(mutex_); + if (zoom_percent_ > 100) { + view_x_percent_ = std::max(0, std::min(100, view_x_percent_ + dx * 8)); + view_y_percent_ = std::max(0, std::min(100, view_y_percent_ + dy * 8)); + } + report(callback, 0, zoom_status_text_locked()); +#else + (void)arg; + report(callback, -10, "camera unavailable: libcamera/jpeg headers not found\n"); +#endif + } + #if CP0_CAMERA_HAS_LIBCAMERA static std::string lower_string(std::string s) { @@ -273,6 +335,64 @@ class CameraSystem b |= b >> 5; } + static uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) + { + return static_cast(((r & 0xF8) << 8) | + ((g & 0xFC) << 3) | + (b >> 3)); + } + + std::string zoom_status_text_locked() const + { + char buf[96]; + std::snprintf(buf, sizeof(buf), "ZOOM %d %d %d\n", zoom_percent_, view_x_percent_, view_y_percent_); + return std::string(buf); + } + + libcamera::Rectangle crop_rect_locked() const + { + if (zoom_percent_ <= 100 || scaler_crop_max_.width <= 0 || scaler_crop_max_.height <= 0) + return scaler_crop_max_; + + const int max_width = static_cast(scaler_crop_max_.width); + const int max_height = static_cast(scaler_crop_max_.height); + int crop_w = std::max(1, max_width * 100 / zoom_percent_); + int crop_h = std::max(1, max_height * 100 / zoom_percent_); + int max_x = std::max(0, max_width - crop_w); + int max_y = std::max(0, max_height - crop_h); + int x = scaler_crop_max_.x + max_x * std::max(0, std::min(100, view_x_percent_)) / 100; + int y = scaler_crop_max_.y + max_y * std::max(0, std::min(100, view_y_percent_)) / 100; + return libcamera::Rectangle(x, y, crop_w, crop_h); + } + + void apply_crop_locked(libcamera::Request *request) const + { + if (!request || scaler_crop_max_.width <= 0 || scaler_crop_max_.height <= 0) + return; + request->controls().set(libcamera::controls::ScalerCrop, crop_rect_locked()); + } + + bool convert_to_rgb565(const uint8_t *src, size_t bytes_used, std::vector &rgb565) + { + std::vector rgb; + if (!convert_to_rgb888(src, bytes_used, rgb)) + return false; + rgb565.assign(stream_w_ * stream_h_, 0); + for (int i = 0; i < stream_w_ * stream_h_; ++i) + rgb565[i] = rgb888_to_rgb565(rgb[i * 3 + 0], rgb[i * 3 + 1], rgb[i * 3 + 2]); + return true; + } + + std::string make_frame_payload(const std::vector &rgb565, int width, int height) + { + std::string payload; + char header[64]; + const int header_len = std::snprintf(header, sizeof(header), "FRAME %d %d RGB565\n", width, height); + payload.assign(header, header_len); + payload.append(reinterpret_cast(rgb565.data()), rgb565.size() * sizeof(uint16_t)); + return payload; + } + bool convert_to_rgb888(const uint8_t *src, size_t bytes_used, std::vector &rgb) { const bool is_rgb888 = stream_format_ == libcamera::formats::RGB888; @@ -447,6 +567,8 @@ class CameraSystem stream_h_ = cfg.size.height; stream_stride_ = cfg.stride; stream_format_ = cfg.pixelFormat; + const auto crop_max = camera_->properties().get(libcamera::properties::ScalerCropMaximum); + scaler_crop_max_ = crop_max ? *crop_max : libcamera::Rectangle(0, 0, stream_w_, stream_h_); allocator_ = std::make_unique(camera_); if (allocator_->allocate(stream_) < 0) @@ -490,7 +612,10 @@ class CameraSystem } for (std::unique_ptr &request : requests_) + { + apply_crop_locked(request.get()); camera_->queueRequest(request.get()); + } streaming_ = true; return 0; @@ -525,12 +650,15 @@ class CameraSystem std::string save_path; callback_t callback; + callback_t frame_callback; std::vector rgb; + std::vector frame_rgb565; int save_w = 0; int save_h = 0; + int frame_w = 0; + int frame_h = 0; bool should_capture = capture_requested_.exchange(false); - if (should_capture) { std::lock_guard lock(mutex_); auto map_it = mapped_buffers_.find(buffer); @@ -543,19 +671,26 @@ class CameraSystem bytes_used = std::min(bytes_used, static_cast(metadata.planes()[0].bytesused)); dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); - bool converted = convert_to_rgb888(src, bytes_used, rgb); - dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); - - if (converted) + if (frame_callback_ && convert_to_rgb565(src, bytes_used, frame_rgb565)) + { + frame_w = stream_w_; + frame_h = stream_h_; + frame_callback = frame_callback_; + } + if (should_capture && convert_to_rgb888(src, bytes_used, rgb)) { save_path = pending_capture_path_; callback = pending_capture_callback_; save_w = stream_w_; save_h = stream_h_; } + dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); } } + if (frame_callback && !frame_rgb565.empty()) + frame_callback(0, make_frame_payload(frame_rgb565, frame_w, frame_h)); + if (!save_path.empty()) { bool ok = save_jpeg_rgb888(save_path, rgb.data(), save_w, save_h, 90); @@ -567,6 +702,7 @@ class CameraSystem if (camera_ && streaming_) { request->reuse(libcamera::Request::ReuseBuffers); + apply_crop_locked(request); camera_->queueRequest(request); } } @@ -577,6 +713,7 @@ class CameraSystem const bool was_streaming = streaming_; streaming_ = false; capture_requested_.store(false); + frame_callback_ = nullptr; if (camera_) { diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp new file mode 100644 index 00000000..fd6035ba --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp @@ -0,0 +1,10 @@ +#include "hal_lvgl_bsp.h" + + +extern "C" void init_camera(void) +{ + // std::shared_ptr camera = std::make_shared(); + + // cp0_signal_camera_api.append([camera](std::list arg, std::function callback) + // { camera->api_call(arg, callback); }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c index 12b500e0..c4720619 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c @@ -15,4 +15,5 @@ void cp0_lvgl_init(void) init_sdl_disp(); init_sdl_input(); init_audio(); + init_camera(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h index a4882740..adee8709 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h @@ -7,6 +7,7 @@ extern "C" void init_sdl_disp(); void init_sdl_input(); void init_audio(); +void init_camera(void); #ifdef __cplusplus } #endif diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp index 3eb06204..cb0f2e74 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp @@ -1,985 +1,1084 @@ #pragma once -#if !defined(HAL_PLATFORM_SDL) #include "../ui_app_page.hpp" -#include -#include -#include -#include -#include +#include #include +#include +#include #include #include #include #include -#include -#include - +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include #include - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include #include "compat/input_keys.h" +#include "hal_lvgl_bsp.h" -// ============================================================ -// Raspberry Pi MIPI IMX219 Camera Page -// -// - Direct libcamera C++ API video stream -// - LVGL fullscreen preview -// - ENTER: capture current stream frame -// - Save JPEG to /home/pi/Pictures -// - chmod 666 after save -// ============================================================ - -class UICameraPage : public app_base +namespace camera_app { -public: - UICameraPage() : app_base() - { - set_page_title("CAMERA"); +static constexpr int kScreenW = 320; +static constexpr int kContentH = 150; +static constexpr int kPreviewW = 226; +static constexpr int kPreviewH = 150; +static constexpr int kBottomH = 25; +static constexpr uint32_t kBlack = 0x000000; +static constexpr uint32_t kText = 0xFFFFFF; +static constexpr uint32_t kMuted = 0xCAC4CF; +static constexpr uint32_t kPanel = 0x1C1B1E; +static constexpr uint32_t kPanelHigh = 0x2B292D; +static constexpr uint32_t kOutline = 0x49454E; +static constexpr uint32_t kPrimary = 0xCFBCFF; +static constexpr uint32_t kDanger = 0xFFB4AB; + +static inline lv_color_t color(uint32_t hex) +{ + return lv_color_hex(hex); +} - ensure_picture_dir(); +static inline void clear_obj(lv_obj_t *obj) +{ + lv_obj_remove_style_all(obj); + lv_obj_clear_flag(obj, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); +} - create_UI(); - event_handler_init(); +static inline std::string trim_line(std::string value) +{ + while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) + value.pop_back(); + return value; +} - open_imx219_camera(); +static inline std::string home_dir() +{ + const char *home = std::getenv("HOME"); + return home && home[0] ? std::string(home) : std::string("/home/pi"); +} - frame_timer_ = lv_timer_create(frame_timer_cb, 33, this); - } +static inline void ensure_dir(const std::string &dir) +{ + std::string current; + if (!dir.empty() && dir[0] == '/') + current = "/"; - ~UICameraPage() + size_t start = current == "/" ? 1 : 0; + while (start <= dir.size()) { - if (frame_timer_) + size_t slash = dir.find('/', start); + std::string part = dir.substr(start, slash == std::string::npos ? std::string::npos : slash - start); + if (!part.empty()) { - lv_timer_delete(frame_timer_); - frame_timer_ = nullptr; + if (current.size() > 1) + current += "/"; + current += part; + struct stat st; + if (stat(current.c_str(), &st) != 0) + mkdir(current.c_str(), 0777); + chmod(current.c_str(), 0777); } - - close_camera(); + if (slash == std::string::npos) + break; + start = slash + 1; } +} -private: - // ==================== constants ==================== - - static constexpr int PREVIEW_W = 320; - static constexpr int PREVIEW_H = 150; - - // ==================== LVGL ==================== - - std::unordered_map ui_obj_; - - lv_image_dsc_t img_dsc_{}; - lv_image_dsc_t snapshot_img_dsc_{}; - - std::vector display_buf_; - std::vector pending_lv_buf_; - std::vector snapshot_lv_buf_; - - std::vector pending_rgb_buf_; - std::vector snapshot_rgb_buf_; +static inline std::string pictures_dir() +{ + std::string dir = home_dir() + "/Pictures/DCIM/Camera"; + ensure_dir(dir); + return dir; +} - std::mutex frame_mutex_; +static inline std::string filename_only(const std::string &path) +{ + size_t slash = path.find_last_of('/'); + return slash == std::string::npos ? path : path.substr(slash + 1); +} - bool new_frame_ = false; - bool snapshot_ready_ = false; - bool snapshot_review_active_ = false; - uint32_t snapshot_review_until_ = 0; +static inline bool is_image_name(std::string name) +{ + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return (name.size() > 4 && name.substr(name.size() - 4) == ".jpg") || + (name.size() > 5 && name.substr(name.size() - 5) == ".jpeg") || + (name.size() > 4 && name.substr(name.size() - 4) == ".png"); +} - lv_timer_t *frame_timer_ = nullptr; +static inline std::string make_photo_path() +{ + std::time_t now = std::time(nullptr); + std::tm tm_now{}; + localtime_r(&now, &tm_now); + char time_buf[64]; + std::strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", &tm_now); + return pictures_dir() + "/CAM_" + time_buf + ".jpg"; +} + +static inline std::string format_file_time(const std::string &path) +{ + struct stat st; + if (stat(path.c_str(), &st) != 0) + return "Unknown"; + std::tm tm_now{}; + localtime_r(&st.st_mtime, &tm_now); + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_now); + return buf; +} + +struct Frame +{ + int width = 0; + int height = 0; + std::vector rgb565; +}; - // ==================== camera ==================== +struct ZoomState +{ + int zoom = 100; + int view_x = 50; + int view_y = 50; +}; - std::unique_ptr cm_; - std::shared_ptr camera_; - std::unique_ptr config_; - std::unique_ptr allocator_; +class HardwareCameraClient +{ +public: + using Callback = std::function; - libcamera::Stream *stream_ = nullptr; + void set_status_callback(Callback cb) + { + request({"SetCallback"}, std::move(cb), false); + } - std::vector> requests_; + void set_frame_callback(Callback cb) + { + request({"SetFrameCallback"}, std::move(cb), false); + } - struct MappedBuffer + void start(int w, int h, Callback cb) { - void *addr = nullptr; - size_t size = 0; - int fd = -1; - }; + request({"Start", std::to_string(w), std::to_string(h)}, std::move(cb)); + } - std::unordered_map mapped_buffers_; + void stop() + { + request({"Stop"}); + } - bool camera_found_ = false; - bool streaming_ = false; + void capture(const std::string &path, int w, int h, Callback cb) + { + request({"Capture", path, std::to_string(w), std::to_string(h)}, std::move(cb)); + } - int stream_w_ = PREVIEW_W; - int stream_h_ = PREVIEW_H; - int stream_stride_ = PREVIEW_W * 2; - libcamera::PixelFormat stream_format_ = libcamera::formats::RGB565; + void zoom_in(Callback cb) + { + request({"ZoomIn"}, std::move(cb)); + } - std::atomic capture_requested_{false}; + void zoom_out(Callback cb) + { + request({"ZoomOut"}, std::move(cb)); + } - int capture_counter_ = 0; - std::string last_file_; + void pan(int dx, int dy, Callback cb) + { + request({"Pan", std::to_string(dx), std::to_string(dy)}, std::move(cb)); + } private: - // ============================================================ - // UI - // ============================================================ - - void create_UI() - { - lv_obj_t *bg = lv_obj_create(ui_APP_Container); - lv_obj_set_size(bg, 320, 150); - lv_obj_set_pos(bg, 0, 0); - lv_obj_set_style_radius(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(bg, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(bg, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["bg"] = bg; - - init_image_buffer(PREVIEW_W, PREVIEW_H); - - lv_obj_t *img = lv_img_create(bg); - lv_obj_set_pos(img, 0, 0); - lv_img_set_src(img, &img_dsc_); - ui_obj_["img"] = img; - - // // top overlay - // lv_obj_t *top = lv_obj_create(bg); - // lv_obj_set_size(top, 320, 24); - // lv_obj_set_pos(top, 0, 0); - // lv_obj_set_style_radius(top, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_set_style_bg_color(top, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_set_style_bg_opa(top, 150, LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_set_style_border_width(top, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_set_style_pad_all(top, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_clear_flag(top, LV_OBJ_FLAG_SCROLLABLE); - - // lv_obj_t *lbl_title = lv_label_create(top); - // lv_label_set_text(lbl_title, "IMX219 Stream"); - // lv_obj_set_pos(lbl_title, 6, 4); - // lv_obj_set_style_text_color(lbl_title, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - - // lv_obj_t *lbl_status = lv_label_create(top); - // lv_label_set_text(lbl_status, "Opening..."); - // lv_obj_set_pos(lbl_status, 220, 4); - // lv_obj_set_style_text_color(lbl_status, lv_color_hex(0xF1C40F), LV_PART_MAIN | LV_STATE_DEFAULT); - // lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - // ui_obj_["lbl_status"] = lbl_status; - - lv_obj_t *photo_frame = lv_obj_create(bg); - lv_obj_set_size(photo_frame, 186, 118); - lv_obj_center(photo_frame); - lv_obj_set_style_radius(photo_frame, 4, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(photo_frame, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(photo_frame, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(photo_frame, lv_color_hex(0xF7F7F7), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(photo_frame, 3, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_width(photo_frame, 12, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(photo_frame, 120, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_transform_rotation(photo_frame, -60, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(photo_frame, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(photo_frame, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(photo_frame, LV_OBJ_FLAG_HIDDEN); - ui_obj_["photo_frame"] = photo_frame; - - lv_obj_t *photo_img = lv_img_create(photo_frame); - lv_obj_set_size(photo_img, 174, 78); - lv_obj_align(photo_img, LV_ALIGN_TOP_MID, 0, 0); - lv_image_set_inner_align(photo_img, LV_IMAGE_ALIGN_COVER); - lv_img_set_src(photo_img, &snapshot_img_dsc_); - ui_obj_["photo_img"] = photo_img; - - lv_obj_t *lbl_save_info = lv_label_create(photo_frame); - lv_label_set_text(lbl_save_info, ""); - lv_obj_set_width(lbl_save_info, 174); - lv_obj_align(lbl_save_info, LV_ALIGN_BOTTOM_MID, 0, 0); - lv_label_set_long_mode(lbl_save_info, LV_LABEL_LONG_WRAP); - lv_obj_set_style_text_color(lbl_save_info, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_save_info, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["lbl_info"] = lbl_save_info; - } - - void init_image_buffer(int w, int h) + void request(std::initializer_list args, Callback cb = nullptr, bool swallow = true) { - std::lock_guard lock(frame_mutex_); - - stream_w_ = w; - stream_h_ = h; - - display_buf_.assign(w * h, 0); - pending_lv_buf_.assign(w * h, 0); - snapshot_lv_buf_.assign(w * h, 0); - pending_rgb_buf_.assign(w * h * 3, 0); - - memset(&img_dsc_, 0, sizeof(img_dsc_)); - - // LVGL 9.x image descriptor - img_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC; - img_dsc_.header.cf = LV_COLOR_FORMAT_RGB565; - img_dsc_.header.flags = 0; - img_dsc_.header.w = w; - img_dsc_.header.h = h; - img_dsc_.header.stride = w * sizeof(uint16_t); - - img_dsc_.data_size = display_buf_.size() * sizeof(uint16_t); - img_dsc_.data = reinterpret_cast(display_buf_.data()); - - snapshot_img_dsc_ = img_dsc_; - snapshot_img_dsc_.data_size = snapshot_lv_buf_.size() * sizeof(uint16_t); - snapshot_img_dsc_.data = reinterpret_cast(snapshot_lv_buf_.data()); + if (!cb && swallow) + cb = [](int, std::string) {}; + cp0_signal_camera_api(std::list(args), std::move(cb)); } +}; - // ============================================================ - // directory / filename / jpeg - // ============================================================ - - void ensure_picture_dir() +class GalleryStore +{ +public: + void refresh() { - const char *dir = "/home/pi/Pictures"; + items_.clear(); + DIR *dir = opendir(pictures_dir().c_str()); + if (!dir) + return; - struct stat st; - if (stat(dir, &st) != 0) + struct dirent *entry = nullptr; + while ((entry = readdir(dir)) != nullptr) { - mkdir(dir, 0777); + if (entry->d_name[0] == '.') + continue; + std::string name = entry->d_name; + if (is_image_name(name)) + items_.push_back(pictures_dir() + "/" + name); } - - chmod(dir, 0777); + closedir(dir); + std::sort(items_.begin(), items_.end()); + if (index_ >= static_cast(items_.size())) + index_ = items_.empty() ? 0 : static_cast(items_.size()) - 1; } - std::string make_photo_path() + bool empty() const { return items_.empty(); } + int count() const { return static_cast(items_.size()); } + int index() const { return items_.empty() ? 0 : index_ + 1; } + const std::string ¤t() const { - ensure_picture_dir(); - - time_t now = time(nullptr); - - struct tm tm_now; - localtime_r(&now, &tm_now); - - char time_buf[64]; - strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", &tm_now); + static const std::string empty_path; + return items_.empty() ? empty_path : items_[index_]; + } - char path[256]; - snprintf(path, - sizeof(path), - "/home/pi/Pictures/IMX219_%s_%03d.jpg", - time_buf, - ++capture_counter_); + void prev() + { + if (items_.empty()) + return; + index_ = index_ > 0 ? index_ - 1 : static_cast(items_.size()) - 1; + } - return std::string(path); + void next() + { + if (items_.empty()) + return; + index_ = index_ < static_cast(items_.size()) - 1 ? index_ + 1 : 0; } - bool save_jpeg_rgb888(const std::string &path, - const uint8_t *rgb, - int width, - int height, - int quality = 90) + bool delete_current() { - FILE *fp = fopen(path.c_str(), "wb"); - if (!fp) - { - printf("[Camera] Failed to open jpeg file: %s\n", path.c_str()); + if (items_.empty()) return false; - } - - jpeg_compress_struct cinfo; - jpeg_error_mgr jerr; + std::string path = items_[index_]; + if (std::remove(path.c_str()) != 0) + return false; + refresh(); + return true; + } - cinfo.err = jpeg_std_error(&jerr); +private: + std::vector items_; + int index_ = 0; +}; +} // namespace camera_app - jpeg_create_compress(&cinfo); - jpeg_stdio_dest(&cinfo, fp); +class UICameraPage : public app_base +{ +public: + UICameraPage() : app_base() + { + app_name = "CAMERA"; + set_page_title(app_name); + build_ui(); + bind_keyboard(); + start_camera(); + ui_timer_ = lv_timer_create(&UICameraPage::ui_timer_cb, 33, this); + } - cinfo.image_width = width; - cinfo.image_height = height; - cinfo.input_components = 3; - cinfo.in_color_space = JCS_RGB; + ~UICameraPage() + { + alive_->store(false); + camera_.set_frame_callback(nullptr); + camera_.set_status_callback(nullptr); + camera_.stop(); + if (ui_timer_) + lv_timer_delete(ui_timer_); + } - jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); +private: + enum class Page + { + Camera, + Gallery, + DeleteConfirm, + Info, + }; - jpeg_start_compress(&cinfo, TRUE); + struct LvglCall + { + void *data = nullptr; + std::function cb; + }; - while (cinfo.next_scanline < cinfo.image_height) + static void lvgl_event_handler(lv_event_t *e) + { + LvglCall *call = static_cast(lv_event_get_user_data(e)); + if (!call) + return; + if (lv_event_get_code(e) == LV_EVENT_DELETE) { - JSAMPROW row_pointer[1]; - row_pointer[0] = const_cast( - &rgb[cinfo.next_scanline * width * 3]); - - jpeg_write_scanlines(&cinfo, row_pointer, 1); + delete call; + return; } + if (call->cb) + call->cb(lv_event_get_code(e), lv_event_get_param(e), call->data); + } - jpeg_finish_compress(&cinfo); - jpeg_destroy_compress(&cinfo); - - fclose(fp); + static void ui_timer_cb(lv_timer_t *timer) + { + UICameraPage *self = static_cast(lv_timer_get_user_data(timer)); + if (self) + self->poll_ui(); + } - chmod(path.c_str(), 0666); + void lvgl_add_call(lv_obj_t *obj, std::function cb, void *data = nullptr) + { + if (!obj || !cb) + return; + LvglCall *call = new LvglCall; + call->data = data; + call->cb = std::move(cb); + lv_obj_add_event_cb(obj, &UICameraPage::lvgl_event_handler, LV_EVENT_ALL, call); + } - return true; + void build_ui() + { + lv_obj_clean(ui_APP_Container); + lv_obj_set_height(ui_APP_Container, camera_app::kContentH); + lv_obj_set_y(ui_APP_Container, 10); + lv_obj_clear_flag(ui_APP_Container, LV_OBJ_FLAG_SCROLLABLE); + + page_camera_ = make_page(); + page_gallery_ = make_page(); + build_camera_page(); + build_gallery_page(); + build_bottom_bar(); + build_delete_dialog(); + build_info_panel(); + show_page(Page::Camera); } - // ============================================================ - // libcamera open / close - // ============================================================ + lv_obj_t *make_page() + { + lv_obj_t *page = lv_obj_create(ui_APP_Container); + camera_app::clear_obj(page); + lv_obj_set_size(page, camera_app::kScreenW, camera_app::kContentH); + lv_obj_set_pos(page, 0, 0); + lv_obj_set_style_bg_color(page, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(page, LV_OPA_COVER, 0); + lv_obj_add_flag(page, LV_OBJ_FLAG_HIDDEN); + return page; + } - static std::string lower_string(std::string s) + void build_camera_page() { - std::transform(s.begin(), s.end(), s.begin(), - [](unsigned char c) { return std::tolower(c); }); - return s; + preview_box_ = lv_obj_create(page_camera_); + camera_app::clear_obj(preview_box_); + lv_obj_set_size(preview_box_, camera_app::kPreviewW, camera_app::kPreviewH); + lv_obj_set_pos(preview_box_, (camera_app::kScreenW - camera_app::kPreviewW) / 2, 0); + lv_obj_set_style_bg_color(preview_box_, camera_app::color(0x050505), 0); + lv_obj_set_style_bg_opa(preview_box_, LV_OPA_COVER, 0); + + init_image_descriptor(camera_app::kPreviewW, camera_app::kPreviewH); + preview_img_ = lv_img_create(preview_box_); + lv_obj_set_size(preview_img_, camera_app::kPreviewW, camera_app::kPreviewH); + lv_img_set_src(preview_img_, &preview_dsc_); + lv_obj_center(preview_img_); + + status_label_ = lv_label_create(preview_box_); + lv_obj_set_width(status_label_, camera_app::kPreviewW - 20); + lv_obj_align(status_label_, LV_ALIGN_TOP_MID, 0, 4); + lv_label_set_long_mode(status_label_, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_font(status_label_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(status_label_, camera_app::color(camera_app::kText), 0); + lv_obj_set_style_bg_color(status_label_, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(status_label_, LV_OPA_50, 0); + lv_obj_set_style_pad_hor(status_label_, 5, 0); + lv_label_set_text(status_label_, "Opening camera..."); + + zoom_map_ = lv_obj_create(page_camera_); + camera_app::clear_obj(zoom_map_); + lv_obj_set_size(zoom_map_, 64, 46); + lv_obj_align(zoom_map_, LV_ALIGN_TOP_RIGHT, -8, 8); + lv_obj_set_style_bg_color(zoom_map_, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(zoom_map_, LV_OPA_40, 0); + lv_obj_set_style_border_color(zoom_map_, camera_app::color(camera_app::kText), 0); + lv_obj_set_style_border_width(zoom_map_, 1, 0); + zoom_view_ = lv_obj_create(zoom_map_); + camera_app::clear_obj(zoom_view_); + lv_obj_set_size(zoom_view_, 24, 18); + lv_obj_set_style_bg_color(zoom_view_, camera_app::color(camera_app::kText), 0); + lv_obj_set_style_bg_opa(zoom_view_, LV_OPA_20, 0); + lv_obj_set_style_border_color(zoom_view_, camera_app::color(camera_app::kText), 0); + lv_obj_set_style_border_width(zoom_view_, 1, 0); + zoom_label_ = lv_label_create(zoom_map_); + lv_obj_set_style_text_font(zoom_label_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(zoom_label_, camera_app::color(camera_app::kText), 0); + lv_obj_align(zoom_label_, LV_ALIGN_BOTTOM_RIGHT, -2, -2); + update_zoom_ui(); + + flash_ = lv_obj_create(page_camera_); + camera_app::clear_obj(flash_); + lv_obj_set_size(flash_, camera_app::kScreenW, camera_app::kContentH); + lv_obj_set_style_bg_color(flash_, camera_app::color(camera_app::kText), 0); + lv_obj_set_style_bg_opa(flash_, LV_OPA_TRANSP, 0); + lv_obj_add_flag(flash_, LV_OBJ_FLAG_HIDDEN); } - void open_imx219_camera() + void build_gallery_page() { - cm_ = std::make_unique(); + gallery_img_ = lv_img_create(page_gallery_); + lv_obj_set_size(gallery_img_, camera_app::kScreenW, camera_app::kContentH); + lv_obj_center(gallery_img_); + lv_image_set_inner_align(gallery_img_, LV_IMAGE_ALIGN_CONTAIN); + + gallery_empty_ = lv_label_create(page_gallery_); + lv_obj_set_width(gallery_empty_, camera_app::kScreenW); + lv_obj_set_style_text_align(gallery_empty_, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(gallery_empty_, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(gallery_empty_, camera_app::color(camera_app::kText), 0); + lv_label_set_text(gallery_empty_, "No photos"); + lv_obj_center(gallery_empty_); + + gallery_top_ = lv_obj_create(page_gallery_); + camera_app::clear_obj(gallery_top_); + lv_obj_set_size(gallery_top_, camera_app::kScreenW, 24); + lv_obj_set_pos(gallery_top_, 0, 0); + lv_obj_set_style_bg_color(gallery_top_, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(gallery_top_, LV_OPA_50, 0); + + gallery_counter_ = lv_label_create(gallery_top_); + lv_obj_set_pos(gallery_counter_, 8, 5); + lv_obj_set_width(gallery_counter_, 60); + lv_obj_set_style_text_font(gallery_counter_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(gallery_counter_, camera_app::color(camera_app::kText), 0); + + gallery_title_ = lv_label_create(gallery_top_); + lv_obj_set_pos(gallery_title_, 72, 5); + lv_obj_set_width(gallery_title_, 240); + lv_label_set_long_mode(gallery_title_, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_font(gallery_title_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(gallery_title_, camera_app::color(camera_app::kMuted), 0); + } - if (cm_->start()) + void build_bottom_bar() + { + bottom_bar_ = lv_obj_create(ui_root); + camera_app::clear_obj(bottom_bar_); + lv_obj_set_size(bottom_bar_, camera_app::kScreenW, camera_app::kBottomH); + lv_obj_set_pos(bottom_bar_, 0, 145); + lv_obj_set_style_bg_color(bottom_bar_, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(bottom_bar_, LV_OPA_40, 0); + + for (int i = 0; i < 5; ++i) { - set_status("CM failed", false); - printf("[Camera] CameraManager start failed\n"); - return; + bottom_btn_[i] = lv_btn_create(bottom_bar_); + lv_obj_remove_style_all(bottom_btn_[i]); + lv_obj_set_size(bottom_btn_[i], 64, camera_app::kBottomH); + lv_obj_set_pos(bottom_btn_[i], i * 64, 0); + lv_obj_add_flag(bottom_btn_[i], LV_OBJ_FLAG_CLICKABLE); + bottom_label_[i] = lv_label_create(bottom_btn_[i]); + lv_obj_set_style_text_font(bottom_label_[i], &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(bottom_label_[i], camera_app::color(camera_app::kText), 0); + lv_obj_center(bottom_label_[i]); + lvgl_add_call(bottom_btn_[i], [this, i](lv_event_code_t c, void *, void *) { + if (c == LV_EVENT_CLICKED) + dispatch_button(i); + }); } + } - std::shared_ptr selected; + void build_delete_dialog() + { + dialog_scrim_ = lv_obj_create(ui_root); + camera_app::clear_obj(dialog_scrim_); + lv_obj_set_size(dialog_scrim_, camera_app::kScreenW, 170); + lv_obj_set_pos(dialog_scrim_, 0, 0); + lv_obj_set_style_bg_color(dialog_scrim_, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(dialog_scrim_, LV_OPA_70, 0); + + lv_obj_t *panel = lv_obj_create(dialog_scrim_); + camera_app::clear_obj(panel); + lv_obj_set_size(panel, 236, 112); + lv_obj_center(panel); + lv_obj_set_style_bg_color(panel, camera_app::color(camera_app::kPanelHigh), 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(panel, camera_app::color(camera_app::kOutline), 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_radius(panel, 8, 0); + lv_obj_set_style_pad_all(panel, 10, 0); + + lv_obj_t *title = lv_label_create(panel); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(title, camera_app::color(camera_app::kText), 0); + lv_label_set_text(title, "Delete photo?"); + lv_obj_set_pos(title, 10, 9); + + lv_obj_t *body = lv_label_create(panel); + lv_obj_set_style_text_font(body, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(body, camera_app::color(camera_app::kMuted), 0); + lv_label_set_text(body, "This cannot be undone."); + lv_obj_set_pos(body, 10, 35); + + dialog_cancel_ = make_dialog_button(panel, "Cancel", 28, false); + dialog_confirm_ = make_dialog_button(panel, "Confirm", 132, true); + lv_obj_add_flag(dialog_scrim_, LV_OBJ_FLAG_HIDDEN); + } - for (const std::shared_ptr &cam : cm_->cameras()) - { - std::string model_text; + lv_obj_t *make_dialog_button(lv_obj_t *parent, const char *text, int x, bool danger) + { + lv_obj_t *btn = lv_obj_create(parent); + camera_app::clear_obj(btn); + lv_obj_set_size(btn, 76, 30); + lv_obj_set_pos(btn, x, 72); + lv_obj_set_style_radius(btn, 6, 0); + lv_obj_set_style_border_width(btn, 1, 0); + lv_obj_t *label = lv_label_create(btn); + lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0); + lv_label_set_text(label, text); + lv_obj_center(label); + (void)danger; + return btn; + } - const auto &props = cam->properties(); - auto model = props.get(libcamera::properties::Model); + void build_info_panel() + { + info_scrim_ = lv_obj_create(ui_root); + camera_app::clear_obj(info_scrim_); + lv_obj_set_size(info_scrim_, camera_app::kScreenW, 170); + lv_obj_set_pos(info_scrim_, 0, 0); + lv_obj_set_style_bg_color(info_scrim_, camera_app::color(camera_app::kBlack), 0); + lv_obj_set_style_bg_opa(info_scrim_, LV_OPA_70, 0); + + lv_obj_t *panel = lv_obj_create(info_scrim_); + camera_app::clear_obj(panel); + lv_obj_set_size(panel, 244, 160); + lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, 5); + lv_obj_set_style_bg_color(panel, camera_app::color(camera_app::kPanelHigh), 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(panel, camera_app::color(camera_app::kOutline), 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_radius(panel, 8, 0); + lv_obj_set_style_pad_all(panel, 10, 0); + + lv_obj_t *title = lv_label_create(panel); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(title, camera_app::color(camera_app::kText), 0); + lv_label_set_text(title, "Photo info"); + lv_obj_set_pos(title, 10, 8); + + info_body_ = lv_label_create(panel); + lv_obj_set_pos(info_body_, 10, 32); + lv_obj_set_width(info_body_, 224); + lv_label_set_long_mode(info_body_, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(info_body_, &lv_font_montserrat_10, 0); + lv_obj_set_style_text_color(info_body_, camera_app::color(camera_app::kMuted), 0); + lv_obj_add_flag(info_scrim_, LV_OBJ_FLAG_HIDDEN); + } - if (model) - model_text = *model; - else - model_text = cam->id(); + void init_image_descriptor(int w, int h) + { + std::lock_guard lock(frame_mutex_); + display_buf_.assign(w * h, 0); + std::memset(&preview_dsc_, 0, sizeof(preview_dsc_)); + preview_dsc_.header.magic = LV_IMAGE_HEADER_MAGIC; + preview_dsc_.header.cf = LV_COLOR_FORMAT_RGB565; + preview_dsc_.header.w = w; + preview_dsc_.header.h = h; + preview_dsc_.header.stride = w * sizeof(uint16_t); + preview_dsc_.data_size = display_buf_.size() * sizeof(uint16_t); + preview_dsc_.data = reinterpret_cast(display_buf_.data()); + } - std::string lower = lower_string(model_text); + void bind_keyboard() + { + lvgl_add_call(ui_root, [this](lv_event_code_t c, void *event_param, void *) { + if (c != static_cast(LV_EVENT_KEYBOARD)) + return; + struct key_item *key = static_cast(event_param); + if (!key || key->key_state != 0) + return; + handle_key(key->key_code); + }); + } - printf("[Camera] Found camera: %s\n", model_text.c_str()); + void start_camera() + { + camera_.set_frame_callback(make_callback([this](int code, std::string data) { + if (code == 0) + on_frame_payload(std::move(data)); + })); + camera_.set_status_callback(make_callback([this](int code, std::string data) { + set_async_status(code == 0 ? camera_app::trim_line(data) : "Camera unavailable"); + })); + camera_.start(camera_app::kPreviewW, camera_app::kPreviewH, make_callback([this](int code, std::string data) { + set_async_status(code == 0 ? "Camera ready" : camera_app::trim_line(data)); + })); + } - if (lower.find("imx219") != std::string::npos) - { - selected = cam; - break; - } - } + camera_app::HardwareCameraClient::Callback make_callback(camera_app::HardwareCameraClient::Callback cb) + { + auto alive = alive_; + return [alive, cb = std::move(cb)](int code, std::string data) mutable { + if (!alive->load() || !cb) + return; + cb(code, std::move(data)); + }; + } - if (!selected) + void handle_key(uint32_t key) + { + if (key == KEY_ESC) { - camera_found_ = false; - set_status("No IMX219", false); - printf("[Camera] IMX219 not found\n"); + handle_exit(); return; } - - camera_ = selected; - camera_found_ = true; - - if (camera_->acquire()) + if (key == KEY_ENTER) { - set_status("Acquire fail", false); - printf("[Camera] Camera acquire failed\n"); - camera_.reset(); + if (current_page_ == Page::DeleteConfirm) + confirm_delete(); + else if (current_page_ == Page::Camera) + capture_photo(); return; } - - config_ = camera_->generateConfiguration({ libcamera::StreamRole::Viewfinder }); - - if (!config_ || config_->empty()) + if (key == KEY_UP) { - set_status("Config fail", false); - printf("[Camera] generateConfiguration failed\n"); - camera_->release(); - camera_.reset(); + if (current_page_ == Page::Camera) + pan(0, -1); return; } - - libcamera::StreamConfiguration &cfg = config_->at(0); - - cfg.size.width = PREVIEW_W; - cfg.size.height = PREVIEW_H; - cfg.pixelFormat = libcamera::formats::RGB565; - cfg.bufferCount = 4; - - libcamera::CameraConfiguration::Status validation = config_->validate(); - - if (validation == libcamera::CameraConfiguration::Invalid) + if (key == KEY_DOWN) { - set_status("Invalid cfg", false); - printf("[Camera] Invalid camera configuration\n"); - camera_->release(); - camera_.reset(); + if (current_page_ == Page::Camera) + pan(0, 1); return; } - - if (camera_->configure(config_.get())) + if (key == KEY_LEFT) { - set_status("Cfg failed", false); - printf("[Camera] camera configure failed\n"); - camera_->release(); - camera_.reset(); + if (current_page_ == Page::Gallery) + gallery_prev(); + else if (current_page_ == Page::DeleteConfirm) + delete_choice_ = 0, update_delete_choice_ui(); + else if (current_page_ == Page::Camera) + pan(-1, 0); return; } - - cfg = config_->at(0); - - if (!is_supported_preview_format(cfg.pixelFormat)) + if (key == KEY_RIGHT) { - set_status("Fmt fail", false); - printf("[Camera] Unsupported preview format=%s\n", - cfg.pixelFormat.toString().c_str()); - - camera_->release(); - camera_.reset(); + if (current_page_ == Page::Gallery) + gallery_next(); + else if (current_page_ == Page::DeleteConfirm) + delete_choice_ = 1, update_delete_choice_ui(); + else if (current_page_ == Page::Camera) + pan(1, 0); return; } - stream_ = cfg.stream(); - - stream_w_ = cfg.size.width; - stream_h_ = cfg.size.height; - stream_stride_ = cfg.stride; - stream_format_ = cfg.pixelFormat; - - printf("[Camera] Stream: %dx%d stride=%d format=%s\n", - stream_w_, - stream_h_, - stream_stride_, - cfg.pixelFormat.toString().c_str()); - - init_image_buffer(stream_w_, stream_h_); - - if (ui_obj_.count("img")) - lv_img_set_src(ui_obj_["img"], &img_dsc_); - - allocator_ = std::make_unique(camera_); - - if (allocator_->allocate(stream_) < 0) + int button = -1; + switch (key) { - set_status("Alloc fail", false); - printf("[Camera] FrameBuffer allocation failed\n"); - camera_->release(); - camera_.reset(); - return; + case KEY_1: button = 0; break; + case KEY_2: button = 1; break; + case KEY_3: button = 2; break; + case KEY_4: button = 3; break; + case KEY_5: button = 4; break; + default: break; } + if (button >= 0) + dispatch_button(button); + } - const std::vector> &buffers = - allocator_->buffers(stream_); - - for (const std::unique_ptr &buffer : buffers) + void dispatch_button(int idx) + { + switch (current_page_) { - auto planes = buffer->planes(); - - if (planes.empty()) - continue; - - const libcamera::FrameBuffer::Plane &plane = planes[0]; - - void *memory = mmap(nullptr, - plane.length, - PROT_READ | PROT_WRITE, - MAP_SHARED, - plane.fd.get(), - plane.offset); - - if (memory == MAP_FAILED) - { - printf("[Camera] mmap failed\n"); - continue; - } - - mapped_buffers_[buffer.get()] = { memory, plane.length, plane.fd.get() }; - - std::unique_ptr request = - camera_->createRequest(); - - if (!request) - { - printf("[Camera] createRequest failed\n"); - continue; - } - - if (request->addBuffer(stream_, buffer.get()) < 0) - { - printf("[Camera] addBuffer failed\n"); - continue; - } - - requests_.push_back(std::move(request)); + case Page::Camera: + if (idx == 0) handle_exit(); + else if (idx == 1) zoom_out(); + else if (idx == 2) capture_photo(); + else if (idx == 3) zoom_in(); + else if (idx == 4) open_gallery(); + break; + case Page::Gallery: + if (idx == 0) show_page(Page::Camera); + else if (idx == 1) gallery_prev(); + else if (idx == 2) show_info(); + else if (idx == 3) gallery_next(); + else if (idx == 4) open_delete_confirm(); + break; + case Page::DeleteConfirm: + if (idx == 0) close_delete_confirm(); + else if (idx == 1) delete_choice_ = 0, update_delete_choice_ui(); + else if (idx == 3) delete_choice_ = 1, update_delete_choice_ui(); + else if (idx == 2 || idx == 4) confirm_delete(); + break; + case Page::Info: + if (idx == 0 || idx == 2) close_info(); + break; } + } - if (requests_.empty()) + void handle_exit() + { + if (current_page_ == Page::Info) { - set_status("Req fail", false); - printf("[Camera] No camera request created\n"); - close_camera(); + close_info(); return; } - - camera_->requestCompleted.connect(this, &UICameraPage::request_complete); - - if (camera_->start()) + if (current_page_ == Page::DeleteConfirm) { - set_status("Start fail", false); - printf("[Camera] camera start failed\n"); - close_camera(); + close_delete_confirm(); return; } - - for (std::unique_ptr &request : requests_) + if (current_page_ == Page::Gallery) { - camera_->queueRequest(request.get()); + show_page(Page::Camera); + return; } - - streaming_ = true; - - set_status("STREAMING", true); - - printf("[Camera] IMX219 stream started\n"); + if (go_back_home) + go_back_home(); } - void close_camera() + void show_page(Page page) { - streaming_ = false; - - if (camera_) + current_page_ = page; + if (page == Page::Camera) { - camera_->requestCompleted.disconnect(this); - - camera_->stop(); - - requests_.clear(); - - for (auto &it : mapped_buffers_) - { - if (it.second.addr && it.second.addr != MAP_FAILED) - munmap(it.second.addr, it.second.size); - } - - mapped_buffers_.clear(); - - allocator_.reset(); - - camera_->release(); - camera_.reset(); + lv_obj_clear_flag(page_camera_, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(page_gallery_, LV_OBJ_FLAG_HIDDEN); + set_buttons({"<", "-", "O", "+", "G"}); } - - if (cm_) + else if (page == Page::Gallery) { - cm_->stop(); - cm_.reset(); + lv_obj_add_flag(page_camera_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(page_gallery_, LV_OBJ_FLAG_HIDDEN); + set_buttons({"<", "<", "i", ">", "X"}); + refresh_gallery_ui(); } } - void set_status(const char *text, bool ok) + void set_buttons(std::initializer_list labels) { - if (!ui_obj_.count("lbl_status")) - return; - - lv_label_set_text(ui_obj_["lbl_status"], text); - - lv_obj_set_style_text_color(ui_obj_["lbl_status"], - ok ? lv_color_hex(0x2ECC71) - : lv_color_hex(0xE74C3C), - LV_PART_MAIN | LV_STATE_DEFAULT); + int i = 0; + for (const char *label : labels) + { + if (i < 5 && bottom_label_[i]) + lv_label_set_text(bottom_label_[i], label); + ++i; + } } - // ============================================================ - // frame callback - // ============================================================ - - void request_complete(libcamera::Request *request) + void capture_photo() { - if (request->status() == libcamera::Request::RequestCancelled) + if (capture_pending_) return; + capture_pending_ = true; + std::string path = camera_app::make_photo_path(); + play_flash(); + set_async_status("Capturing..."); + camera_.capture(path, camera_app::kPreviewW, camera_app::kPreviewH, make_callback([this](int code, std::string data) { + capture_pending_ = false; + if (code == 0) + set_async_status("Saved " + camera_app::trim_line(data)); + else + set_async_status("Capture failed"); + status_hide_at_ = lv_tick_get() + 2800; + })); + } - auto it = request->buffers().find(stream_); - if (it == request->buffers().end()) - return; + void zoom_in() + { + zoom_.zoom = zoom_.zoom < 250 ? 250 : 500; + update_zoom_ui(); + camera_.zoom_in(make_callback([this](int, std::string data) { parse_zoom(data); })); + } - libcamera::FrameBuffer *buffer = it->second; + void zoom_out() + { + zoom_.zoom = zoom_.zoom > 250 ? 250 : 100; + if (zoom_.zoom == 100) + zoom_.view_x = zoom_.view_y = 50; + update_zoom_ui(); + camera_.zoom_out(make_callback([this](int, std::string data) { parse_zoom(data); })); + } - auto map_it = mapped_buffers_.find(buffer); - if (map_it == mapped_buffers_.end()) + void pan(int dx, int dy) + { + if (zoom_.zoom <= 100) return; - - const uint8_t *src = - reinterpret_cast(map_it->second.addr); - - size_t bytes_used = map_it->second.size; - const auto &metadata = buffer->metadata(); - if (!metadata.planes().empty() && metadata.planes()[0].bytesused > 0) - bytes_used = std::min(bytes_used, - static_cast(metadata.planes()[0].bytesused)); - - dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ); - convert_preview_frame(src, bytes_used); - dma_buf_sync(map_it->second.fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ); - - request->reuse(libcamera::Request::ReuseBuffers); - camera_->queueRequest(request); + zoom_.view_x = std::max(0, std::min(100, zoom_.view_x + dx * 8)); + zoom_.view_y = std::max(0, std::min(100, zoom_.view_y + dy * 8)); + update_zoom_ui(); + camera_.pan(dx, dy, make_callback([this](int, std::string data) { parse_zoom(data); })); } - static bool is_supported_preview_format(const libcamera::PixelFormat &format) + void parse_zoom(const std::string &data) { - return format == libcamera::formats::RGB888 || - format == libcamera::formats::BGR888 || - format == libcamera::formats::XRGB8888 || - format == libcamera::formats::XBGR8888 || - format == libcamera::formats::RGB565; + int z = 0, x = 0, y = 0; + if (std::sscanf(data.c_str(), "ZOOM %d %d %d", &z, &x, &y) == 3) + { + zoom_.zoom = z; + zoom_.view_x = x; + zoom_.view_y = y; + zoom_dirty_.store(true); + } } - static void dma_buf_sync(int fd, uint64_t flags) + void update_zoom_ui() { - if (fd < 0) + if (!zoom_map_ || !zoom_view_) return; - - struct dma_buf_sync sync = { flags }; - ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync); + if (zoom_.zoom <= 100) + { + lv_obj_add_flag(zoom_map_, LV_OBJ_FLAG_HIDDEN); + return; + } + lv_obj_clear_flag(zoom_map_, LV_OBJ_FLAG_HIDDEN); + int inner_w = 56; + int inner_h = 38; + int view_w = std::max(8, inner_w * 100 / zoom_.zoom); + int view_h = std::max(6, inner_h * 100 / zoom_.zoom); + int max_x = std::max(0, inner_w - view_w); + int max_y = std::max(0, inner_h - view_h); + lv_obj_set_size(zoom_view_, view_w, view_h); + lv_obj_set_pos(zoom_view_, 4 + max_x * zoom_.view_x / 100, 4 + max_y * zoom_.view_y / 100); + lv_label_set_text(zoom_label_, zoom_.zoom >= 500 ? "x5" : "x2.5"); } - static void rgb565_to_rgb888(uint16_t p, - uint8_t &r, - uint8_t &g, - uint8_t &b) + void open_gallery() { - r = ((p >> 11) & 0x1F) << 3; - g = ((p >> 5) & 0x3F) << 2; - b = (p & 0x1F) << 3; - r |= r >> 5; - g |= g >> 6; - b |= b >> 5; + gallery_.refresh(); + show_page(Page::Gallery); } - static uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) + void gallery_prev() { - return static_cast(((r & 0xF8) << 8) | - ((g & 0xFC) << 3) | - (b >> 3)); + gallery_.prev(); + refresh_gallery_ui(); } - void store_preview_pixel(int idx, uint8_t r, uint8_t g, uint8_t b) + void gallery_next() { - pending_lv_buf_[idx] = rgb888_to_rgb565(r, g, b); - - int rgb_idx = idx * 3; - pending_rgb_buf_[rgb_idx + 0] = r; - pending_rgb_buf_[rgb_idx + 1] = g; - pending_rgb_buf_[rgb_idx + 2] = b; + gallery_.next(); + refresh_gallery_ui(); } - void store_preview_pixel_rgb565(int idx, uint16_t rgb565) + void refresh_gallery_ui() { - uint8_t r = 0; - uint8_t g = 0; - uint8_t b = 0; - - pending_lv_buf_[idx] = rgb565; - rgb565_to_rgb888(rgb565, r, g, b); - - int rgb_idx = idx * 3; - pending_rgb_buf_[rgb_idx + 0] = r; - pending_rgb_buf_[rgb_idx + 1] = g; - pending_rgb_buf_[rgb_idx + 2] = b; + gallery_.refresh(); + bool empty = gallery_.empty(); + if (empty) + { + lv_obj_clear_flag(gallery_empty_, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(gallery_img_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(gallery_counter_, "0 / 0"); + lv_label_set_text(gallery_title_, "No photos"); + return; + } + lv_obj_add_flag(gallery_empty_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(gallery_img_, LV_OBJ_FLAG_HIDDEN); + lv_img_set_src(gallery_img_, gallery_.current().c_str()); + char counter[32]; + std::snprintf(counter, sizeof(counter), "%d / %d", gallery_.index(), gallery_.count()); + lv_label_set_text(gallery_counter_, counter); + lv_label_set_text(gallery_title_, camera_app::filename_only(gallery_.current()).c_str()); } - void convert_preview_frame(const uint8_t *src, size_t bytes_used) + void open_delete_confirm() { - std::lock_guard lock(frame_mutex_); - - if ((int)pending_lv_buf_.size() != stream_w_ * stream_h_) + if (gallery_.empty()) return; + current_page_ = Page::DeleteConfirm; + delete_choice_ = 0; + update_delete_choice_ui(); + lv_obj_clear_flag(dialog_scrim_, LV_OBJ_FLAG_HIDDEN); + set_buttons({"<", "<", "OK", ">", "OK"}); + } - if ((int)pending_rgb_buf_.size() != stream_w_ * stream_h_ * 3) - return; + void close_delete_confirm() + { + lv_obj_add_flag(dialog_scrim_, LV_OBJ_FLAG_HIDDEN); + show_page(Page::Gallery); + } - if ((int)snapshot_lv_buf_.size() != stream_w_ * stream_h_) - return; + void update_delete_choice_ui() + { + style_dialog_button(dialog_cancel_, delete_choice_ == 0, false); + style_dialog_button(dialog_confirm_, delete_choice_ == 1, true); + } - const bool is_rgb888 = stream_format_ == libcamera::formats::RGB888; - const bool is_bgr888 = stream_format_ == libcamera::formats::BGR888; - const bool is_xrgb8888 = stream_format_ == libcamera::formats::XRGB8888; - const bool is_xbgr8888 = stream_format_ == libcamera::formats::XBGR8888; - const bool is_rgb565 = stream_format_ == libcamera::formats::RGB565; + void style_dialog_button(lv_obj_t *obj, bool selected, bool danger) + { + lv_obj_set_style_bg_color(obj, camera_app::color(selected ? (danger ? 0x93000A : 0x4F378A) : camera_app::kPanel), 0); + lv_obj_set_style_bg_opa(obj, selected ? LV_OPA_COVER : LV_OPA_60, 0); + lv_obj_set_style_border_color(obj, camera_app::color(selected ? (danger ? camera_app::kDanger : camera_app::kPrimary) : camera_app::kOutline), 0); + lv_obj_set_style_text_color(obj, camera_app::color(selected ? camera_app::kText : camera_app::kMuted), 0); + } - const int bytes_per_pixel = is_rgb888 || is_bgr888 ? 3 : - is_rgb565 ? 2 : 4; - const int min_stride = stream_w_ * bytes_per_pixel; - const int row_stride = stream_stride_ > 0 ? stream_stride_ : min_stride; + void confirm_delete() + { + if (delete_choice_ == 1) + gallery_.delete_current(); + close_delete_confirm(); + refresh_gallery_ui(); + } - if (row_stride < min_stride) + void show_info() + { + if (gallery_.empty()) return; - - for (int y = 0; y < stream_h_; ++y) - { - const size_t row_offset = static_cast(y) * row_stride; - if (row_offset + min_stride > bytes_used) - break; - - const uint8_t *line = src + row_offset; - const int dst_y = stream_h_ - 1 - y; - - for (int x = 0; x < stream_w_; ++x) - { - const int dst_x = stream_w_ - 1 - x; - uint8_t r = 0; - uint8_t g = 0; - uint8_t b = 0; - - if (is_rgb888) - { - const uint8_t *p = line + x * 3; - r = p[0]; - g = p[1]; - b = p[2]; - } - else if (is_bgr888) - { - const uint8_t *p = line + x * 3; - b = p[0]; - g = p[1]; - r = p[2]; - } - else if (is_xrgb8888) - { - const uint8_t *p = line + x * 4; - b = p[0]; - g = p[1]; - r = p[2]; - } - else if (is_xbgr8888) - { - const uint8_t *p = line + x * 4; - r = p[0]; - g = p[1]; - b = p[2]; - } - else if (is_rgb565) - { - const uint8_t *p = line + x * 2; - int idx = dst_y * stream_w_ + dst_x; - store_preview_pixel_rgb565(idx, - static_cast(p[0] | (p[1] << 8))); - continue; - } - - int idx = dst_y * stream_w_ + dst_x; - store_preview_pixel(idx, r, g, b); - } - } - - if (capture_requested_.exchange(false)) - { - snapshot_rgb_buf_ = pending_rgb_buf_; - snapshot_lv_buf_ = pending_lv_buf_; - snapshot_ready_ = true; - } - - new_frame_ = true; + current_page_ = Page::Info; + const std::string &path = gallery_.current(); + std::ostringstream out; + out << "File\n" << camera_app::filename_only(path) << "\n\n" + << "Path\n" << path << "\n\n" + << "Created\n" << camera_app::format_file_time(path); + lv_label_set_text(info_body_, out.str().c_str()); + lv_obj_clear_flag(info_scrim_, LV_OBJ_FLAG_HIDDEN); + set_buttons({"<", "", "i", "", ""}); } - // ============================================================ - // LVGL timer: update display and save snapshot - // ============================================================ - - static void frame_timer_cb(lv_timer_t *t) + void close_info() { - UICameraPage *self = - static_cast(lv_timer_get_user_data(t)); - - if (self) - self->on_frame_timer(); + lv_obj_add_flag(info_scrim_, LV_OBJ_FLAG_HIDDEN); + show_page(Page::Gallery); } - void on_frame_timer() + void on_frame_payload(std::string payload) { - bool need_update = false; - - bool need_save = false; - std::vector save_rgb; - int save_w = 0; - int save_h = 0; - std::string save_path; + int w = 0, h = 0; + char fmt[16]{}; + int header_len = 0; + if (std::sscanf(payload.c_str(), "FRAME %d %d %15s\n%n", &w, &h, fmt, &header_len) != 3) + return; + if (w <= 0 || h <= 0 || std::strcmp(fmt, "RGB565") != 0) + return; + size_t bytes = static_cast(w) * h * sizeof(uint16_t); + if (payload.size() < static_cast(header_len) + bytes) + return; + std::vector pixels(static_cast(w) * h); + std::memcpy(pixels.data(), payload.data() + header_len, bytes); + std::lock_guard lock(frame_mutex_); + pending_frame_.width = w; + pending_frame_.height = h; + pending_frame_.rgb565 = std::move(pixels); + new_frame_.store(true); + } + void poll_ui() + { + if (new_frame_.exchange(false)) { - std::lock_guard lock(frame_mutex_); - - if (new_frame_) + camera_app::Frame frame; { - std::swap(display_buf_, pending_lv_buf_); - - img_dsc_.data = - reinterpret_cast(display_buf_.data()); - - new_frame_ = false; - need_update = true; + std::lock_guard lock(frame_mutex_); + frame = pending_frame_; } - - if (snapshot_ready_) + if (frame.width > 0 && frame.height > 0 && !frame.rgb565.empty()) { - save_rgb = snapshot_rgb_buf_; - save_w = stream_w_; - save_h = stream_h_; - save_path = last_file_; - - snapshot_ready_ = false; - need_save = true; + if (frame.width != static_cast(preview_dsc_.header.w) || + frame.height != static_cast(preview_dsc_.header.h)) + { + init_image_descriptor(frame.width, frame.height); + lv_obj_set_size(preview_box_, frame.width, frame.height); + lv_img_set_src(preview_img_, &preview_dsc_); + } + display_buf_ = std::move(frame.rgb565); + preview_dsc_.data = reinterpret_cast(display_buf_.data()); + preview_dsc_.data_size = display_buf_.size() * sizeof(uint16_t); + lv_img_set_src(preview_img_, &preview_dsc_); + lv_obj_invalidate(preview_img_); } } - - if (need_update) + if (status_dirty_.exchange(false)) { - lv_img_set_src(ui_obj_["img"], &img_dsc_); - lv_obj_invalidate(ui_obj_["img"]); + std::lock_guard lock(status_mutex_); + lv_label_set_text(status_label_, status_text_.c_str()); + lv_obj_clear_flag(status_label_, LV_OBJ_FLAG_HIDDEN); } - - if (need_save) + if (zoom_dirty_.exchange(false)) + update_zoom_ui(); + if (status_hide_at_ && static_cast(lv_tick_get() - status_hide_at_) >= 0) { - show_snapshot_review(); - - if (save_jpeg_rgb888(save_path, - save_rgb.data(), - save_w, - save_h, - 90)) - { - char buf[256]; - - snprintf(buf, - sizeof(buf), - "Saved: %s chmod 666", - save_path.c_str()); - - lv_label_set_text(ui_obj_["lbl_info"], buf); - - printf("[Camera] Saved frame: %s\n", save_path.c_str()); - } - else - { - lv_label_set_text(ui_obj_["lbl_info"], "Save failed"); - } + status_hide_at_ = 0; + lv_obj_add_flag(status_label_, LV_OBJ_FLAG_HIDDEN); } - - update_snapshot_review_timeout(); - } - - void show_snapshot_review() - { - if (!ui_obj_.count("photo_frame") || !ui_obj_.count("photo_img")) - return; - - snapshot_img_dsc_.data = reinterpret_cast(snapshot_lv_buf_.data()); - lv_img_set_src(ui_obj_["photo_img"], &snapshot_img_dsc_); - lv_obj_invalidate(ui_obj_["photo_img"]); - - lv_obj_remove_flag(ui_obj_["photo_frame"], LV_OBJ_FLAG_HIDDEN); - lv_obj_move_foreground(ui_obj_["photo_frame"]); - - snapshot_review_active_ = true; - snapshot_review_until_ = lv_tick_get() + 3000; - } - - void update_snapshot_review_timeout() - { - if (!snapshot_review_active_) - return; - - if (static_cast(lv_tick_get() - snapshot_review_until_) < 0) - return; - - if (ui_obj_.count("photo_frame")) - lv_obj_add_flag(ui_obj_["photo_frame"], LV_OBJ_FLAG_HIDDEN); - - snapshot_review_active_ = false; } - // ============================================================ - // capture - // ============================================================ - - void capture_from_stream() + void set_async_status(std::string text) { - if (!camera_found_ || !streaming_) - { - set_status("Not ready", false); - return; - } - - last_file_ = make_photo_path(); - - capture_requested_ = true; + std::lock_guard lock(status_mutex_); + status_text_ = std::move(text); + status_dirty_.store(true); } - // ============================================================ - // event - // ============================================================ - - void event_handler_init() + static void flash_opa_anim_cb(void *obj, int32_t value) { - lv_obj_add_event_cb(ui_root, - UICameraPage::static_lvgl_handler, - LV_EVENT_ALL, - this); + lv_obj_set_style_bg_opa(static_cast(obj), static_cast(value), 0); } - static void static_lvgl_handler(lv_event_t *e) + static void flash_done_cb(lv_anim_t *anim) { - UICameraPage *self = - static_cast(lv_event_get_user_data(e)); - - if (self) - self->event_handler(e); + lv_obj_t *obj = static_cast(lv_anim_get_user_data(anim)); + if (obj) + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } - void event_handler(lv_event_t *e) + void play_flash() { - if (IS_KEY_RELEASED(e)) - { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); - handle_key(key); - } + if (!flash_) + return; + lv_obj_clear_flag(flash_, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(flash_); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, flash_); + lv_anim_set_values(&a, LV_OPA_80, LV_OPA_TRANSP); + lv_anim_set_time(&a, 320); + lv_anim_set_exec_cb(&a, flash_opa_anim_cb); + lv_anim_set_user_data(&a, flash_); + lv_anim_set_ready_cb(&a, flash_done_cb); + lv_anim_start(&a); } - void handle_key(uint32_t key) - { - switch (key) - { - case KEY_ENTER: - capture_from_stream(); - break; - - case KEY_ESC: - close_camera(); - - if (go_back_home) - go_back_home(); - - break; - - default: - break; - } - } + std::shared_ptr> alive_ = std::make_shared>(true); + camera_app::HardwareCameraClient camera_; + camera_app::GalleryStore gallery_; + Page current_page_ = Page::Camera; + camera_app::ZoomState zoom_; + + lv_obj_t *page_camera_ = nullptr; + lv_obj_t *page_gallery_ = nullptr; + lv_obj_t *preview_box_ = nullptr; + lv_obj_t *preview_img_ = nullptr; + lv_obj_t *status_label_ = nullptr; + lv_obj_t *zoom_map_ = nullptr; + lv_obj_t *zoom_view_ = nullptr; + lv_obj_t *zoom_label_ = nullptr; + lv_obj_t *flash_ = nullptr; + lv_obj_t *gallery_img_ = nullptr; + lv_obj_t *gallery_empty_ = nullptr; + lv_obj_t *gallery_top_ = nullptr; + lv_obj_t *gallery_counter_ = nullptr; + lv_obj_t *gallery_title_ = nullptr; + lv_obj_t *bottom_bar_ = nullptr; + lv_obj_t *bottom_btn_[5]{}; + lv_obj_t *bottom_label_[5]{}; + lv_obj_t *dialog_scrim_ = nullptr; + lv_obj_t *dialog_cancel_ = nullptr; + lv_obj_t *dialog_confirm_ = nullptr; + lv_obj_t *info_scrim_ = nullptr; + lv_obj_t *info_body_ = nullptr; + lv_timer_t *ui_timer_ = nullptr; + + lv_image_dsc_t preview_dsc_{}; + std::vector display_buf_; + camera_app::Frame pending_frame_; + std::mutex frame_mutex_; + std::atomic new_frame_{false}; + std::atomic status_dirty_{false}; + std::atomic zoom_dirty_{false}; + std::mutex status_mutex_; + std::string status_text_; + uint32_t status_hide_at_ = 0; + bool capture_pending_ = false; + int delete_choice_ = 0; }; - -#endif // !HAL_PLATFORM_SDL From 0a5376d2858f6a3c9fdff97263f4e4b5d8d772ef Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 13:41:17 +0800 Subject: [PATCH 08/70] Move APPLaunch HAL into cp0_lvgl Centralize APPLaunch hardware/platform interfaces under the cp0_lvgl component so the project no longer depends on projects/APPLaunch/main/hal. Changes:\n- Move public hal/*.h headers and keyboard_input.h into ext_components/cp0_lvgl/include.\n- Move Linux/CP0 HAL implementations into ext_components/cp0_lvgl/src/cp0 as cp0_hal_* files.\n- Move CP0 keyboard input into cp0_lvgl and keep app battery timer dispatch under main/src.\n- Move SDL HAL implementations into ext_components/cp0_lvgl/src/sdl and add web/win32 stubs under cp0_lvgl.\n- Remove APPLaunch main/hal from project sources and make all APPLaunch configs depend on cp0_lvgl.\n- Align CP0/SDL keyboard event dispatch with the shared lv_c_event[CP0_C_EVENT_KEYBOARD] registration. Validation:\n- CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed\n- gcc/g++ -fsyntax-only over migrated SDL/Web/Win32 HAL files\n- Verified no residual main/hal, hal/sdl, hal/web, hal/win32, or ../hal_*.h references. --- .../cp0_lvgl/include/compat/input_keys.h | 82 ++++++++++++++++ .../cp0_lvgl/include}/hal/hal_audio.h | 0 .../cp0_lvgl/include}/hal/hal_config.h | 0 .../cp0_lvgl/include}/hal/hal_filesystem.h | 0 .../cp0_lvgl/include}/hal/hal_network.h | 0 .../cp0_lvgl/include}/hal/hal_paths.h | 0 .../cp0_lvgl/include}/hal/hal_process.h | 0 .../cp0_lvgl/include}/hal/hal_pty.h | 0 .../cp0_lvgl/include}/hal/hal_screenshot.h | 0 .../cp0_lvgl/include}/hal/hal_settings.h | 0 .../cp0_lvgl/include/keyboard_input.h | 48 +++++++++ .../cp0_lvgl/src/cp0/cp0_hal_audio.c | 0 .../cp0_lvgl/src/cp0/cp0_hal_config.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_hal_network.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_hal_paths.c | 2 +- .../cp0_lvgl/src/cp0/cp0_hal_process.cpp | 7 +- .../cp0_lvgl/src/cp0/cp0_hal_pty.cpp | 4 +- .../cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_hal_settings.cpp | 2 +- .../cp0_lvgl/src/cp0/cp0_keyboard_input.c | 0 .../cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c | 3 +- .../cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c | 0 .../cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp | 2 +- .../src/sdl/cp0_hal_filesystem_sdl.cpp | 2 +- .../cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp | 2 +- .../cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c | 2 +- .../cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp | 2 +- .../cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp | 2 +- .../src/sdl/cp0_hal_screenshot_sdl.cpp | 2 +- .../cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp | 2 +- ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c | 16 +-- .../cp0_lvgl/src/web/cp0_hal_stubs_web.c | 98 +++++++++++++++++++ .../cp0_lvgl/src/win32/cp0_hal_stubs_win32.c | 98 +++++++++++++++++++ projects/APPLaunch/config_defaults.mk | 3 +- projects/APPLaunch/darwin_config_defaults.mk | 3 +- .../linux_x86_cross_cp0_config_defaults.mk | 3 +- .../linux_x86_sdl2_config_defaults.mk | 3 +- .../mac_cross_cp0_config_defaults.mk | 3 +- projects/APPLaunch/main/SConstruct | 15 ++- projects/APPLaunch/main/hal/hal_keyboard.h | 18 ---- projects/APPLaunch/main/hal/hal_platform.h | 18 ---- .../APPLaunch/main/hal/web/hal_stubs_web.c | 39 -------- .../main/hal/win32/hal_stubs_win32.c | 39 -------- .../main/{hal/battey.c => src/app_battery.c} | 0 45 files changed, 370 insertions(+), 158 deletions(-) create mode 100644 ext_components/cp0_lvgl/include/compat/input_keys.h rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_audio.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_config.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_filesystem.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_network.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_paths.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_process.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_pty.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_screenshot.h (100%) rename {projects/APPLaunch/main => ext_components/cp0_lvgl/include}/hal/hal_settings.h (100%) create mode 100644 ext_components/cp0_lvgl/include/keyboard_input.h rename projects/APPLaunch/main/hal/linux/hal_audio_linux.c => ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c (100%) rename projects/APPLaunch/main/hal/linux/hal_config_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp (98%) rename projects/APPLaunch/main/hal/linux/hal_filesystem_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp (97%) rename projects/APPLaunch/main/hal/linux/hal_network_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp (97%) rename projects/APPLaunch/main/hal/linux/hal_paths_linux.c => ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c (98%) rename projects/APPLaunch/main/hal/linux/hal_process_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp (97%) rename projects/APPLaunch/main/hal/linux/hal_pty_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp (98%) rename projects/APPLaunch/main/hal/linux/hal_screenshot_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp (99%) rename projects/APPLaunch/main/hal/linux/hal_settings_linux.cpp => ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp (99%) rename projects/APPLaunch/main/hal/keyboard_input.c => ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c (100%) rename projects/APPLaunch/main/hal/sdl/hal_audio_sdl.c => ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c (100%) rename projects/APPLaunch/main/hal/sdl/hal_config_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp (94%) rename projects/APPLaunch/main/hal/sdl/hal_filesystem_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp (98%) rename projects/APPLaunch/main/hal/sdl/hal_network_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp (97%) rename projects/APPLaunch/main/hal/sdl/hal_paths_sdl.c => ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c (98%) rename projects/APPLaunch/main/hal/sdl/hal_process_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp (99%) rename projects/APPLaunch/main/hal/sdl/hal_pty_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp (99%) rename projects/APPLaunch/main/hal/sdl/hal_screenshot_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp (82%) rename projects/APPLaunch/main/hal/sdl/hal_settings_sdl.cpp => ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp (98%) create mode 100644 ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c create mode 100644 ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c delete mode 100644 projects/APPLaunch/main/hal/hal_keyboard.h delete mode 100644 projects/APPLaunch/main/hal/hal_platform.h delete mode 100644 projects/APPLaunch/main/hal/web/hal_stubs_web.c delete mode 100644 projects/APPLaunch/main/hal/win32/hal_stubs_win32.c rename projects/APPLaunch/main/{hal/battey.c => src/app_battery.c} (100%) diff --git a/ext_components/cp0_lvgl/include/compat/input_keys.h b/ext_components/cp0_lvgl/include/compat/input_keys.h new file mode 100644 index 00000000..da5eefe3 --- /dev/null +++ b/ext_components/cp0_lvgl/include/compat/input_keys.h @@ -0,0 +1,82 @@ +#pragma once +// Cross-platform KEY_* constants +// On Linux: use the real header. On macOS/Windows: define ourselves. + +#ifdef __linux__ +#include +#else + +#ifndef KEY_ESC +#define KEY_ESC 1 +#define KEY_1 2 +#define KEY_2 3 +#define KEY_3 4 +#define KEY_4 5 +#define KEY_5 6 +#define KEY_6 7 +#define KEY_7 8 +#define KEY_8 9 +#define KEY_9 10 +#define KEY_0 11 +#define KEY_BACKSPACE 14 +#define KEY_TAB 15 +#define KEY_Q 16 +#define KEY_W 17 +#define KEY_E 18 +#define KEY_R 19 +#define KEY_T 20 +#define KEY_Y 21 +#define KEY_U 22 +#define KEY_I 23 +#define KEY_O 24 +#define KEY_P 25 +#define KEY_ENTER 28 +#define KEY_LEFTCTRL 29 +#define KEY_A 30 +#define KEY_S 31 +#define KEY_D 32 +#define KEY_F 33 +#define KEY_G 34 +#define KEY_H 35 +#define KEY_J 36 +#define KEY_K 37 +#define KEY_L 38 +#define KEY_LEFTSHIFT 42 +#define KEY_Z 44 +#define KEY_X 45 +#define KEY_C 46 +#define KEY_V 47 +#define KEY_B 48 +#define KEY_N 49 +#define KEY_M 50 +#define KEY_SPACE 57 +#define KEY_LEFTALT 56 +#define KEY_CAPSLOCK 58 +#define KEY_F1 59 +#define KEY_F2 60 +#define KEY_F3 61 +#define KEY_F4 62 +#define KEY_F5 63 +#define KEY_F6 64 +#define KEY_F7 65 +#define KEY_F8 66 +#define KEY_F9 67 +#define KEY_F10 68 +#define KEY_F11 87 +#define KEY_F12 88 +#define KEY_KPENTER 96 +#define KEY_HOME 102 +#define KEY_UP 103 +#define KEY_PAGEUP 104 +#define KEY_LEFT 105 +#define KEY_RIGHT 106 +#define KEY_END 107 +#define KEY_DOWN 108 +#define KEY_PAGEDOWN 109 +#define KEY_INSERT 110 +#define KEY_DELETE 111 +#define KEY_NEXT 407 +#define KEY_PREVIOUS 412 +#endif + +#endif // __linux__ diff --git a/projects/APPLaunch/main/hal/hal_audio.h b/ext_components/cp0_lvgl/include/hal/hal_audio.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_audio.h rename to ext_components/cp0_lvgl/include/hal/hal_audio.h diff --git a/projects/APPLaunch/main/hal/hal_config.h b/ext_components/cp0_lvgl/include/hal/hal_config.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_config.h rename to ext_components/cp0_lvgl/include/hal/hal_config.h diff --git a/projects/APPLaunch/main/hal/hal_filesystem.h b/ext_components/cp0_lvgl/include/hal/hal_filesystem.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_filesystem.h rename to ext_components/cp0_lvgl/include/hal/hal_filesystem.h diff --git a/projects/APPLaunch/main/hal/hal_network.h b/ext_components/cp0_lvgl/include/hal/hal_network.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_network.h rename to ext_components/cp0_lvgl/include/hal/hal_network.h diff --git a/projects/APPLaunch/main/hal/hal_paths.h b/ext_components/cp0_lvgl/include/hal/hal_paths.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_paths.h rename to ext_components/cp0_lvgl/include/hal/hal_paths.h diff --git a/projects/APPLaunch/main/hal/hal_process.h b/ext_components/cp0_lvgl/include/hal/hal_process.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_process.h rename to ext_components/cp0_lvgl/include/hal/hal_process.h diff --git a/projects/APPLaunch/main/hal/hal_pty.h b/ext_components/cp0_lvgl/include/hal/hal_pty.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_pty.h rename to ext_components/cp0_lvgl/include/hal/hal_pty.h diff --git a/projects/APPLaunch/main/hal/hal_screenshot.h b/ext_components/cp0_lvgl/include/hal/hal_screenshot.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_screenshot.h rename to ext_components/cp0_lvgl/include/hal/hal_screenshot.h diff --git a/projects/APPLaunch/main/hal/hal_settings.h b/ext_components/cp0_lvgl/include/hal/hal_settings.h similarity index 100% rename from projects/APPLaunch/main/hal/hal_settings.h rename to ext_components/cp0_lvgl/include/hal/hal_settings.h diff --git a/ext_components/cp0_lvgl/include/keyboard_input.h b/ext_components/cp0_lvgl/include/keyboard_input.h new file mode 100644 index 00000000..ada761b4 --- /dev/null +++ b/ext_components/cp0_lvgl/include/keyboard_input.h @@ -0,0 +1,48 @@ +#ifndef __MAIN__H__ +#define __MAIN__H__ + +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +// modifier bitmask +#define KBD_MOD_SHIFT (1u << 0) +#define KBD_MOD_CTRL (1u << 1) +#define KBD_MOD_ALT (1u << 2) +#define KBD_MOD_LOGO (1u << 3) +#define KBD_MOD_CAPS (1u << 4) +#define KBD_MOD_NUM (1u << 5) + +// key state +#define KBD_KEY_RELEASED 0 +#define KBD_KEY_PRESSED 1 +#define KBD_KEY_REPEATED 2 + +struct key_item { + uint32_t key_code; // Linux evdev key code + uint32_t keysym; // primary XKB keysym (xkb_keysym_t) + uint32_t codepoint; // corresponding Unicode code point, or 0 if none + uint32_t mods; // modifier bitmask (KBD_MOD_*) + int key_state; // 0=released, 1=pressed, 2=repeat + char sym_name[65]; // XKB keysym name + char utf8[16]; // UTF-8 character (supports multi-byte compose output) + char flage; // whether free is required + STAILQ_ENTRY(key_item) entries; +}; + +STAILQ_HEAD(keyboard_queue_t, key_item); +extern struct keyboard_queue_t keyboard_queue; +extern pthread_mutex_t keyboard_mutex; +extern volatile int LVGL_HOME_KEY_FLAG; +extern volatile int LVGL_RUN_FLAGE; +extern volatile uint32_t LV_EVENT_KEYBOARD; + +void *keyboard_read_thread(void *argv); +const char *kbd_state_name(int state); +void kbd_dump_keymap_table(void); +#ifdef __cplusplus +} +#endif +#endif \ No newline at end of file diff --git a/projects/APPLaunch/main/hal/linux/hal_audio_linux.c b/ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c similarity index 100% rename from projects/APPLaunch/main/hal/linux/hal_audio_linux.c rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c diff --git a/projects/APPLaunch/main/hal/linux/hal_config_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp similarity index 98% rename from projects/APPLaunch/main/hal/linux/hal_config_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp index ac0e4fe4..fae03728 100644 --- a/projects/APPLaunch/main/hal/linux/hal_config_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp @@ -1,4 +1,4 @@ -#include "../hal_config.h" +#include "hal/hal_config.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/linux/hal_filesystem_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp similarity index 97% rename from projects/APPLaunch/main/hal/linux/hal_filesystem_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp index adb9ea27..52ac25e4 100644 --- a/projects/APPLaunch/main/hal/linux/hal_filesystem_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp @@ -1,4 +1,4 @@ -#include "../hal_filesystem.h" +#include "hal/hal_filesystem.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/linux/hal_network_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp similarity index 97% rename from projects/APPLaunch/main/hal/linux/hal_network_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp index 13e9f7c2..b73dc299 100644 --- a/projects/APPLaunch/main/hal/linux/hal_network_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp @@ -1,4 +1,4 @@ -#include "../hal_network.h" +#include "hal/hal_network.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/linux/hal_paths_linux.c b/ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c similarity index 98% rename from projects/APPLaunch/main/hal/linux/hal_paths_linux.c rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c index 39a6f1e6..0b6b5aea 100644 --- a/projects/APPLaunch/main/hal/linux/hal_paths_linux.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c @@ -1,4 +1,4 @@ -#include "../hal_paths.h" +#include "hal/hal_paths.h" #include #include diff --git a/projects/APPLaunch/main/hal/linux/hal_process_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp similarity index 97% rename from projects/APPLaunch/main/hal/linux/hal_process_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp index dd3b42ce..7da6fd5a 100644 --- a/projects/APPLaunch/main/hal/linux/hal_process_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp @@ -1,5 +1,5 @@ -#include "../hal_process.h" -#include "../hal_config.h" +#include "hal/hal_process.h" +#include "hal/hal_config.h" #include #include #include @@ -18,6 +18,9 @@ extern "C" { extern void keyboard_resume(void); } +extern "C" void __attribute__((weak)) keyboard_pause(void) {} +extern "C" void __attribute__((weak)) keyboard_resume(void) {} + static const char *get_kbd_device() { const char *env = getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); diff --git a/projects/APPLaunch/main/hal/linux/hal_pty_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp similarity index 98% rename from projects/APPLaunch/main/hal/linux/hal_pty_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp index 48c2e2c7..adfd78a6 100644 --- a/projects/APPLaunch/main/hal/linux/hal_pty_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp @@ -1,5 +1,5 @@ -#include "../hal_pty.h" -#include "../hal_config.h" +#include "hal/hal_pty.h" +#include "hal/hal_config.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/linux/hal_screenshot_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp similarity index 99% rename from projects/APPLaunch/main/hal/linux/hal_screenshot_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp index b0a54e01..5269c8e7 100644 --- a/projects/APPLaunch/main/hal/linux/hal_screenshot_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp @@ -1,4 +1,4 @@ -#include "../hal_screenshot.h" +#include "hal/hal_screenshot.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/linux/hal_settings_linux.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp similarity index 99% rename from projects/APPLaunch/main/hal/linux/hal_settings_linux.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp index 10faa838..134c2202 100644 --- a/projects/APPLaunch/main/hal/linux/hal_settings_linux.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp @@ -1,4 +1,4 @@ -#include "../hal_settings.h" +#include "hal/hal_settings.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/keyboard_input.c b/ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c similarity index 100% rename from projects/APPLaunch/main/hal/keyboard_input.c rename to ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c index 9fc5e5b0..1ecf1cb4 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c @@ -243,7 +243,7 @@ static void cp0_send_keyboard_event(const cp0_key_event_t *event) { lv_obj_t *root = lv_display_get_screen_active(NULL); if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)LV_C_EVENT_KEYBOARD, (void *)event); + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_KEYBOARD], (void *)event); } static int cp0_init_xkb(cp0_input_ctx_t *ctx) @@ -456,4 +456,3 @@ void init_input() } pthread_detach(ctx->thread); } - diff --git a/projects/APPLaunch/main/hal/sdl/hal_audio_sdl.c b/ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c similarity index 100% rename from projects/APPLaunch/main/hal/sdl/hal_audio_sdl.c rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c diff --git a/projects/APPLaunch/main/hal/sdl/hal_config_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp similarity index 94% rename from projects/APPLaunch/main/hal/sdl/hal_config_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp index 3dfdc40e..535fffbc 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_config_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_config.h" +#include "hal/hal_config.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/sdl/hal_filesystem_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp similarity index 98% rename from projects/APPLaunch/main/hal/sdl/hal_filesystem_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp index 1232d540..456a1a9e 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_filesystem_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_filesystem.h" +#include "hal/hal_filesystem.h" #include // snprintf #include #include diff --git a/projects/APPLaunch/main/hal/sdl/hal_network_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp similarity index 97% rename from projects/APPLaunch/main/hal/sdl/hal_network_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp index 1813b6e5..a4570c63 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_network_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_network.h" +#include "hal/hal_network.h" #include #ifdef _WIN32 diff --git a/projects/APPLaunch/main/hal/sdl/hal_paths_sdl.c b/ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c similarity index 98% rename from projects/APPLaunch/main/hal/sdl/hal_paths_sdl.c rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c index b811442b..38d52d64 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_paths_sdl.c +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c @@ -1,4 +1,4 @@ -#include "../hal_paths.h" +#include "hal/hal_paths.h" #include #include #include diff --git a/projects/APPLaunch/main/hal/sdl/hal_process_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp similarity index 99% rename from projects/APPLaunch/main/hal/sdl/hal_process_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp index 683336b4..4d42e735 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_process_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_process.h" +#include "hal/hal_process.h" #include #include diff --git a/projects/APPLaunch/main/hal/sdl/hal_pty_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp similarity index 99% rename from projects/APPLaunch/main/hal/sdl/hal_pty_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp index f4b04a50..14001acc 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_pty_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_pty.h" +#include "hal/hal_pty.h" #if defined(_WIN32) || defined(__EMSCRIPTEN__) hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int cols, int rows) diff --git a/projects/APPLaunch/main/hal/sdl/hal_screenshot_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp similarity index 82% rename from projects/APPLaunch/main/hal/sdl/hal_screenshot_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp index 5d190de1..e81d14cd 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_screenshot_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_screenshot.h" +#include "hal/hal_screenshot.h" #include int hal_screenshot_save(const char *dir) diff --git a/projects/APPLaunch/main/hal/sdl/hal_settings_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp similarity index 98% rename from projects/APPLaunch/main/hal/sdl/hal_settings_sdl.cpp rename to ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp index 25ce75c0..739f4196 100644 --- a/projects/APPLaunch/main/hal/sdl/hal_settings_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp @@ -1,4 +1,4 @@ -#include "../hal_settings.h" +#include "hal/hal_settings.h" #include #include #include diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c index 48c0ef53..825d933a 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c @@ -1,4 +1,5 @@ #include "hal_lvgl_bsp.h" +#include "commount.h" #include "lvgl/lvgl.h" #if LV_USE_SDL @@ -22,11 +23,6 @@ #define CP0_KBD_MOD_ALT (1u << 2) #define CP0_KBD_MOD_LOGO (1u << 3) -uint32_t LV_C_EVENT_KEYBOARD; -uint32_t LV_C_EVENT_BATTERY; -uint32_t LV_C_EVENT_NETWORK; -uint32_t LV_C_EVENT_DATATIME; - typedef struct { char buf[KEYBOARD_BUFFER_SIZE]; bool dummy_read; @@ -40,14 +36,6 @@ typedef struct { static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data); static void cp0_sdl_keyboard_delete_cb(lv_event_t *event); -void init_lvgl_event(void) -{ - LV_C_EVENT_KEYBOARD = lv_event_register_id(); - LV_C_EVENT_BATTERY = lv_event_register_id(); - LV_C_EVENT_NETWORK = lv_event_register_id(); - LV_C_EVENT_DATATIME = lv_event_register_id(); -} - static const char *getenv_default(const char *name, const char *dflt) { const char *value = getenv(name); @@ -363,7 +351,7 @@ static void cp0_send_keyboard_event(const cp0_key_event_t *event) { lv_obj_t *root = lv_display_get_screen_active(NULL); if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)LV_C_EVENT_KEYBOARD, (void *)event); + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_KEYBOARD], (void *)event); } static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEvent *event) diff --git a/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c b/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c new file mode 100644 index 00000000..c768d49f --- /dev/null +++ b/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c @@ -0,0 +1,98 @@ +#include "hal/hal_audio.h" +#include "hal/hal_config.h" +#include "hal/hal_filesystem.h" +#include "hal/hal_network.h" +#include "hal/hal_paths.h" +#include "hal/hal_process.h" +#include "hal/hal_pty.h" +#include "hal/hal_screenshot.h" +#include "hal/hal_settings.h" + +#include +#include +#include + +void hal_audio_init(void) {} +void hal_audio_play(const char *path) { (void)path; } +void hal_audio_play_sync(const char *path) { (void)path; } +void hal_audio_stop(void) {} +void hal_audio_deinit(void) {} + +void hal_config_init(void) {} +int hal_config_get_int(const char *key, int default_val) { (void)key; return default_val; } +void hal_config_set_int(const char *key, int val) { (void)key; (void)val; } +const char *hal_config_get_str(const char *key, const char *default_val) { (void)key; return default_val; } +void hal_config_set_str(const char *key, const char *val) { (void)key; (void)val; } +void hal_config_save(void) {} + +int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) +{ (void)path; (void)entries; (void)max_entries; *out_count = 0; return -1; } +hal_watcher_t hal_dir_watch_start(const char *path) { (void)path; return 0; } +int hal_dir_watch_poll(hal_watcher_t watcher) { (void)watcher; return 0; } +void hal_dir_watch_stop(hal_watcher_t watcher) { (void)watcher; } + +int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) +{ (void)entries; (void)max_entries; *out_count = 0; return 0; } + +void hal_paths_init(const char *exe_dir) { (void)exe_dir; } +const char *hal_path_data_dir(void) { return "/"; } +const char *hal_path_applications_dir(void) { return "/applications"; } +const char *hal_path_store_cache_dir(void) { return "/store_cache"; } +const char *hal_path_lock_file(void) { return "/tmp/lock"; } +const char *hal_path_font_dir(void) { return "/font"; } +const char *hal_path_font_regular(void) { return "/font/regular.ttf"; } +const char *hal_path_font_mono(void) { return "/font/mono.ttf"; } +const char *hal_path_keyboard_device(void) { return 0; } +const char *hal_path_keyboard_map(void) { return 0; } +const char *hal_path_store_sync_cmd(void) { return ""; } +const char *hal_path_images_dir(void) { return "/images"; } +const char *hal_path_audio_dir(void) { return "/audio"; } + +int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ (void)exec_path; (void)home_key_flag; (void)keep_root; return -1; } +hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) +{ (void)exec_path; (void)keep_root; return -1; } +void hal_process_stop(hal_pid_t pid) { (void)pid; } +int hal_process_check_lock(const char *lock_path, int *holder_pid) +{ (void)lock_path; *holder_pid = 0; return 0; } +void hal_process_kill(int pid, int grace_ms) { (void)pid; (void)grace_ms; } +void hal_system_shutdown(void) {} +void hal_system_reboot(void) {} + +hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int cols, int rows) +{ (void)cmd; (void)args; (void)cols; (void)rows; return 0; } +int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) +{ (void)pty; (void)buf; (void)buf_size; return -1; } +int hal_pty_write(hal_pty_t pty, const char *buf, size_t len) +{ (void)pty; (void)buf; (void)len; return -1; } +int hal_pty_check_child(hal_pty_t pty, int *exit_status) +{ (void)pty; (void)exit_status; return -1; } +void hal_pty_close(hal_pty_t pty) { (void)pty; } + +int hal_screenshot_save(const char *dir) +{ (void)dir; return -1; } + +hal_battery_info_t hal_battery_read(void) +{ hal_battery_info_t info; memset(&info, 0, sizeof(info)); return info; } +int hal_backlight_read(void) { return -1; } +int hal_backlight_max(void) { return 100; } +int hal_backlight_write(int val) { return val; } +int hal_volume_read(void) { return -1; } +int hal_volume_write(int val) { return val; } +hal_wifi_status_t hal_wifi_get_status(void) +{ hal_wifi_status_t st; memset(&st, 0, sizeof(st)); return st; } +int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) { (void)out; (void)max_aps; return 0; } +int hal_wifi_connect(const char *ssid, const char *password) +{ (void)ssid; (void)password; return -1; } +int hal_wifi_disconnect(void) { return 0; } +hal_bt_status_t hal_bt_get_status(void) +{ hal_bt_status_t st; memset(&st, 0, sizeof(st)); return st; } +int hal_bt_set_power(int on) { (void)on; return 0; } +int hal_bt_scan(hal_bt_device_t *out, int max_devices) { (void)out; (void)max_devices; return 0; } +void hal_time_str(char *buf, int buf_size) +{ + time_t now = time(NULL); + struct tm *t = localtime(&now); + if (t) snprintf(buf, buf_size, "%02d:%02d", t->tm_hour, t->tm_min); + else if (buf_size > 0) buf[0] = '\0'; +} diff --git a/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c b/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c new file mode 100644 index 00000000..c768d49f --- /dev/null +++ b/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c @@ -0,0 +1,98 @@ +#include "hal/hal_audio.h" +#include "hal/hal_config.h" +#include "hal/hal_filesystem.h" +#include "hal/hal_network.h" +#include "hal/hal_paths.h" +#include "hal/hal_process.h" +#include "hal/hal_pty.h" +#include "hal/hal_screenshot.h" +#include "hal/hal_settings.h" + +#include +#include +#include + +void hal_audio_init(void) {} +void hal_audio_play(const char *path) { (void)path; } +void hal_audio_play_sync(const char *path) { (void)path; } +void hal_audio_stop(void) {} +void hal_audio_deinit(void) {} + +void hal_config_init(void) {} +int hal_config_get_int(const char *key, int default_val) { (void)key; return default_val; } +void hal_config_set_int(const char *key, int val) { (void)key; (void)val; } +const char *hal_config_get_str(const char *key, const char *default_val) { (void)key; return default_val; } +void hal_config_set_str(const char *key, const char *val) { (void)key; (void)val; } +void hal_config_save(void) {} + +int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) +{ (void)path; (void)entries; (void)max_entries; *out_count = 0; return -1; } +hal_watcher_t hal_dir_watch_start(const char *path) { (void)path; return 0; } +int hal_dir_watch_poll(hal_watcher_t watcher) { (void)watcher; return 0; } +void hal_dir_watch_stop(hal_watcher_t watcher) { (void)watcher; } + +int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) +{ (void)entries; (void)max_entries; *out_count = 0; return 0; } + +void hal_paths_init(const char *exe_dir) { (void)exe_dir; } +const char *hal_path_data_dir(void) { return "/"; } +const char *hal_path_applications_dir(void) { return "/applications"; } +const char *hal_path_store_cache_dir(void) { return "/store_cache"; } +const char *hal_path_lock_file(void) { return "/tmp/lock"; } +const char *hal_path_font_dir(void) { return "/font"; } +const char *hal_path_font_regular(void) { return "/font/regular.ttf"; } +const char *hal_path_font_mono(void) { return "/font/mono.ttf"; } +const char *hal_path_keyboard_device(void) { return 0; } +const char *hal_path_keyboard_map(void) { return 0; } +const char *hal_path_store_sync_cmd(void) { return ""; } +const char *hal_path_images_dir(void) { return "/images"; } +const char *hal_path_audio_dir(void) { return "/audio"; } + +int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ (void)exec_path; (void)home_key_flag; (void)keep_root; return -1; } +hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) +{ (void)exec_path; (void)keep_root; return -1; } +void hal_process_stop(hal_pid_t pid) { (void)pid; } +int hal_process_check_lock(const char *lock_path, int *holder_pid) +{ (void)lock_path; *holder_pid = 0; return 0; } +void hal_process_kill(int pid, int grace_ms) { (void)pid; (void)grace_ms; } +void hal_system_shutdown(void) {} +void hal_system_reboot(void) {} + +hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int cols, int rows) +{ (void)cmd; (void)args; (void)cols; (void)rows; return 0; } +int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) +{ (void)pty; (void)buf; (void)buf_size; return -1; } +int hal_pty_write(hal_pty_t pty, const char *buf, size_t len) +{ (void)pty; (void)buf; (void)len; return -1; } +int hal_pty_check_child(hal_pty_t pty, int *exit_status) +{ (void)pty; (void)exit_status; return -1; } +void hal_pty_close(hal_pty_t pty) { (void)pty; } + +int hal_screenshot_save(const char *dir) +{ (void)dir; return -1; } + +hal_battery_info_t hal_battery_read(void) +{ hal_battery_info_t info; memset(&info, 0, sizeof(info)); return info; } +int hal_backlight_read(void) { return -1; } +int hal_backlight_max(void) { return 100; } +int hal_backlight_write(int val) { return val; } +int hal_volume_read(void) { return -1; } +int hal_volume_write(int val) { return val; } +hal_wifi_status_t hal_wifi_get_status(void) +{ hal_wifi_status_t st; memset(&st, 0, sizeof(st)); return st; } +int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) { (void)out; (void)max_aps; return 0; } +int hal_wifi_connect(const char *ssid, const char *password) +{ (void)ssid; (void)password; return -1; } +int hal_wifi_disconnect(void) { return 0; } +hal_bt_status_t hal_bt_get_status(void) +{ hal_bt_status_t st; memset(&st, 0, sizeof(st)); return st; } +int hal_bt_set_power(int on) { (void)on; return 0; } +int hal_bt_scan(hal_bt_device_t *out, int max_devices) { (void)out; (void)max_devices; return 0; } +void hal_time_str(char *buf, int buf_size) +{ + time_t now = time(NULL); + struct tm *t = localtime(&now); + if (t) snprintf(buf, buf_size, "%02d:%02d", t->tm_hour, t->tm_min); + else if (buf_size > 0) buf[0] = '\0'; +} diff --git a/projects/APPLaunch/config_defaults.mk b/projects/APPLaunch/config_defaults.mk index 2ff378a8..35f99bfd 100644 --- a/projects/APPLaunch/config_defaults.mk +++ b/projects/APPLaunch/config_defaults.mk @@ -89,4 +89,5 @@ CONFIG_V9_5_LV_USE_EVDEV=y CONFIG_V9_5_LV_OS_PTHREAD=y CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 -CONFIG_MINIAUDIO_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y diff --git a/projects/APPLaunch/darwin_config_defaults.mk b/projects/APPLaunch/darwin_config_defaults.mk index e15e4c1c..010396f8 100644 --- a/projects/APPLaunch/darwin_config_defaults.mk +++ b/projects/APPLaunch/darwin_config_defaults.mk @@ -90,4 +90,5 @@ CONFIG_V9_5_LV_FS_POSIX_PATH="./" CONFIG_V9_5_LV_OS_PTHREAD=y CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 -CONFIG_MINIAUDIO_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y diff --git a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk index 03b15c1f..255d511b 100644 --- a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk @@ -94,4 +94,5 @@ CONFIG_V9_5_LV_USE_EVDEV=y CONFIG_V9_5_LV_OS_PTHREAD=y CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 -CONFIG_MINIAUDIO_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y diff --git a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk index 7183ee29..0fb4237e 100644 --- a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk @@ -90,4 +90,5 @@ CONFIG_V9_5_LV_FS_POSIX_PATH="./" CONFIG_V9_5_LV_OS_PTHREAD=y CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 -CONFIG_MINIAUDIO_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y diff --git a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk index 25ad3b23..c8bc93ac 100644 --- a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk @@ -95,4 +95,5 @@ CONFIG_V9_5_LV_USE_EVDEV=y CONFIG_V9_5_LV_OS_PTHREAD=y CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 -CONFIG_MINIAUDIO_COMPONENT_ENABLED=y \ No newline at end of file +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 87cc24f8..507e4a6b 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -79,6 +79,7 @@ SRCS += Glob('src/*.c*') SRCS += append_srcs_dir(ADir('ui')) # add includes INCLUDE += [ADir('.'), ADir('include')] +INCLUDE += [os.path.join(os.environ['EXT_COMPONENTS_PATH'], 'cp0_lvgl', 'include')] # add requirements REQUIREMENTS += ['lvgl_component', 'pthread'] @@ -104,13 +105,12 @@ lvgl_component['SRCS'] = list(filter( lvgl_component['SRCS'] )) -SRCS += Glob('hal/*.c*') # x86 if 'linux_x86_sdl2_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") lvgl_component['REQUIREMENTS'] += pkg_config_ldflags("freetype2") - SRCS += Glob('hal/sdl/*.c*') + REQUIREMENTS += ['cp0_lvgl'] DEFINITIONS += pkg_config_cflags("sdl2") REQUIREMENTS += pkg_config_ldflags("sdl2") REQUIREMENTS += ['input', 'xkbcommon', 'udev'] @@ -123,16 +123,21 @@ if 'linux_x86_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FI f'{rootfs_path}/usr/include/libcamera', ] - SRCS += Glob('hal/linux/*.c*') + REQUIREMENTS += ['cp0_lvgl'] REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'freetype'] REQUIREMENTS += ['camera', 'camera-base', 'jpeg'] LDFLAGS += [f'{rootfs_path}/usr/lib/aarch64-linux-gnu/libstdc++.so.6'] DEFINITIONS += ['-Wno-format-truncation'] + +if os.environ.get("CONFIG_DEFAULT_FILE", '') == 'config_defaults.mk': + REQUIREMENTS += ['cp0_lvgl'] + REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'freetype'] + if 'mac_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] lvgl_component['INCLUDE'] += [f'{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/include/freetype2', f'{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/include/libpng16'] - SRCS += Glob('hal/linux/*.c*') + REQUIREMENTS += ['cp0_lvgl'] REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'freetype'] # add libcamera — note: pkg-config name is 'libcamera', not 'camera'. # Linker still wants -lcamera but we need explicit cflags for the headers. @@ -151,7 +156,7 @@ if 'mac_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ' # macOS if 'darwin_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): # macOS Use the SDL backend - SRCS += Glob('hal/sdl/*.c*') + REQUIREMENTS += ['cp0_lvgl'] # nlohmann_json DEFINITIONS += pkg_config_cflags("nlohmann_json") diff --git a/projects/APPLaunch/main/hal/hal_keyboard.h b/projects/APPLaunch/main/hal/hal_keyboard.h deleted file mode 100644 index 65aaed83..00000000 --- a/projects/APPLaunch/main/hal/hal_keyboard.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -int hal_keyboard_init(void); -void hal_keyboard_deinit(void); - -struct _lv_indev_t *hal_keyboard_get_indev(void); - -extern volatile int HAL_HOME_KEY_FLAG; -extern volatile int HAL_LVGL_RUN_FLAG; -extern volatile uint32_t HAL_LV_EVENT_KEYBOARD; - -#ifdef __cplusplus -} -#endif diff --git a/projects/APPLaunch/main/hal/hal_platform.h b/projects/APPLaunch/main/hal/hal_platform.h deleted file mode 100644 index eaa2ada6..00000000 --- a/projects/APPLaunch/main/hal/hal_platform.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -/* - * Exactly one of these is defined by CMake: - * HAL_PLATFORM_LINUX -- native Raspberry Pi CM Zero - * HAL_PLATFORM_SDL -- macOS / Linux desktop emulator (SDL2) - * HAL_PLATFORM_WIN32 -- Windows MinGW + SDL2 - * HAL_PLATFORM_WEB -- Emscripten + SDL2 - */ - -#if !defined(HAL_PLATFORM_LINUX) && !defined(HAL_PLATFORM_SDL) && \ - !defined(HAL_PLATFORM_WIN32) && !defined(HAL_PLATFORM_WEB) -#error "No HAL platform defined. Set -DHAL_PLATFORM_xxx=1 in CMake." -#endif - -#include -#include -#include diff --git a/projects/APPLaunch/main/hal/web/hal_stubs_web.c b/projects/APPLaunch/main/hal/web/hal_stubs_web.c deleted file mode 100644 index d8a364cb..00000000 --- a/projects/APPLaunch/main/hal/web/hal_stubs_web.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "../hal_paths.h" -#include "../hal_process.h" -#include "../hal_pty.h" -#include "../hal_filesystem.h" -#include "../hal_network.h" - -/* Paths */ -void hal_paths_init(const char *d) { (void)d; } -const char *hal_path_data_dir(void) { return "/"; } -const char *hal_path_applications_dir(void) { return "/applications"; } -const char *hal_path_store_cache_dir(void) { return "/store_cache"; } -const char *hal_path_lock_file(void) { return "/tmp/lock"; } -const char *hal_path_font_dir(void) { return "/font"; } -const char *hal_path_font_regular(void) { return "/font/regular.ttf"; } -const char *hal_path_font_mono(void) { return "/font/mono.ttf"; } -const char *hal_path_keyboard_device(void) { return 0; } -const char *hal_path_keyboard_map(void) { return 0; } -const char *hal_path_store_sync_cmd(void) { return ""; } - -/* Process */ -int hal_process_exec_blocking(const char *p, volatile int *f) { (void)p; (void)f; return -1; } -int hal_process_check_lock(const char *p, int *pid) { (void)p; *pid = 0; return 0; } -void hal_process_kill(int pid, int g) { (void)pid; (void)g; } - -/* PTY */ -hal_pty_t hal_pty_open(const char *c, const char *const *a, int co, int ro) { (void)c; (void)a; (void)co; (void)ro; return 0; } -int hal_pty_read(hal_pty_t p, char *b, size_t s) { (void)p; (void)b; (void)s; return -1; } -int hal_pty_write(hal_pty_t p, const char *b, size_t l) { (void)p; (void)b; (void)l; return -1; } -int hal_pty_check_child(hal_pty_t p, int *e) { (void)p; (void)e; return -1; } -void hal_pty_close(hal_pty_t p) { (void)p; } - -/* Filesystem */ -int hal_dir_list(const char *p, hal_dirent_t *e, int m, int *c) { (void)p; (void)e; (void)m; *c = 0; return -1; } -hal_watcher_t hal_dir_watch_start(const char *p) { (void)p; return 0; } -int hal_dir_watch_poll(hal_watcher_t w) { (void)w; return 0; } -void hal_dir_watch_stop(hal_watcher_t w) { (void)w; } - -/* Network */ -int hal_network_list(hal_netif_info_t *e, int m, int *c) { (void)e; (void)m; *c = 0; return 0; } diff --git a/projects/APPLaunch/main/hal/win32/hal_stubs_win32.c b/projects/APPLaunch/main/hal/win32/hal_stubs_win32.c deleted file mode 100644 index d8a364cb..00000000 --- a/projects/APPLaunch/main/hal/win32/hal_stubs_win32.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "../hal_paths.h" -#include "../hal_process.h" -#include "../hal_pty.h" -#include "../hal_filesystem.h" -#include "../hal_network.h" - -/* Paths */ -void hal_paths_init(const char *d) { (void)d; } -const char *hal_path_data_dir(void) { return "/"; } -const char *hal_path_applications_dir(void) { return "/applications"; } -const char *hal_path_store_cache_dir(void) { return "/store_cache"; } -const char *hal_path_lock_file(void) { return "/tmp/lock"; } -const char *hal_path_font_dir(void) { return "/font"; } -const char *hal_path_font_regular(void) { return "/font/regular.ttf"; } -const char *hal_path_font_mono(void) { return "/font/mono.ttf"; } -const char *hal_path_keyboard_device(void) { return 0; } -const char *hal_path_keyboard_map(void) { return 0; } -const char *hal_path_store_sync_cmd(void) { return ""; } - -/* Process */ -int hal_process_exec_blocking(const char *p, volatile int *f) { (void)p; (void)f; return -1; } -int hal_process_check_lock(const char *p, int *pid) { (void)p; *pid = 0; return 0; } -void hal_process_kill(int pid, int g) { (void)pid; (void)g; } - -/* PTY */ -hal_pty_t hal_pty_open(const char *c, const char *const *a, int co, int ro) { (void)c; (void)a; (void)co; (void)ro; return 0; } -int hal_pty_read(hal_pty_t p, char *b, size_t s) { (void)p; (void)b; (void)s; return -1; } -int hal_pty_write(hal_pty_t p, const char *b, size_t l) { (void)p; (void)b; (void)l; return -1; } -int hal_pty_check_child(hal_pty_t p, int *e) { (void)p; (void)e; return -1; } -void hal_pty_close(hal_pty_t p) { (void)p; } - -/* Filesystem */ -int hal_dir_list(const char *p, hal_dirent_t *e, int m, int *c) { (void)p; (void)e; (void)m; *c = 0; return -1; } -hal_watcher_t hal_dir_watch_start(const char *p) { (void)p; return 0; } -int hal_dir_watch_poll(hal_watcher_t w) { (void)w; return 0; } -void hal_dir_watch_stop(hal_watcher_t w) { (void)w; } - -/* Network */ -int hal_network_list(hal_netif_info_t *e, int m, int *c) { (void)e; (void)m; *c = 0; return 0; } diff --git a/projects/APPLaunch/main/hal/battey.c b/projects/APPLaunch/main/src/app_battery.c similarity index 100% rename from projects/APPLaunch/main/hal/battey.c rename to projects/APPLaunch/main/src/app_battery.c From 521a5800b27b0f80763ab80e4900c0c850b42dfa Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 14:07:09 +0800 Subject: [PATCH 09/70] Migrate APPLaunch to cp0_lvgl app interfaces Add cp0_lvgl_app as an APPLaunch-facing facade for hardware services that were previously consumed through hal_* headers. The facade exposes cp0_* types and functions for settings, paths, filesystem watching, process control, PTY, networking, screenshots, audio, battery, backlight, volume, WiFi, Bluetooth, and time. Switch APPLaunch main/UI code from hal_* includes and symbols to cp0_lvgl_app.h. This removes direct dependency on the legacy hal_* application interface while preserving behavior through cp0_lvgl-backed wrappers. Validated with: CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed from projects/APPLaunch. --- .../cp0_lvgl/include/cp0_lvgl_app.h | 136 ++++++++++++++++++ .../cp0_lvgl/src/cp0/cp0_lvgl_app.cpp | 132 +++++++++++++++++ projects/APPLaunch/main/src/app_battery.c | 2 +- projects/APPLaunch/main/src/main.cpp | 23 ++- .../ui/components/page_app/ui_app_IpPanel.hpp | 6 +- .../ui/components/page_app/ui_app_UnitEnv.hpp | 2 +- .../ui/components/page_app/ui_app_console.hpp | 24 ++-- .../ui/components/page_app/ui_app_hack.hpp | 15 +- .../ui/components/page_app/ui_app_setup.hpp | 105 +++++++------- .../main/ui/components/ui_app_launch.cpp | 29 ++-- .../main/ui/components/ui_app_page.hpp | 20 +-- .../main/ui/components/ui_launch_page.hpp | 4 +- projects/APPLaunch/main/ui/ui.c | 27 ++-- projects/APPLaunch/main/ui/ui.h | 4 +- projects/APPLaunch/main/ui/ui_global_hint.cpp | 4 +- projects/APPLaunch/main/ui/ui_loading.cpp | 2 +- 16 files changed, 395 insertions(+), 140 deletions(-) create mode 100644 ext_components/cp0_lvgl/include/cp0_lvgl_app.h create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h new file mode 100644 index 00000000..6c1cbb5e --- /dev/null +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CP0_WIFI_AP_MAX 32 +#define CP0_WIFI_SSID_MAX 64 +#define CP0_BT_DEVICE_MAX 16 +#define CP0_BT_NAME_MAX 64 + +typedef struct { + int voltage_mv; + int current_ma; + int temperature_c10; + int soc; + int remain_mah; + int full_mah; + int flags; + int avg_current_ma; + int valid; +} cp0_battery_info_t; + +typedef struct { + char ssid[CP0_WIFI_SSID_MAX]; + int signal; + char security[32]; + int in_use; +} cp0_wifi_ap_t; + +typedef struct { + int connected; + char ssid[CP0_WIFI_SSID_MAX]; + char ip[48]; + int signal; +} cp0_wifi_status_t; + +typedef struct { + int powered; + char address[24]; +} cp0_bt_status_t; + +typedef struct { + char name[CP0_BT_NAME_MAX]; + char address[24]; + int rssi; + int connected; +} cp0_bt_device_t; + +typedef struct { + char iface[32]; + char ipv4[16]; + char netmask[16]; + int is_up; +} cp0_netif_info_t; + +typedef struct { + char name[256]; + int is_dir; +} cp0_dirent_t; + +typedef void *cp0_watcher_t; +typedef void *cp0_pty_t; +typedef int cp0_pid_t; + +void cp0_audio_init(void); +void cp0_audio_play(const char *path); +void cp0_audio_play_sync(const char *path); +void cp0_audio_stop(void); +void cp0_audio_deinit(void); + +void cp0_config_init(void); +int cp0_config_get_int(const char *key, int default_val); +void cp0_config_set_int(const char *key, int val); +const char *cp0_config_get_str(const char *key, const char *default_val); +void cp0_config_set_str(const char *key, const char *val); +void cp0_config_save(void); + +void cp0_paths_init(const char *exe_dir); +const char *cp0_path_data_dir(void); +const char *cp0_path_applications_dir(void); +const char *cp0_path_store_cache_dir(void); +const char *cp0_path_lock_file(void); +const char *cp0_path_font_dir(void); +const char *cp0_path_font_regular(void); +const char *cp0_path_font_mono(void); +const char *cp0_path_keyboard_device(void); +const char *cp0_path_keyboard_map(void); +const char *cp0_path_store_sync_cmd(void); +const char *cp0_path_images_dir(void); +const char *cp0_path_audio_dir(void); + +int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count); +cp0_watcher_t cp0_dir_watch_start(const char *path); +int cp0_dir_watch_poll(cp0_watcher_t watcher); +void cp0_dir_watch_stop(cp0_watcher_t watcher); + +int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count); + +int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root); +cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root); +void cp0_process_stop(cp0_pid_t pid); +int cp0_process_check_lock(const char *lock_path, int *holder_pid); +void cp0_process_kill(int pid, int grace_ms); +void cp0_system_shutdown(void); +void cp0_system_reboot(void); + +cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows); +int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size); +int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len); +int cp0_pty_check_child(cp0_pty_t pty, int *exit_status); +void cp0_pty_close(cp0_pty_t pty); + +int cp0_screenshot_save(const char *dir); + +cp0_battery_info_t cp0_battery_read(void); +int cp0_backlight_read(void); +int cp0_backlight_max(void); +int cp0_backlight_write(int val); +int cp0_volume_read(void); +int cp0_volume_write(int val); +cp0_wifi_status_t cp0_wifi_get_status(void); +int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps); +int cp0_wifi_connect(const char *ssid, const char *password); +int cp0_wifi_disconnect(void); +cp0_bt_status_t cp0_bt_get_status(void); +int cp0_bt_set_power(int on); +int cp0_bt_scan(cp0_bt_device_t *out, int max_devices); +void cp0_time_str(char *buf, int buf_size); + +#ifdef __cplusplus +} +#endif diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp new file mode 100644 index 00000000..1c898091 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp @@ -0,0 +1,132 @@ +#include "cp0_lvgl_app.h" +#include "hal/hal_audio.h" +#include "hal/hal_config.h" +#include "hal/hal_filesystem.h" +#include "hal/hal_network.h" +#include "hal/hal_paths.h" +#include "hal/hal_process.h" +#include "hal/hal_pty.h" +#include "hal/hal_screenshot.h" +#include "hal/hal_settings.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include + +static_assert(sizeof(cp0_battery_info_t) == sizeof(hal_battery_info_t), "battery ABI mismatch"); +static_assert(sizeof(cp0_wifi_ap_t) == sizeof(hal_wifi_ap_t), "wifi AP ABI mismatch"); +static_assert(sizeof(cp0_wifi_status_t) == sizeof(hal_wifi_status_t), "wifi status ABI mismatch"); +static_assert(sizeof(cp0_bt_status_t) == sizeof(hal_bt_status_t), "bt status ABI mismatch"); +static_assert(sizeof(cp0_bt_device_t) == sizeof(hal_bt_device_t), "bt device ABI mismatch"); +static_assert(sizeof(cp0_netif_info_t) == sizeof(hal_netif_info_t), "network ABI mismatch"); +static_assert(sizeof(cp0_dirent_t) == sizeof(hal_dirent_t), "dirent ABI mismatch"); + +extern "C" { + +void cp0_audio_init(void) { hal_audio_init(); } +void cp0_audio_play(const char *path) +{ + if (path && path[0]) + cp0_signal_audio_play(std::string(path)); +} +void cp0_audio_play_sync(const char *path) { hal_audio_play_sync(path); } +void cp0_audio_stop(void) { hal_audio_stop(); } +void cp0_audio_deinit(void) { hal_audio_deinit(); } + +void cp0_config_init(void) { hal_config_init(); } +int cp0_config_get_int(const char *key, int default_val) { return hal_config_get_int(key, default_val); } +void cp0_config_set_int(const char *key, int val) { hal_config_set_int(key, val); } +const char *cp0_config_get_str(const char *key, const char *default_val) { return hal_config_get_str(key, default_val); } +void cp0_config_set_str(const char *key, const char *val) { hal_config_set_str(key, val); } +void cp0_config_save(void) { hal_config_save(); } + +void cp0_paths_init(const char *exe_dir) { hal_paths_init(exe_dir); } +const char *cp0_path_data_dir(void) { return hal_path_data_dir(); } +const char *cp0_path_applications_dir(void) { return hal_path_applications_dir(); } +const char *cp0_path_store_cache_dir(void) { return hal_path_store_cache_dir(); } +const char *cp0_path_lock_file(void) { return hal_path_lock_file(); } +const char *cp0_path_font_dir(void) { return hal_path_font_dir(); } +const char *cp0_path_font_regular(void) { return hal_path_font_regular(); } +const char *cp0_path_font_mono(void) { return hal_path_font_mono(); } +const char *cp0_path_keyboard_device(void) { return hal_path_keyboard_device(); } +const char *cp0_path_keyboard_map(void) { return hal_path_keyboard_map(); } +const char *cp0_path_store_sync_cmd(void) { return hal_path_store_sync_cmd(); } +const char *cp0_path_images_dir(void) { return hal_path_images_dir(); } +const char *cp0_path_audio_dir(void) { return hal_path_audio_dir(); } + +int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) +{ + return hal_dir_list(path, reinterpret_cast(entries), max_entries, out_count); +} +cp0_watcher_t cp0_dir_watch_start(const char *path) { return hal_dir_watch_start(path); } +int cp0_dir_watch_poll(cp0_watcher_t watcher) { return hal_dir_watch_poll(reinterpret_cast(watcher)); } +void cp0_dir_watch_stop(cp0_watcher_t watcher) { hal_dir_watch_stop(reinterpret_cast(watcher)); } + +int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) +{ + return hal_network_list(reinterpret_cast(entries), max_entries, out_count); +} + +int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ + return hal_process_exec_blocking(exec_path, home_key_flag, keep_root); +} +cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) { return hal_process_spawn(exec_path, keep_root); } +void cp0_process_stop(cp0_pid_t pid) { hal_process_stop(pid); } +int cp0_process_check_lock(const char *lock_path, int *holder_pid) { return hal_process_check_lock(lock_path, holder_pid); } +void cp0_process_kill(int pid, int grace_ms) { hal_process_kill(pid, grace_ms); } +void cp0_system_shutdown(void) { hal_system_shutdown(); } +void cp0_system_reboot(void) { hal_system_reboot(); } + +cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows) +{ + return hal_pty_open(cmd, args, cols, rows); +} +int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size) { return hal_pty_read(reinterpret_cast(pty), buf, buf_size); } +int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len) { return hal_pty_write(reinterpret_cast(pty), buf, len); } +int cp0_pty_check_child(cp0_pty_t pty, int *exit_status) { return hal_pty_check_child(reinterpret_cast(pty), exit_status); } +void cp0_pty_close(cp0_pty_t pty) { hal_pty_close(reinterpret_cast(pty)); } + +int cp0_screenshot_save(const char *dir) { return hal_screenshot_save(dir); } + +cp0_battery_info_t cp0_battery_read(void) +{ + hal_battery_info_t hal = hal_battery_read(); + cp0_battery_info_t out; + std::memcpy(&out, &hal, sizeof(out)); + return out; +} +int cp0_backlight_read(void) { return hal_backlight_read(); } +int cp0_backlight_max(void) { return hal_backlight_max(); } +int cp0_backlight_write(int val) { return hal_backlight_write(val); } +int cp0_volume_read(void) { return hal_volume_read(); } +int cp0_volume_write(int val) { return hal_volume_write(val); } +cp0_wifi_status_t cp0_wifi_get_status(void) +{ + hal_wifi_status_t hal = hal_wifi_get_status(); + cp0_wifi_status_t out; + std::memcpy(&out, &hal, sizeof(out)); + return out; +} +int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps) +{ + return hal_wifi_scan(reinterpret_cast(out), max_aps); +} +int cp0_wifi_connect(const char *ssid, const char *password) { return hal_wifi_connect(ssid, password); } +int cp0_wifi_disconnect(void) { return hal_wifi_disconnect(); } +cp0_bt_status_t cp0_bt_get_status(void) +{ + hal_bt_status_t hal = hal_bt_get_status(); + cp0_bt_status_t out; + std::memcpy(&out, &hal, sizeof(out)); + return out; +} +int cp0_bt_set_power(int on) { return hal_bt_set_power(on); } +int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) +{ + return hal_bt_scan(reinterpret_cast(out), max_devices); +} +void cp0_time_str(char *buf, int buf_size) { hal_time_str(buf, buf_size); } + +} diff --git a/projects/APPLaunch/main/src/app_battery.c b/projects/APPLaunch/main/src/app_battery.c index c2f1aafe..0b8b1bc2 100644 --- a/projects/APPLaunch/main/src/app_battery.c +++ b/projects/APPLaunch/main/src/app_battery.c @@ -8,7 +8,7 @@ static void _battery_timer_cb(int *workingp) { lv_battery_event_data_t data; memset(&data, 0, sizeof(data)); - data.info = hal_battery_read(); + data.info = cp0_battery_read(); lv_lock(); lv_obj_t *root = lv_screen_active(); if(root) diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index ef761ef5..d49084f9 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -13,9 +13,7 @@ #include "keyboard_input.h" #include "battery.h" #include "compat/input_keys.h" -#include "hal/hal_process.h" -#include "hal/hal_settings.h" -#include "hal/hal_config.h" +#include "cp0_lvgl_app.h" #include "global_config.h" #if CONFIG_BACKWARD_CPP_ENABLED #define BACKWARD_HAS_DW 1 @@ -24,7 +22,6 @@ #endif #include "thpool.h" -#include "hal/hal_paths.h" extern "C" { threadpool g_launch_thread_pool; } @@ -172,8 +169,8 @@ static void keypad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) static void lv_linux_indev_init(void) { const char *mouse_device = getenv_default("LV_LINUX_MOUSE_DEVICE", NULL); - const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", hal_path_keyboard_device()); - const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", hal_path_keyboard_map()); + const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", cp0_path_keyboard_device()); + const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", cp0_path_keyboard_map()); setenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE", keyboard_device, 1); setenv("APPLAUNCH_LINUX_KEYBOARD_MAP", keyboard_map, 1); @@ -271,7 +268,7 @@ void APPLaunch_lock() static std::chrono::time_point start_time; int holder_pid = 0; - hal_process_check_lock(lock_file, &holder_pid); + cp0_process_check_lock(lock_file, &holder_pid); static int lvgl_lock = 0; if (holder_pid == 0) { @@ -289,7 +286,7 @@ void APPLaunch_lock() auto elapsed = std::chrono::steady_clock::now() - start_time; auto secs = std::chrono::duration_cast(elapsed).count(); if (secs >= 5) { - hal_process_kill(holder_pid, 3000); + cp0_process_kill(holder_pid, 3000); home_back_status = 0; } } else { @@ -308,7 +305,7 @@ int main(void) setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", 1); setenv("PULSE_SERVER", "unix:/run/user/1000/pulse/native", 1); - lock_file = hal_path_lock_file(); + lock_file = cp0_path_lock_file(); g_launch_thread_pool = thpool_init(3); lv_init(); printf("[BOOT] lv_init() done\n"); @@ -329,16 +326,16 @@ int main(void) // Restore saved brightness { - int saved_bright = hal_config_get_int("brightness", -1); + int saved_bright = cp0_config_get_int("brightness", -1); if (saved_bright > 0) - hal_backlight_write(saved_bright); + cp0_backlight_write(saved_bright); } // Restore saved volume { - int saved_vol = hal_config_get_int("volume", -1); + int saved_vol = cp0_config_get_int("volume", -1); if (saved_vol >= 0) - hal_volume_write(saved_vol); + cp0_volume_write(saved_vol); } init_audio(); init_camera(); diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp index 60a885ce..39d97c55 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp @@ -6,7 +6,7 @@ #include #include #include -#include "hal/hal_network.h" +#include "cp0_lvgl_app.h" // ============================================================ // IP panel screen UIIpPanelPage @@ -56,9 +56,9 @@ class UIIpPanelPage : public app_base { iface_list_.clear(); - hal_netif_info_t entries[16]; + cp0_netif_info_t entries[16]; int count = 0; - if (hal_network_list(entries, 16, &count) != 0) + if (cp0_network_list(entries, 16, &count) != 0) return; for (int i = 0; i < count; i++) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_UnitEnv.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_UnitEnv.hpp index 5114baf6..98c38eec 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_UnitEnv.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_UnitEnv.hpp @@ -383,7 +383,7 @@ class UIUnitEnvPage : public app_ void on_refresh_timer() { /* - * If the project has hal_time_str, this can use real time. + * If the project has cp0_time_str, this can use real time. * Keep a static date format here. */ static int minute = 10; diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp index 2700ba01..c970307a 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp @@ -14,7 +14,7 @@ #include #include #include -#include "hal/hal_pty.h" +#include "cp0_lvgl_app.h" // ============================================================ // Terminal console UIConsolePage @@ -215,7 +215,7 @@ class UIConsolePage : public app_base bool vt100_skip_until_st = false; /* ── PTY ──────────────────────────────────────────────── */ - hal_pty_t pty_handle = NULL; + cp0_pty_t pty_handle = NULL; lv_timer_t *poll_timer = nullptr; lv_timer_t *cursor_timer = nullptr; @@ -680,7 +680,7 @@ class UIConsolePage : public app_base /* Reply: VT100 (type 0), firmware v10, no options */ fprintf(stderr, "[VT100-DBG] SDA reply: \\033[>0;10;0c\n"); if (pty_handle != NULL) - hal_pty_write(pty_handle, "\033[>0;10;0c", 10); + cp0_pty_write(pty_handle, "\033[>0;10;0c", 10); break; case 'm': /* xterm set-modifyOtherKeys — ignore */ fprintf(stderr, "[VT100-DBG] SDA: set-modifyOtherKeys ignored\n"); @@ -811,14 +811,14 @@ class UIConsolePage : public app_base case 'c': /* DA — Device Attributes: reply with \033[?1;0c (VT100) */ if (pty_handle != NULL) { const char *reply = "\033[?1;0c"; - hal_pty_write(pty_handle, reply, strlen(reply)); + cp0_pty_write(pty_handle, reply, strlen(reply)); } break; case 'n': /* DSR — Device Status Report */ fprintf(stderr, "[VT100-DBG] DSR query param[0]=%d\n", vt100_params[0]); if (vt100_params[0] == 5) { fprintf(stderr, "[VT100-DBG] DSR 5: reply \\033[0n (OK)\n"); - if (pty_handle != NULL) hal_pty_write(pty_handle, "\033[0n", 4); + if (pty_handle != NULL) cp0_pty_write(pty_handle, "\033[0n", 4); } else if (vt100_params[0] == 6) { /* Cursor Position Report */ char buf[32]; @@ -826,7 +826,7 @@ class UIConsolePage : public app_base vt100_cur_row + 1, vt100_cur_col + 1); fprintf(stderr, "[VT100-DBG] DSR 6: cursor=(%d,%d) reply=%s\n", vt100_cur_row + 1, vt100_cur_col + 1, buf); - if (pty_handle != NULL) hal_pty_write(pty_handle, buf, len); + if (pty_handle != NULL) cp0_pty_write(pty_handle, buf, len); } break; @@ -1059,14 +1059,14 @@ class UIConsolePage : public app_base for (const auto &a : args) argv.push_back(a.c_str()); argv.push_back(nullptr); - pty_handle = hal_pty_open(cmd.c_str(), argv.data(), COLS, ROWS); + pty_handle = cp0_pty_open(cmd.c_str(), argv.data(), COLS, ROWS); return pty_handle != NULL; } void stop_pty() { if (pty_handle) { - hal_pty_close(pty_handle); + cp0_pty_close(pty_handle); pty_handle = NULL; } } @@ -1084,7 +1084,7 @@ class UIConsolePage : public app_base int n; bool changed = false; - while ((n = hal_pty_read(pty_handle, buf, sizeof(buf))) > 0) + while ((n = cp0_pty_read(pty_handle, buf, sizeof(buf))) > 0) { vt100_process_bytes(buf, n); changed = true; @@ -1101,7 +1101,7 @@ class UIConsolePage : public app_base else if (pty_handle != NULL) { int status = 0; - if (hal_pty_check_child(pty_handle, &status) == 1) + if (cp0_pty_check_child(pty_handle, &status) == 1) child_exited = true; } @@ -1112,7 +1112,7 @@ class UIConsolePage : public app_base vt100_process_bytes(hint, (int)strlen(hint)); vt100_render_all(); waiting_key_to_exit = true; - hal_pty_close(pty_handle); + cp0_pty_close(pty_handle); pty_handle = NULL; } } @@ -1210,7 +1210,7 @@ class UIConsolePage : public app_base for (int ki = 0; ki < len; ki++) fprintf(stderr, "%02X ", (unsigned char)buf[ki]); fprintf(stderr, "\n"); - hal_pty_write(pty_handle, buf, (size_t)len); + cp0_pty_write(pty_handle, buf, (size_t)len); } } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_hack.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_hack.hpp index c3812638..f79881d4 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_hack.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_hack.hpp @@ -19,8 +19,7 @@ #include #include #endif -#include "hal/hal_settings.h" -#include "hal/hal_network.h" +#include "cp0_lvgl_app.h" #include "compat/input_keys.h" // ============================================================ @@ -68,7 +67,7 @@ class UIHackPage : public app_base static constexpr int LIST_H = 128; // ---- WiFi Scanner state ---- - hal_wifi_ap_t wifi_aps_[WIFI_AP_MAX]; + cp0_wifi_ap_t wifi_aps_[CP0_WIFI_AP_MAX]; int wifi_ap_count_ = 0; int wifi_sel_ = 0; lv_obj_t *wifi_list_cont_ = nullptr; @@ -212,7 +211,7 @@ class UIHackPage : public app_base void wifi_do_scan() { - wifi_ap_count_ = hal_wifi_scan(wifi_aps_, WIFI_AP_MAX); + wifi_ap_count_ = cp0_wifi_scan(wifi_aps_, CP0_WIFI_AP_MAX); wifi_sel_ = 0; wifi_build_ap_rows(); } @@ -236,7 +235,7 @@ class UIHackPage : public app_base for (int vi = 0; vi < visible && (vi + offset) < wifi_ap_count_; ++vi) { int ai = vi + offset; bool sel = (ai == wifi_sel_); - hal_wifi_ap_t *ap = &wifi_aps_[ai]; + cp0_wifi_ap_t *ap = &wifi_aps_[ai]; lv_obj_t *row = lv_obj_create(wifi_list_cont_); lv_obj_set_size(row, 294, 18); @@ -490,9 +489,9 @@ class UIHackPage : public app_base make_label(c, host_buf, 0, 2, 0x58A6FF); // Network interfaces - hal_netif_info_t entries[16]; + cp0_netif_info_t entries[16]; int count = 0; - hal_network_list(entries, 16, &count); + cp0_network_list(entries, 16, &count); if (count == 0) { make_label(c, "No network interfaces found", 0, 22, 0x555555); @@ -513,7 +512,7 @@ class UIHackPage : public app_base } // WiFi status - hal_wifi_status_t ws = hal_wifi_get_status(); + cp0_wifi_status_t ws = cp0_wifi_get_status(); char wifi_buf[128]; if (ws.connected && ws.ip[0]) snprintf(wifi_buf, sizeof(wifi_buf), "WiFi: %s IP: %s Signal: %d%%", diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp index 766ef3c2..3ebc4cb6 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp @@ -1,8 +1,8 @@ #pragma once // Note: this file used to be wrapped in `#if !defined(HAL_PLATFORM_SDL)` to // exclude it from the emulator build, but ui_app_launch.cpp references -// UISetupPage unconditionally. The class body is HAL-clean (uses hal_wifi_*, -// hal_battery_*, hal_volume_*); residual raw syscalls (i2c ioctl, popen for +// UISetupPage unconditionally. The class body is cp0_lvgl-clean (uses cp0_wifi_*, +// cp0_battery_*, cp0_volume_*); residual raw syscalls (i2c ioctl, popen for // IP/whoami, sudo date) are either already inside #ifdef __linux__ or only // triggered by user actions that the emulator never performs. Keeping the // class compiled on every platform lets the emulator open the SETTING page. @@ -31,10 +31,7 @@ #include #include #endif -#include "hal/hal_settings.h" -#include "hal/hal_process.h" -#include "hal/hal_config.h" -#include "hal/hal_audio.h" +#include "cp0_lvgl_app.h" // ============================================================ // System settings screen UISetupPage (Carousel Design) @@ -88,7 +85,7 @@ class UISetupPage : public app_base std::string img_cross_; // WiFi state - hal_wifi_ap_t wifi_aps_[WIFI_AP_MAX]; + cp0_wifi_ap_t wifi_aps_[CP0_WIFI_AP_MAX]; int wifi_ap_count_ = 0; std::string wifi_pw_ssid_; std::string wifi_pw_buf_; @@ -120,8 +117,8 @@ class UISetupPage : public app_base std::string snd_enter_; std::string snd_back_; - void play_enter() { if (!snd_enter_.empty()) hal_audio_play(snd_enter_.c_str()); } - void play_back() { if (!snd_back_.empty()) hal_audio_play(snd_back_.c_str()); } + void play_enter() { if (!snd_enter_.empty()) cp0_audio_play(snd_enter_.c_str()); } + void play_back() { if (!snd_back_.empty()) cp0_audio_play(snd_back_.c_str()); } void cache_image_paths() { @@ -163,7 +160,7 @@ class UISetupPage : public app_base for (int i = 0; i < 27; ++i) { char cfg_key[64]; snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[i]); - bool enabled = hal_config_get_int(cfg_key, 1) != 0; + bool enabled = cp0_config_get_int(cfg_key, 1) != 0; m.sub_items.push_back({app_labels[i], true, enabled, [this, i]() { save_app_toggle(i); }}); } @@ -174,8 +171,8 @@ class UISetupPage : public app_base MenuItem m; m.label = "Boot"; m.sub_items = { - {"Reboot", false, false, [this]() { enter_confirm_action("Reboot?", [this](){ hal_system_reboot(); }); }}, - {"Shutdown", false, false, [this]() { enter_confirm_action("Shutdown?", [this](){ hal_system_shutdown(); }); }}, + {"Reboot", false, false, [this]() { enter_confirm_action("Reboot?", [this](){ cp0_system_reboot(); }); }}, + {"Shutdown", false, false, [this]() { enter_confirm_action("Shutdown?", [this](){ cp0_system_shutdown(); }); }}, }; menu_items_.push_back(m); } @@ -256,18 +253,18 @@ class UISetupPage : public app_base { MenuItem m; m.label = "ExtPort"; - bool usb_en = hal_config_get_int("extport_usb", 1) != 0; - bool vout_en = hal_config_get_int("extport_5vout", 1) != 0; + bool usb_en = cp0_config_get_int("extport_usb", 1) != 0; + bool vout_en = cp0_config_get_int("extport_5vout", 1) != 0; m.sub_items = { {"USB", true, usb_en, [this]() { bool en = menu_items_[7].sub_items[0].toggle_state; - hal_config_set_int("extport_usb", en ? 1 : 0); - hal_config_save(); + cp0_config_set_int("extport_usb", en ? 1 : 0); + cp0_config_save(); }}, {"5VOUT", true, vout_en, [this]() { bool en = menu_items_[7].sub_items[1].toggle_state; - hal_config_set_int("extport_5vout", en ? 1 : 0); - hal_config_save(); + cp0_config_set_int("extport_5vout", en ? 1 : 0); + cp0_config_save(); }}, }; menu_items_.push_back(m); @@ -359,7 +356,7 @@ class UISetupPage : public app_base { val_title_ = "Volume"; val_options_ = {"100%", "75%", "50%", "25%", "0%"}; - vol_val_ = hal_config_get_int("volume", hal_volume_read()); + vol_val_ = cp0_config_get_int("volume", cp0_volume_read()); int pct = vol_val_ * 100 / 63; if (pct >= 87) val_sel_idx_ = 0; else if (pct >= 62) val_sel_idx_ = 1; @@ -383,7 +380,7 @@ class UISetupPage : public app_base { val_title_ = "Startup"; val_options_ = {"Launcher", "CLI"}; - val_sel_idx_ = hal_config_get_int("startup_mode", 0); + val_sel_idx_ = cp0_config_get_int("startup_mode", 0); view_state_ = ViewState::VALUE_SELECT; transition_enter_level(); } @@ -406,7 +403,7 @@ class UISetupPage : public app_base // Title + current connection status lv_obj_t *title = lv_label_create(cont); { - hal_wifi_status_t ws = hal_wifi_get_status(); + cp0_wifi_status_t ws = cp0_wifi_get_status(); static char title_buf[128]; if (ws.connected) snprintf(title_buf, sizeof(title_buf), "Connected WiFi: %s %s", ws.ssid, ws.ip); @@ -456,7 +453,7 @@ class UISetupPage : public app_base for (int vi = 0; vi < visible && (vi + offset) < wifi_ap_count_; ++vi) { int ai = vi + offset; bool sel = (ai == wifi_list_sel_); - hal_wifi_ap_t *ap = &wifi_aps_[ai]; + cp0_wifi_ap_t *ap = &wifi_aps_[ai]; int y = 30 + vi * 22; // Selection highlight @@ -476,7 +473,7 @@ class UISetupPage : public app_base // SSID (append * if we have a saved password for it) lv_obj_t *ssid_lbl = lv_label_create(cont); - static char ssid_buf[WIFI_SSID_MAX + 4]; + static char ssid_buf[CP0_WIFI_SSID_MAX + 4]; if (wifi_has_saved_profile(ap->ssid)) snprintf(ssid_buf, sizeof(ssid_buf), "%s *", ap->ssid); else @@ -547,7 +544,7 @@ class UISetupPage : public app_base { for (auto &m : menu_items_) { if (m.label != "Info") continue; - hal_battery_info_t bat = hal_battery_read(); + cp0_battery_info_t bat = cp0_battery_read(); char buf[64]; snprintf(buf, sizeof(buf), "Battery: %d%%", bat.valid ? bat.soc : 0); m.sub_items[0].label = buf; @@ -649,14 +646,14 @@ class UISetupPage : public app_base char cfg_key[64]; snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); bool enabled = menu_items_[0].sub_items[idx].toggle_state; - hal_config_set_int(cfg_key, enabled ? 1 : 0); - hal_config_save(); + cp0_config_set_int(cfg_key, enabled ? 1 : 0); + cp0_config_save(); } // ==================== Bluetooth ==================== void refresh_bt_status() { - hal_bt_status_t st = hal_bt_get_status(); + cp0_bt_status_t st = cp0_bt_get_status(); // Find Bluetooth menu and update Power toggle state for (auto &m : menu_items_) { if (m.label != "Bluetooth") continue; @@ -670,15 +667,15 @@ class UISetupPage : public app_base for (auto &m : menu_items_) { if (m.label != "Bluetooth") continue; bool on = m.sub_items[0].toggle_state; - hal_bt_set_power(on ? 1 : 0); + cp0_bt_set_power(on ? 1 : 0); break; } } void bt_do_scan() { - hal_bt_device_t devices[BT_DEVICE_MAX]; - hal_bt_scan(devices, BT_DEVICE_MAX); + cp0_bt_device_t devices[CP0_BT_DEVICE_MAX]; + cp0_bt_scan(devices, CP0_BT_DEVICE_MAX); } // ==================== Ethernet ==================== @@ -894,21 +891,21 @@ class UISetupPage : public app_base { // TODO: implement BT scan list page similar to WiFi // For now just trigger scan - hal_bt_device_t devices[BT_DEVICE_MAX]; - int count = hal_bt_scan(devices, BT_DEVICE_MAX); + cp0_bt_device_t devices[CP0_BT_DEVICE_MAX]; + int count = cp0_bt_scan(devices, CP0_BT_DEVICE_MAX); (void)count; } void factory_reset() { remove("/var/lib/applaunch/settings"); - hal_system_reboot(); + cp0_system_reboot(); } // ==================== WiFi functions ==================== void wifi_do_scan() { - wifi_ap_count_ = hal_wifi_scan(wifi_aps_, WIFI_AP_MAX); + wifi_ap_count_ = cp0_wifi_scan(wifi_aps_, CP0_WIFI_AP_MAX); } void wifi_toggle_enable() @@ -925,17 +922,17 @@ class UISetupPage : public app_base void wifi_try_connect(int idx) { if (idx < 0 || idx >= wifi_ap_count_) return; - hal_wifi_ap_t *ap = &wifi_aps_[idx]; + cp0_wifi_ap_t *ap = &wifi_aps_[idx]; if (ap->in_use) return; bool needs_password = false; int ret = -1; if (strcmp(ap->security, "Open") == 0 || ap->security[0] == 0) { wifi_show_connecting(ap->ssid); - ret = hal_wifi_connect(ap->ssid, NULL); + ret = cp0_wifi_connect(ap->ssid, NULL); } else if (wifi_has_saved_profile(ap->ssid)) { wifi_show_connecting(ap->ssid); - ret = hal_wifi_connect(ap->ssid, NULL); + ret = cp0_wifi_connect(ap->ssid, NULL); } else { needs_password = true; wifi_pw_ssid_ = ap->ssid; @@ -982,7 +979,7 @@ class UISetupPage : public app_base void wifi_forget_selected() { if (wifi_list_sel_ < 0 || wifi_list_sel_ >= wifi_ap_count_) return; - hal_wifi_ap_t *ap = &wifi_aps_[wifi_list_sel_]; + cp0_wifi_ap_t *ap = &wifi_aps_[wifi_list_sel_]; if (!wifi_has_saved_profile(ap->ssid)) { wifi_show_error("No saved password for this network"); @@ -1092,7 +1089,7 @@ class UISetupPage : public app_base if (key == KEY_ENTER) { if (pw_hint_lbl_) lv_label_set_text(pw_hint_lbl_, "Connecting..."); lv_refr_now(NULL); - int ret = hal_wifi_connect(wifi_pw_ssid_.c_str(), wifi_pw_buf_.c_str()); + int ret = cp0_wifi_connect(wifi_pw_ssid_.c_str(), wifi_pw_buf_.c_str()); if (ret != 0) { // Connection failed — delete the broken profile that nmcli just // saved with the wrong password, so next attempt won't reuse it. @@ -1137,9 +1134,9 @@ class UISetupPage : public app_base { int pcts[] = {100, 75, 50, 25, 0}; int new_val = 63 * pcts[val_sel_idx_] / 100; - hal_volume_write(new_val); - hal_config_set_int("volume", new_val); - hal_config_save(); + cp0_volume_write(new_val); + cp0_config_set_int("volume", new_val); + cp0_config_save(); } // ==================== Brightness ==================== @@ -1147,8 +1144,8 @@ class UISetupPage : public app_base { val_title_ = "Brightness"; val_options_ = {"100%", "75%", "50%", "25%"}; - bright_val_ = hal_backlight_read(); - int mx = hal_backlight_max(); + bright_val_ = cp0_backlight_read(); + int mx = cp0_backlight_max(); int pct = mx > 0 ? bright_val_ * 100 / mx : 100; if (pct >= 87) val_sel_idx_ = 0; else if (pct >= 62) val_sel_idx_ = 1; @@ -1161,26 +1158,26 @@ class UISetupPage : public app_base void apply_value_selection() { if (val_title_ == "Brightness") { - int mx = hal_backlight_max(); + int mx = cp0_backlight_max(); int pcts[] = {100, 75, 50, 25}; int new_val = mx * pcts[val_sel_idx_] / 100; if (new_val < 1) new_val = 1; - hal_backlight_write(new_val); - hal_config_set_int("brightness", new_val); - hal_config_save(); + cp0_backlight_write(new_val); + cp0_config_set_int("brightness", new_val); + cp0_config_save(); } else if (val_title_ == "Volume") { apply_volume(); } else if (val_title_ == "DarkTime") { // TODO: save dark time setting int times[] = {0, 10, 30, 60, 300}; - hal_config_set_int("dark_time", times[val_sel_idx_]); - hal_config_save(); + cp0_config_set_int("dark_time", times[val_sel_idx_]); + cp0_config_save(); } else if (val_title_ == "Resolution") { - hal_config_set_int("cam_resolution", val_sel_idx_); - hal_config_save(); + cp0_config_set_int("cam_resolution", val_sel_idx_); + cp0_config_save(); } else if (val_title_ == "Startup") { - hal_config_set_int("startup_mode", val_sel_idx_); - hal_config_save(); + cp0_config_set_int("startup_mode", val_sel_idx_); + cp0_config_save(); } else if (val_title_ == "Year" || val_title_ == "Month" || val_title_ == "Day" || val_title_ == "Hour" || val_title_ == "Minute" || val_title_ == "Second") { apply_rtc_value(); diff --git a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp index 6e7a8379..f0d18f96 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp +++ b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp @@ -4,12 +4,7 @@ #include #include #include -#include "hal/hal_paths.h" -#include "hal/hal_filesystem.h" -#include "hal/hal_process.h" -#include "hal/hal_settings.h" -#include "hal/hal_config.h" -#include "hal/hal_audio.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -118,7 +113,7 @@ class app_launch_S { private: int current_app = 2; - hal_watcher_t dir_watcher = NULL; + cp0_watcher_t dir_watcher = NULL; lv_timer_t *watch_timer = nullptr; // LVGL 3s timer lv_timer_t *status_timer = nullptr; // status-bar refresh timer int fixed_count; @@ -174,7 +169,7 @@ class app_launch_S } // Dynamic icons filtered by Settings configuration - #define APP_ENABLED(key) (hal_config_get_int("app_" key, 1) != 0) + #define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) if (APP_ENABLED("Music")) app_list.emplace_back("MUSIC", @@ -319,7 +314,7 @@ class app_launch_S /* Show overlay BEFORE we tear down LVGL input/timers so the user * gets immediate feedback when ENTER was pressed. The overlay * stays drawn on the framebuffer right up until the child takes - * it over via hal_process_exec_blocking(). */ + * it over via cp0_process_exec_blocking(). */ ui_loading_show("Loading..."); lv_disp_t *disp = lv_disp_get_default(); lv_indev_t *indev = lv_indev_get_next(NULL); @@ -329,7 +324,7 @@ class app_launch_S lv_timer_enable(false); lv_refr_now(disp); - int ret = hal_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); + int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); printf("App %s exited with code %d\n", exec.c_str(), ret); lv_timer_enable(true); if (indev) @@ -367,7 +362,7 @@ class app_launch_S void applications_load() { - const char *app_dir = hal_path_applications_dir(); + const char *app_dir = cp0_path_applications_dir(); DIR *dir = opendir(app_dir); if (!dir) { @@ -489,7 +484,7 @@ class app_launch_S // ============================================================ void inotify_init_watch() { - dir_watcher = hal_dir_watch_start(hal_path_applications_dir()); + dir_watcher = cp0_dir_watch_start(cp0_path_applications_dir()); } // ============================================================ @@ -572,7 +567,7 @@ class app_launch_S void update_home_status_bar() { // WiFi signal bars: show/hide + color by strength - hal_wifi_status_t wifi = hal_wifi_get_status(); + cp0_wifi_status_t wifi = cp0_wifi_get_status(); fprintf(stderr, "[HOME_STATUS] connected=%d sig=%d ssid=%s\n", wifi.connected, wifi.signal, wifi.ssid); if (wifi.connected) { @@ -590,11 +585,11 @@ class app_launch_S // Time char time_buf[16]; - hal_time_str(time_buf, sizeof(time_buf)); + cp0_time_str(time_buf, sizeof(time_buf)); lv_label_set_text(ui_timeLabel, time_buf); // Battery - hal_battery_info_t bat = hal_battery_read(); + cp0_battery_info_t bat = cp0_battery_read(); if (bat.valid) { int soc = bat.soc; @@ -623,7 +618,7 @@ class app_launch_S if (!self || !self->dir_watcher) return; - if (hal_dir_watch_poll(self->dir_watcher) > 0) + if (cp0_dir_watch_poll(self->dir_watcher) > 0) { printf("app_dir_watch_cb: applications dir changed, reloading...\n"); self->applications_reload(); @@ -725,7 +720,7 @@ app_launch_S::~app_launch_S() } if (dir_watcher) { - hal_dir_watch_stop(dir_watcher); + cp0_dir_watch_stop(dir_watcher); dir_watcher = NULL; } } diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 9223c503..3938d517 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -14,17 +14,17 @@ #include #include #include -#include "hal/hal_settings.h" +#include "cp0_lvgl_app.h" #define APP_CONSOLE_EXIT_EVENT (lv_event_code_t)(LV_EVENT_LAST + 1) static inline std::string img_path(const char *name) { - return std::string(hal_path_images_dir()) + PATH_SEP + name; + return std::string(cp0_path_images_dir()) + PATH_SEP + name; } static inline std::string audio_path(const char *name) { - return std::string(hal_path_audio_dir()) + PATH_SEP + name; + return std::string(cp0_path_audio_dir()) + PATH_SEP + name; } class app_ @@ -102,7 +102,7 @@ class home_base : public app_ { home_base *self = static_cast(lv_event_get_user_data(e)); if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) return; - const hal_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); + const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); if (bat) self->update_battery_status(*bat); } @@ -115,12 +115,12 @@ class home_base : public app_ void update_status_bar() { char time_buf[16]; - hal_time_str(time_buf, sizeof(time_buf)); + cp0_time_str(time_buf, sizeof(time_buf)); lv_label_set_text(ui_TOP_time_Label, time_buf); } - void update_battery_status(const hal_battery_info_t &bat) + void update_battery_status(const cp0_battery_info_t &bat) { if (bat.valid) { int soc = bat.soc; @@ -250,7 +250,7 @@ class app_base : public app_ { app_base *self = static_cast(lv_event_get_user_data(e)); if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) return; - const hal_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); + const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); if (bat) self->update_battery_status(*bat); } @@ -263,10 +263,10 @@ class app_base : public app_ void update_status_bar() { char time_buf[16]; - hal_time_str(time_buf, sizeof(time_buf)); + cp0_time_str(time_buf, sizeof(time_buf)); lv_label_set_text(ui_TOP_time_Label, time_buf); - hal_wifi_status_t ws = hal_wifi_get_status(); + cp0_wifi_status_t ws = cp0_wifi_get_status(); int sig = ws.connected ? ws.signal : 0; uint32_t on_color = 0x00CCFF; uint32_t off_color = 0x4D4D4D; @@ -283,7 +283,7 @@ class app_base : public app_ lv_color_hex(sig >= 80 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); } - void update_battery_status(const hal_battery_info_t &bat) + void update_battery_status(const cp0_battery_info_t &bat) { if (bat.valid) { int soc = bat.soc; diff --git a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp index cf63743e..55c49786 100644 --- a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp @@ -4,7 +4,7 @@ #include #include #include -#include "hal/hal_process.h" +#include "cp0_lvgl_app.h" // ==================== standard coordinates for 5 slots ==================== static const lv_coord_t LP_SLOT_X[] = {-177, -99, 0, 99, 177, -177, -99, 0, 99, 177 }; @@ -575,7 +575,7 @@ class UILaunchPage : public home_base lv_timer_enable(false); lv_refr_now(disp); - int ret = hal_process_exec_blocking(it->Exec.c_str(), &LVGL_HOME_KEY_FLAG, 0); + int ret = cp0_process_exec_blocking(it->Exec.c_str(), &LVGL_HOME_KEY_FLAG, 0); printf("App %s exited with code %d\n", it->Exec.c_str(), ret); lv_timer_enable(true); if (indev) lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index 380553c9..c0e831c2 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -8,8 +8,7 @@ #include #include #include "lvgl/src/widgets/gif/lv_gif.h" -#include "hal/hal_paths.h" -#include "hal/hal_audio.h" +#include "cp0_lvgl_app.h" ///////////////////// VARIABLES //////////////////// @@ -53,7 +52,7 @@ const char *ui_img_camera_png; static char _img_path_buf[16][256]; static void ui_images_init(void) { - const char *d = hal_path_images_dir(); + const char *d = cp0_path_images_dir(); struct { const char **ptr; const char *name; } tbl[] = { { &ui_img_zero_png, "zero.png" }, { &ui_img_time_png, "time_bg.png" }, @@ -154,7 +153,7 @@ void font_manager_init(void) { static char bold_path[512]; - snprintf(bold_path, sizeof(bold_path), "%s/Montserrat-Bold.ttf", hal_path_font_dir()); + snprintf(bold_path, sizeof(bold_path), "%s/Montserrat-Bold.ttf", cp0_path_font_dir()); g_font_bold_20 = lv_freetype_font_create( bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 18, LV_FREETYPE_FONT_STYLE_BOLD); @@ -186,8 +185,8 @@ void home_screen_load() lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); static char _startup_snd[256]; - snprintf(_startup_snd, sizeof(_startup_snd), "%s/startup.mp3", hal_path_images_dir()); - hal_audio_play(_startup_snd); + snprintf(_startup_snd, sizeof(_startup_snd), "%s/startup.mp3", cp0_path_images_dir()); + cp0_audio_play(_startup_snd); } void audio_system_init(); @@ -222,7 +221,7 @@ void ui_event_logo_over(lv_event_t * e) { static char _gif_path[256]; void start_startup_gif() { - snprintf(_gif_path, sizeof(_gif_path), "%s/logo_output.gif", hal_path_images_dir()); + snprintf(_gif_path, sizeof(_gif_path), "%s/logo_output.gif", cp0_path_images_dir()); startup_gif = lv_gif_create(NULL); lv_gif_set_src(startup_gif, _gif_path); lv_obj_center(startup_gif); @@ -232,10 +231,10 @@ void start_startup_gif() void ui_init(void) { - hal_paths_init(NULL); + cp0_paths_init(NULL); ui_images_init(); - font_path = hal_path_font_regular(); - mono_font_path = hal_path_font_mono(); + font_path = cp0_path_font_regular(); + mono_font_path = cp0_path_font_mono(); font_manager_init(); LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); @@ -264,7 +263,7 @@ void ui_init(void) #else { char gif_check[256]; - snprintf(gif_check, sizeof(gif_check), "%s/logo_output.gif", hal_path_images_dir()); + snprintf(gif_check, sizeof(gif_check), "%s/logo_output.gif", cp0_path_images_dir()); FILE *_gif_f = fopen(gif_check, "r"); if (_gif_f) { fclose(_gif_f); start_startup_gif(); } else { home_screen_load(); } @@ -278,20 +277,20 @@ void ui_init(void) char* cimg_path(const char *name) { static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s%s%s", hal_path_images_dir(), PATH_SEP, name); + snprintf(path_buf, sizeof(path_buf), "%s%s%s", cp0_path_images_dir(), PATH_SEP, name); return path_buf; } char* caudio_path(const char *name) { static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s%s%s", hal_path_audio_dir(), PATH_SEP, name); + snprintf(path_buf, sizeof(path_buf), "%s%s%s", cp0_path_audio_dir(), PATH_SEP, name); return path_buf; } char* cfont_path(const char *name) { static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s%s%s", hal_path_font_dir(), PATH_SEP, name); + snprintf(path_buf, sizeof(path_buf), "%s%s%s", cp0_path_font_dir(), PATH_SEP, name); return path_buf; } \ No newline at end of file diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 5d55f90a..af23e1a6 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -18,7 +18,7 @@ extern "C" { #include "ui_events.h" #include "ui_input_group.h" #include "keyboard_input.h" -#include "hal/hal_settings.h" +#include "cp0_lvgl_app.h" #define lv_mem_alloc lv_malloc #define lv_mem_free lv_free @@ -67,7 +67,7 @@ extern volatile uint32_t LV_EVENT_BATTERY; extern volatile uint32_t LV_EVENT_DELL_CPP_DATA; typedef struct { - hal_battery_info_t info; + cp0_battery_info_t info; } lv_battery_event_data_t; #define LV_EVENT_BATTERY_GET_INFO(e) (&((lv_battery_event_data_t *)lv_event_get_param(e))->info) diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index 2684ad17..6a8ad845 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -25,7 +25,7 @@ #include "ui.h" #include "keyboard_input.h" #include "lvgl/lvgl.h" -#include "hal/hal_screenshot.h" +#include "cp0_lvgl_app.h" #include "compat/input_keys.h" @@ -253,7 +253,7 @@ extern "C" void ui_global_hint_on_key(const struct key_item *elm) if (sudo_gid) gid = (gid_t)atoi(sudo_gid); chown(scr_dir, uid, gid); } - int ret = hal_screenshot_save(scr_dir); + int ret = cp0_screenshot_save(scr_dir); show_hint(ret == 0 ? "Saved to ~/Screenshots" : "Screenshot failed"); return; } diff --git a/projects/APPLaunch/main/ui/ui_loading.cpp b/projects/APPLaunch/main/ui/ui_loading.cpp index 0b253382..ef8e24dd 100644 --- a/projects/APPLaunch/main/ui/ui_loading.cpp +++ b/projects/APPLaunch/main/ui/ui_loading.cpp @@ -17,7 +17,7 @@ * the slow work begins. Otherwise LVGL would only render on the * next frame, which is after the freeze. * - For external (forked) apps, lv_refr_now() is already performed - * by launch_Exec() before hal_process_exec_blocking() — the + * by launch_Exec() before cp0_process_exec_blocking() — the * overlay stays on-screen while the child owns the framebuffer, * which is exactly the desired "something is happening" feedback. */ From 45de5f1512c52ae9ec8d2e01fc983d09f382eb2f Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 14:18:59 +0800 Subject: [PATCH 10/70] Migrate APPLaunch paths to cp0_file_path Route APPLaunch resource, keyboard, lock, and application-directory path lookups through the cp0_file_path facade instead of calling cp0_path_* directly. Add a C-compatible cp0_file_path_c entry point plus a C++ cp0_lvgl_file.hpp declaration, and expand CP0/SDL file path resolution for images, audio, fonts, and launcher-specific special paths. Continue internalizing cp0_lvgl implementations by renaming Linux cp0_hal_* implementation files to cp0_app_* and leaving cp0_lvgl_app.cpp as the async audio integration point. Validated with CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed; APPLaunch main has no cp0_path_* callsites remaining. --- .../cp0_lvgl/include/cp0_lvgl_app.h | 3 + .../cp0_lvgl/include/cp0_lvgl_file.hpp | 5 + .../cp0_lvgl/src/cp0/cp0_app_audio.c | 26 ++++ ...{cp0_hal_config.cpp => cp0_app_config.cpp} | 16 +-- ..._filesystem.cpp => cp0_app_filesystem.cpp} | 22 ++-- ...p0_hal_network.cpp => cp0_app_network.cpp} | 6 +- .../cp0/{cp0_hal_paths.c => cp0_app_paths.c} | 28 ++--- ...p0_hal_process.cpp => cp0_app_process.cpp} | 44 +++---- .../cp0/{cp0_hal_pty.cpp => cp0_app_pty.cpp} | 37 +++--- ..._screenshot.cpp => cp0_app_screenshot.cpp} | 4 +- ..._hal_settings.cpp => cp0_app_settings.cpp} | 56 ++++----- .../cp0_lvgl/src/cp0/cp0_hal_audio.c | 39 ------ .../cp0_lvgl/src/cp0/cp0_lvgl_app.cpp | 118 ------------------ .../cp0_lvgl/src/cp0/cp0_lvgl_file.cpp | 97 ++++++++++---- .../cp0_lvgl/src/sdl/sdl_lvgl_file.cpp | 100 ++++++++++----- projects/APPLaunch/main/src/main.cpp | 11 +- .../main/ui/components/ui_app_launch.cpp | 7 +- .../main/ui/components/ui_app_page.hpp | 5 +- projects/APPLaunch/main/ui/ui.c | 27 ++-- 19 files changed, 318 insertions(+), 333 deletions(-) create mode 100644 ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_config.cpp => cp0_app_config.cpp} (86%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_filesystem.cpp => cp0_app_filesystem.cpp} (64%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_network.cpp => cp0_app_network.cpp} (87%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_paths.c => cp0_app_paths.c} (62%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_process.cpp => cp0_app_process.cpp} (87%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_pty.cpp => cp0_app_pty.cpp} (72%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_screenshot.cpp => cp0_app_screenshot.cpp} (97%) rename ext_components/cp0_lvgl/src/cp0/{cp0_hal_settings.cpp => cp0_app_settings.cpp} (92%) delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index 6c1cbb5e..ccb4d0d0 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -80,6 +80,7 @@ void cp0_config_set_str(const char *key, const char *val); void cp0_config_save(void); void cp0_paths_init(const char *exe_dir); +const char *cp0_file_path_c(const char *file); const char *cp0_path_data_dir(void); const char *cp0_path_applications_dir(void); const char *cp0_path_store_cache_dir(void); @@ -133,4 +134,6 @@ void cp0_time_str(char *buf, int buf_size); #ifdef __cplusplus } +#else +#define cp0_file_path(file) cp0_file_path_c(file) #endif diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp b/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp new file mode 100644 index 00000000..221bb53a --- /dev/null +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string cp0_file_path(std::string file); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c b/ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c new file mode 100644 index 00000000..c780b87d --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c @@ -0,0 +1,26 @@ +#include "cp0_lvgl_app.h" +#include +#include +#include +#include +#include +#include + +void cp0_audio_init(void) {} + +void cp0_audio_play_sync(const char *path) +{ + if (!path || access(path, F_OK) != 0) return; + pid_t pid = fork(); + if (pid == 0) { + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); } + execlp("aplay", "aplay", "-q", path, (char *)NULL); + execlp("mpv", "mpv", "--no-video", "--really-quiet", path, (char *)NULL); + _exit(127); + } + if (pid > 0) waitpid(pid, NULL, 0); +} + +void cp0_audio_stop(void) {} +void cp0_audio_deinit(void) {} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp similarity index 86% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp index fae03728..471fa10d 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_config.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp @@ -1,4 +1,4 @@ -#include "hal/hal_config.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -24,10 +24,10 @@ static void ensure_loaded(void) { if (s_loaded) return; s_loaded = 1; - hal_config_init(); + cp0_config_init(); } -void hal_config_init(void) +void cp0_config_init(void) { s_count = 0; FILE *fp = fopen(CONFIG_FILE, "r"); @@ -57,7 +57,7 @@ static int find_entry(const char *key) return -1; } -int hal_config_get_int(const char *key, int default_val) +int cp0_config_get_int(const char *key, int default_val) { ensure_loaded(); int idx = find_entry(key); @@ -65,7 +65,7 @@ int hal_config_get_int(const char *key, int default_val) return atoi(s_entries[idx].val); } -void hal_config_set_int(const char *key, int val) +void cp0_config_set_int(const char *key, int val) { ensure_loaded(); int idx = find_entry(key); @@ -77,7 +77,7 @@ void hal_config_set_int(const char *key, int val) snprintf(s_entries[idx].val, VAL_MAX, "%d", val); } -const char *hal_config_get_str(const char *key, const char *default_val) +const char *cp0_config_get_str(const char *key, const char *default_val) { ensure_loaded(); int idx = find_entry(key); @@ -85,7 +85,7 @@ const char *hal_config_get_str(const char *key, const char *default_val) return s_entries[idx].val; } -void hal_config_set_str(const char *key, const char *val) +void cp0_config_set_str(const char *key, const char *val) { ensure_loaded(); int idx = find_entry(key); @@ -97,7 +97,7 @@ void hal_config_set_str(const char *key, const char *val) strncpy(s_entries[idx].val, val, VAL_MAX - 1); } -void hal_config_save(void) +void cp0_config_save(void) { mkdir(CONFIG_DIR, 0755); FILE *fp = fopen(CONFIG_FILE, "w"); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp similarity index 64% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp index 52ac25e4..6c5ebbd7 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_filesystem.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp @@ -1,11 +1,11 @@ -#include "hal/hal_filesystem.h" +#include "cp0_lvgl_app.h" #include #include #include #include #include -int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) +int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) { *out_count = 0; DIR *dir = opendir(path); @@ -23,34 +23,36 @@ int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int * return 0; } -struct hal_watcher { +struct cp0_dir_watcher { int inotify_fd; int watch_fd; }; -hal_watcher_t hal_dir_watch_start(const char *path) +cp0_watcher_t cp0_dir_watch_start(const char *path) { int fd = inotify_init1(IN_NONBLOCK); if (fd < 0) return NULL; int wd = inotify_add_watch(fd, path, IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | IN_MOVED_TO); if (wd < 0) { close(fd); return NULL; } - struct hal_watcher *w = (struct hal_watcher *)malloc(sizeof(struct hal_watcher)); + struct cp0_dir_watcher *w = (struct cp0_dir_watcher *)malloc(sizeof(struct cp0_dir_watcher)); w->inotify_fd = fd; w->watch_fd = wd; return w; } -int hal_dir_watch_poll(hal_watcher_t watcher) +int cp0_dir_watch_poll(cp0_watcher_t watcher) { if (!watcher) return -1; char buf[1024] __attribute__((aligned(8))); - ssize_t n = read(watcher->inotify_fd, buf, sizeof(buf)); + struct cp0_dir_watcher *w = (struct cp0_dir_watcher *)watcher; + ssize_t n = read(w->inotify_fd, buf, sizeof(buf)); return (n > 0) ? 1 : 0; } -void hal_dir_watch_stop(hal_watcher_t watcher) +void cp0_dir_watch_stop(cp0_watcher_t watcher) { if (!watcher) return; - close(watcher->inotify_fd); - free(watcher); + struct cp0_dir_watcher *w = (struct cp0_dir_watcher *)watcher; + close(w->inotify_fd); + free(w); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp similarity index 87% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp index b73dc299..00a11b25 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_network.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp @@ -1,10 +1,10 @@ -#include "hal/hal_network.h" +#include "cp0_lvgl_app.h" #include #include #include #include -int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) +int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) { *out_count = 0; struct ifaddrs *ifap = NULL; @@ -15,7 +15,7 @@ int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) if (strcmp(ifa->ifa_name, "lo") == 0) continue; if (*out_count >= max_entries) break; - hal_netif_info_t *e = &entries[*out_count]; + cp0_netif_info_t *e = &entries[*out_count]; strncpy(e->iface, ifa->ifa_name, 31); e->iface[31] = '\0'; struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; inet_ntop(AF_INET, &sa->sin_addr, e->ipv4, sizeof(e->ipv4)); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c b/ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c similarity index 62% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c rename to ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c index 0b6b5aea..a8e88891 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_paths.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c @@ -1,4 +1,4 @@ -#include "hal/hal_paths.h" +#include "cp0_lvgl_app.h" #include #include @@ -23,20 +23,20 @@ static const char *KBD_DEVICE = "/dev/input/by-path/platform-3f804000.i2c- static const char *KBD_MAP = "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; static char s_store_sync_cmd[512] = "python " APP_PREFIX "/bin/store_cache_sync.py"; -void hal_paths_init(const char *exe_dir) +void cp0_paths_init(const char *exe_dir) { (void)exe_dir; } -const char *hal_path_data_dir(void) { return s_data_dir; } -const char *hal_path_applications_dir(void) { return s_applications_dir; } -const char *hal_path_store_cache_dir(void) { return s_store_cache_dir; } -const char *hal_path_lock_file(void) { return s_lock_file; } -const char *hal_path_font_dir(void) { return s_font_dir; } -const char *hal_path_font_regular(void) { return s_font_regular; } -const char *hal_path_font_mono(void) { return s_font_mono; } -const char *hal_path_keyboard_device(void) { return KBD_DEVICE; } -const char *hal_path_keyboard_map(void) { return KBD_MAP; } -const char *hal_path_store_sync_cmd(void) { return s_store_sync_cmd; } -const char *hal_path_images_dir(void) { return s_images_dir; } -const char *hal_path_audio_dir(void) { return s_audio_dir; } \ No newline at end of file +const char *cp0_path_data_dir(void) { return s_data_dir; } +const char *cp0_path_applications_dir(void) { return s_applications_dir; } +const char *cp0_path_store_cache_dir(void) { return s_store_cache_dir; } +const char *cp0_path_lock_file(void) { return s_lock_file; } +const char *cp0_path_font_dir(void) { return s_font_dir; } +const char *cp0_path_font_regular(void) { return s_font_regular; } +const char *cp0_path_font_mono(void) { return s_font_mono; } +const char *cp0_path_keyboard_device(void) { return KBD_DEVICE; } +const char *cp0_path_keyboard_map(void) { return KBD_MAP; } +const char *cp0_path_store_sync_cmd(void) { return s_store_sync_cmd; } +const char *cp0_path_images_dir(void) { return s_images_dir; } +const char *cp0_path_audio_dir(void) { return s_audio_dir; } \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp similarity index 87% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp index 7da6fd5a..f9f065fa 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_process.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp @@ -1,5 +1,5 @@ -#include "hal/hal_process.h" -#include "hal/hal_config.h" +#include "cp0_lvgl_app.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -38,7 +38,7 @@ static bool is_nologin_shell(const char *shell) static const char *get_run_user() { - const char *cfg = hal_config_get_str("run_as_user", NULL); + const char *cfg = cp0_config_get_str("run_as_user", NULL); if (cfg && cfg[0]) return cfg; struct passwd *pwd; @@ -84,7 +84,7 @@ static void exec_as_user(const char *exec_path) * - keyboard_pause() still suspends libinput so APPLauncher's LVGL * keyboard thread doesn't react while the app is in the foreground. * ------------------------------------------------------------------ */ -int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, +int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) { (void)home_key_flag; @@ -93,11 +93,11 @@ int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag int evfd = open(get_kbd_device(), O_RDONLY | O_NONBLOCK); if (evfd < 0) { - perror("[hal] open evdev"); + perror("[cp0] open evdev"); keyboard_resume(); return -1; } - printf("[hal] Opened evdev %s (no EVIOCGRAB; shared with child)\n", get_kbd_device()); + printf("[cp0] Opened evdev %s (no EVIOCGRAB; shared with child)\n", get_kbd_device()); fflush(stdout); pid_t pid = fork(); @@ -133,7 +133,7 @@ int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag const char *st = (ev.value == 1) ? "DOWN" : (ev.value == 0) ? "UP" : (ev.value == 2) ? "REPEAT" : "???"; - printf("[HAL-EXT] evdev code=%u value=%d(%s) (shared, child reads too)\n", + printf("[CP0-APP] evdev code=%u value=%d(%s) (shared, child reads too)\n", ev.code, ev.value, st); fflush(stdout); } @@ -141,11 +141,11 @@ int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag if (ev.value == 1) { esc_down = true; esc_down_since = std::chrono::steady_clock::now(); - printf("[HAL-EXT] ESC DOWN\n"); + printf("[CP0-APP] ESC DOWN\n"); fflush(stdout); } else if (ev.value == 0) { esc_down = false; - printf("[HAL-EXT] ESC UP\n"); + printf("[CP0-APP] ESC UP\n"); fflush(stdout); } } @@ -155,7 +155,7 @@ int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag auto held_ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - esc_down_since).count(); if (held_ms >= ESC_HOLD_SEC * 1000) { - printf("[hal] ESC held %ldms, SIGTERM pgid %d\n", + printf("[cp0] ESC held %ldms, SIGTERM pgid %d\n", (long)held_ms, pid); fflush(stdout); /* Kill the whole process group, not just pid, because @@ -167,7 +167,7 @@ int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag while (waitpid(pid, &status, WNOHANG) == 0) { if (std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count() >= 3) { - printf("[hal] SIGKILL pgid %d\n", pid); + printf("[cp0] SIGKILL pgid %d\n", pid); fflush(stdout); killpg(pid, SIGKILL); waitpid(pid, &status, 0); @@ -186,13 +186,13 @@ int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag keyboard_resume(); - printf("[hal] Returned to launcher\n"); + printf("[cp0] Returned to launcher\n"); fflush(stdout); if (WIFEXITED(status)) return WEXITSTATUS(status); return -1; } -int hal_process_check_lock(const char *lock_path, int *holder_pid) +int cp0_process_check_lock(const char *lock_path, int *holder_pid) { *holder_pid = 0; int fd = open(lock_path, O_CREAT | O_RDWR, 0666); @@ -210,10 +210,10 @@ int hal_process_check_lock(const char *lock_path, int *holder_pid) return 0; } -void hal_process_kill(int pid, int grace_ms) +void cp0_process_kill(int pid, int grace_ms) { if (pid <= 0) return; - /* killpg: hal_process_spawn puts the child in its own pgid, so + /* killpg: cp0_process_spawn puts the child in its own pgid, so * SIGINT/SIGKILL here reaches grandchildren too (sh + exec'd * binary are typically both inside). */ killpg(pid, SIGINT); @@ -231,7 +231,7 @@ void hal_process_kill(int pid, int grace_ms) } } -hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) +cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) { pid_t pid = fork(); if (pid < 0) return -1; @@ -244,10 +244,10 @@ hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) _exit(127); } setpgid(pid, pid); - return (hal_pid_t)pid; + return (cp0_pid_t)pid; } -void hal_process_stop(hal_pid_t pid) +void cp0_process_stop(cp0_pid_t pid) { if (pid <= 0) return; killpg((pid_t)pid, SIGTERM); @@ -255,15 +255,15 @@ void hal_process_stop(hal_pid_t pid) waitpid((pid_t)pid, &status, WNOHANG); } -void hal_system_shutdown(void) +void cp0_system_shutdown(void) { - printf("[HAL] shutdown\n"); + printf("[CP0] shutdown\n"); system("sudo shutdown -h now"); } -void hal_system_reboot(void) +void cp0_system_reboot(void) { - printf("[HAL] reboot\n"); + printf("[CP0] reboot\n"); system("sudo reboot"); } // rebuild trigger diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp similarity index 72% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp index adfd78a6..89fa2f89 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_pty.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp @@ -1,5 +1,4 @@ -#include "hal/hal_pty.h" -#include "hal/hal_config.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -13,12 +12,12 @@ #include #include -struct hal_pty { +struct cp0_pty_handle { int master_fd; pid_t child_pid; }; -hal_pty_t hal_pty_open(const char *cmd, const char *const *args, +cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows) { int master_fd; @@ -35,7 +34,7 @@ hal_pty_t hal_pty_open(const char *cmd, const char *const *args, // Drop to regular user if running as root if (getuid() == 0) { - const char *cfg_user = hal_config_get_str("run_as_user", NULL); + const char *cfg_user = cp0_config_get_str("run_as_user", NULL); const char *username = NULL; if (cfg_user && cfg_user[0]) { username = cfg_user; @@ -78,16 +77,17 @@ hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int flags = fcntl(master_fd, F_GETFL); fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); - struct hal_pty *pty = (struct hal_pty *)malloc(sizeof(struct hal_pty)); + struct cp0_pty_handle *pty = (struct cp0_pty_handle *)malloc(sizeof(struct cp0_pty_handle)); pty->master_fd = master_fd; pty->child_pid = pid; return pty; } -int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) +int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size) { if (!pty) return -1; - ssize_t n = read(pty->master_fd, buf, buf_size); + struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; + ssize_t n = read(h->master_fd, buf, buf_size); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; return -1; @@ -95,17 +95,19 @@ int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) return (int)n; } -int hal_pty_write(hal_pty_t pty, const char *buf, size_t len) +int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len) { if (!pty) return -1; - return (int)write(pty->master_fd, buf, len); + struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; + return (int)write(h->master_fd, buf, len); } -int hal_pty_check_child(hal_pty_t pty, int *exit_status) +int cp0_pty_check_child(cp0_pty_t pty, int *exit_status) { if (!pty) return -1; + struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; int status; - pid_t r = waitpid(pty->child_pid, &status, WNOHANG); + pid_t r = waitpid(h->child_pid, &status, WNOHANG); if (r == 0) return 0; if (r > 0) { if (exit_status) *exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1; @@ -114,11 +116,12 @@ int hal_pty_check_child(hal_pty_t pty, int *exit_status) return -1; } -void hal_pty_close(hal_pty_t pty) +void cp0_pty_close(cp0_pty_t pty) { if (!pty) return; - kill(pty->child_pid, SIGKILL); - waitpid(pty->child_pid, NULL, 0); - close(pty->master_fd); - free(pty); + struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; + kill(h->child_pid, SIGKILL); + waitpid(h->child_pid, NULL, 0); + close(h->master_fd); + free(h); } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp similarity index 97% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp index 5269c8e7..78a006ba 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_screenshot.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp @@ -1,4 +1,4 @@ -#include "hal/hal_screenshot.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -14,7 +14,7 @@ static void write_le16(FILE *f, uint16_t v) { fwrite(&v, 2, 1, f); } static void write_le32(FILE *f, uint32_t v) { fwrite(&v, 4, 1, f); } -int hal_screenshot_save(const char *dir) +int cp0_screenshot_save(const char *dir) { const char *fbdev = getenv("APPLAUNCH_LINUX_FBDEV_DEVICE"); if (!fbdev) fbdev = "/dev/fb0"; diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp similarity index 92% rename from ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp rename to ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp index 134c2202..895b6b55 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_settings.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp @@ -1,4 +1,4 @@ -#include "hal/hal_settings.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -117,9 +117,9 @@ static int bqmon_find_power_supply(char *out, size_t out_len) return 0; } -hal_battery_info_t hal_battery_read(void) +cp0_battery_info_t cp0_battery_read(void) { - hal_battery_info_t info; + cp0_battery_info_t info; memset(&info, 0, sizeof(info)); char bq_path[256] = {0}; @@ -172,7 +172,7 @@ hal_battery_info_t hal_battery_read(void) return info; } -int hal_backlight_read(void) +int cp0_backlight_read(void) { FILE *f = fopen("/sys/class/backlight/backlight/brightness", "r"); if (!f) return -1; @@ -182,7 +182,7 @@ int hal_backlight_read(void) return val; } -int hal_backlight_max(void) +int cp0_backlight_max(void) { FILE *f = fopen("/sys/class/backlight/backlight/max_brightness", "r"); if (!f) return 100; @@ -192,10 +192,10 @@ int hal_backlight_max(void) return val; } -int hal_backlight_write(int val) +int cp0_backlight_write(int val) { if (val < 0) val = 0; - int mx = hal_backlight_max(); + int mx = cp0_backlight_max(); if (val > mx) val = mx; FILE *f = fopen("/sys/class/backlight/backlight/brightness", "w"); if (!f) return -1; @@ -204,7 +204,7 @@ int hal_backlight_write(int val) return val; } -int hal_volume_read(void) +int cp0_volume_read(void) { FILE *p = popen("amixer -c1 sget 'Headphone Playback Volume' 2>/dev/null", "r"); if (!p) return -1; @@ -218,7 +218,7 @@ int hal_volume_read(void) return val; } -int hal_volume_write(int val) +int cp0_volume_write(int val) { if (val < 0) val = 0; if (val > 63) val = 63; @@ -235,14 +235,14 @@ int hal_volume_write(int val) // ── Async WiFi status: background thread polls nmcli, main thread reads cache ── #include -static hal_wifi_status_t s_wifi_cache; +static cp0_wifi_status_t s_wifi_cache; static pthread_mutex_t s_wifi_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_t s_wifi_thread; static int s_wifi_thread_running = 0; -static void wifi_poll_once(hal_wifi_status_t *out) +static void wifi_poll_once(cp0_wifi_status_t *out) { - hal_wifi_status_t st; + cp0_wifi_status_t st; memset(&st, 0, sizeof(st)); char line[256]; @@ -254,7 +254,7 @@ static void wifi_poll_once(hal_wifi_status_t *out) char *name = line + 5; if (name[0] && strcmp(name, "--") != 0) { st.connected = 1; - strncpy(st.ssid, name, WIFI_SSID_MAX - 1); + strncpy(st.ssid, name, CP0_WIFI_SSID_MAX - 1); } break; } @@ -297,7 +297,7 @@ static void *wifi_poll_thread(void *arg) { (void)arg; while (1) { - hal_wifi_status_t st; + cp0_wifi_status_t st; wifi_poll_once(&st); pthread_mutex_lock(&s_wifi_mutex); s_wifi_cache = st; @@ -316,17 +316,17 @@ static void ensure_wifi_thread(void) } } -hal_wifi_status_t hal_wifi_get_status(void) +cp0_wifi_status_t cp0_wifi_get_status(void) { ensure_wifi_thread(); - hal_wifi_status_t st; + cp0_wifi_status_t st; pthread_mutex_lock(&s_wifi_mutex); st = s_wifi_cache; pthread_mutex_unlock(&s_wifi_mutex); return st; } -int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) +int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps) { system("nmcli dev wifi rescan 2>/dev/null"); usleep(500000); @@ -337,7 +337,7 @@ int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) while (fgets(line, sizeof(line), p) && count < max_aps) { line[strcspn(line, "\n")] = 0; if (line[0] == 0) continue; - hal_wifi_ap_t tmp; + cp0_wifi_ap_t tmp; memset(&tmp, 0, sizeof(tmp)); char *ptr = line; char *last_colon = strrchr(ptr, ':'); @@ -353,7 +353,7 @@ int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) tmp.signal = atoi(sig_colon + 1); *sig_colon = 0; if (ptr[0] == 0) continue; - strncpy(tmp.ssid, ptr, WIFI_SSID_MAX - 1); + strncpy(tmp.ssid, ptr, CP0_WIFI_SSID_MAX - 1); /* Dedup: if same SSID already exists, keep the stronger signal, * but always preserve in_use flag (the connected AP might not be @@ -381,7 +381,7 @@ int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) return count; } -int hal_wifi_connect(const char *ssid, const char *password) +int cp0_wifi_connect(const char *ssid, const char *password) { char cmd[512]; if (password && password[0]) @@ -396,7 +396,7 @@ int hal_wifi_connect(const char *ssid, const char *password) return ok ? 0 : -1; } -int hal_wifi_disconnect(void) +int cp0_wifi_disconnect(void) { // Use "nmcli con down" (deactivate connection) rather than "nmcli dev // disconnect" (which marks the device unmanaged and prevents autoconnect @@ -410,9 +410,9 @@ int hal_wifi_disconnect(void) return ok ? 0 : -1; } -hal_bt_status_t hal_bt_get_status(void) +cp0_bt_status_t cp0_bt_get_status(void) { - hal_bt_status_t st; + cp0_bt_status_t st; memset(&st, 0, sizeof(st)); FILE *p = popen("bluetoothctl show 2>/dev/null", "r"); if (!p) return st; @@ -426,7 +426,7 @@ hal_bt_status_t hal_bt_get_status(void) return st; } -int hal_bt_set_power(int on) +int cp0_bt_set_power(int on) { FILE *p = popen(on ? "bluetoothctl power on 2>/dev/null" : "bluetoothctl power off 2>/dev/null", "r"); if (!p) return -1; @@ -436,7 +436,7 @@ int hal_bt_set_power(int on) return ok ? 0 : -1; } -int hal_bt_scan(hal_bt_device_t *out, int max_devices) +int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) { system("bluetoothctl scan on 2>/dev/null &"); usleep(4000000); @@ -457,10 +457,10 @@ int hal_bt_scan(hal_bt_device_t *out, int max_devices) *sp = 0; char *name = sp + 1; - hal_bt_device_t *dev = &out[count]; + cp0_bt_device_t *dev = &out[count]; memset(dev, 0, sizeof(*dev)); strncpy(dev->address, addr, sizeof(dev->address) - 1); - strncpy(dev->name, name[0] ? name : addr, BT_NAME_MAX - 1); + strncpy(dev->name, name[0] ? name : addr, CP0_BT_NAME_MAX - 1); dev->rssi = 0; dev->connected = 0; count++; @@ -469,7 +469,7 @@ int hal_bt_scan(hal_bt_device_t *out, int max_devices) return count; } -void hal_time_str(char *buf, int buf_size) +void cp0_time_str(char *buf, int buf_size) { time_t now = time(NULL); struct tm *t = localtime(&now); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c b/ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c deleted file mode 100644 index 3b137d4f..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_hal_audio.c +++ /dev/null @@ -1,39 +0,0 @@ -#include "hal/hal_audio.h" -#include -#include -#include -#include -#include -#include - -void hal_audio_init(void) {} - -void hal_audio_play(const char *path) -{ - if (!path || access(path, F_OK) != 0) return; - pid_t pid = fork(); - if (pid == 0) { - int devnull = open("/dev/null", O_WRONLY); - if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); } - execlp("aplay", "aplay", "-q", path, (char *)NULL); - execlp("mpv", "mpv", "--no-video", "--really-quiet", path, (char *)NULL); - _exit(127); - } -} - -void hal_audio_play_sync(const char *path) -{ - if (!path || access(path, F_OK) != 0) return; - pid_t pid = fork(); - if (pid == 0) { - int devnull = open("/dev/null", O_WRONLY); - if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); } - execlp("aplay", "aplay", "-q", path, (char *)NULL); - execlp("mpv", "mpv", "--no-video", "--really-quiet", path, (char *)NULL); - _exit(127); - } - if (pid > 0) waitpid(pid, NULL, 0); -} - -void hal_audio_stop(void) {} -void hal_audio_deinit(void) {} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp index 1c898091..b27f1ebc 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp @@ -1,132 +1,14 @@ #include "cp0_lvgl_app.h" -#include "hal/hal_audio.h" -#include "hal/hal_config.h" -#include "hal/hal_filesystem.h" -#include "hal/hal_network.h" -#include "hal/hal_paths.h" -#include "hal/hal_process.h" -#include "hal/hal_pty.h" -#include "hal/hal_screenshot.h" -#include "hal/hal_settings.h" #include "hal_lvgl_bsp.h" -#include -#include #include -static_assert(sizeof(cp0_battery_info_t) == sizeof(hal_battery_info_t), "battery ABI mismatch"); -static_assert(sizeof(cp0_wifi_ap_t) == sizeof(hal_wifi_ap_t), "wifi AP ABI mismatch"); -static_assert(sizeof(cp0_wifi_status_t) == sizeof(hal_wifi_status_t), "wifi status ABI mismatch"); -static_assert(sizeof(cp0_bt_status_t) == sizeof(hal_bt_status_t), "bt status ABI mismatch"); -static_assert(sizeof(cp0_bt_device_t) == sizeof(hal_bt_device_t), "bt device ABI mismatch"); -static_assert(sizeof(cp0_netif_info_t) == sizeof(hal_netif_info_t), "network ABI mismatch"); -static_assert(sizeof(cp0_dirent_t) == sizeof(hal_dirent_t), "dirent ABI mismatch"); - extern "C" { -void cp0_audio_init(void) { hal_audio_init(); } void cp0_audio_play(const char *path) { if (path && path[0]) cp0_signal_audio_play(std::string(path)); } -void cp0_audio_play_sync(const char *path) { hal_audio_play_sync(path); } -void cp0_audio_stop(void) { hal_audio_stop(); } -void cp0_audio_deinit(void) { hal_audio_deinit(); } - -void cp0_config_init(void) { hal_config_init(); } -int cp0_config_get_int(const char *key, int default_val) { return hal_config_get_int(key, default_val); } -void cp0_config_set_int(const char *key, int val) { hal_config_set_int(key, val); } -const char *cp0_config_get_str(const char *key, const char *default_val) { return hal_config_get_str(key, default_val); } -void cp0_config_set_str(const char *key, const char *val) { hal_config_set_str(key, val); } -void cp0_config_save(void) { hal_config_save(); } - -void cp0_paths_init(const char *exe_dir) { hal_paths_init(exe_dir); } -const char *cp0_path_data_dir(void) { return hal_path_data_dir(); } -const char *cp0_path_applications_dir(void) { return hal_path_applications_dir(); } -const char *cp0_path_store_cache_dir(void) { return hal_path_store_cache_dir(); } -const char *cp0_path_lock_file(void) { return hal_path_lock_file(); } -const char *cp0_path_font_dir(void) { return hal_path_font_dir(); } -const char *cp0_path_font_regular(void) { return hal_path_font_regular(); } -const char *cp0_path_font_mono(void) { return hal_path_font_mono(); } -const char *cp0_path_keyboard_device(void) { return hal_path_keyboard_device(); } -const char *cp0_path_keyboard_map(void) { return hal_path_keyboard_map(); } -const char *cp0_path_store_sync_cmd(void) { return hal_path_store_sync_cmd(); } -const char *cp0_path_images_dir(void) { return hal_path_images_dir(); } -const char *cp0_path_audio_dir(void) { return hal_path_audio_dir(); } - -int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) -{ - return hal_dir_list(path, reinterpret_cast(entries), max_entries, out_count); -} -cp0_watcher_t cp0_dir_watch_start(const char *path) { return hal_dir_watch_start(path); } -int cp0_dir_watch_poll(cp0_watcher_t watcher) { return hal_dir_watch_poll(reinterpret_cast(watcher)); } -void cp0_dir_watch_stop(cp0_watcher_t watcher) { hal_dir_watch_stop(reinterpret_cast(watcher)); } - -int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) -{ - return hal_network_list(reinterpret_cast(entries), max_entries, out_count); -} - -int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) -{ - return hal_process_exec_blocking(exec_path, home_key_flag, keep_root); -} -cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) { return hal_process_spawn(exec_path, keep_root); } -void cp0_process_stop(cp0_pid_t pid) { hal_process_stop(pid); } -int cp0_process_check_lock(const char *lock_path, int *holder_pid) { return hal_process_check_lock(lock_path, holder_pid); } -void cp0_process_kill(int pid, int grace_ms) { hal_process_kill(pid, grace_ms); } -void cp0_system_shutdown(void) { hal_system_shutdown(); } -void cp0_system_reboot(void) { hal_system_reboot(); } - -cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows) -{ - return hal_pty_open(cmd, args, cols, rows); -} -int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size) { return hal_pty_read(reinterpret_cast(pty), buf, buf_size); } -int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len) { return hal_pty_write(reinterpret_cast(pty), buf, len); } -int cp0_pty_check_child(cp0_pty_t pty, int *exit_status) { return hal_pty_check_child(reinterpret_cast(pty), exit_status); } -void cp0_pty_close(cp0_pty_t pty) { hal_pty_close(reinterpret_cast(pty)); } - -int cp0_screenshot_save(const char *dir) { return hal_screenshot_save(dir); } - -cp0_battery_info_t cp0_battery_read(void) -{ - hal_battery_info_t hal = hal_battery_read(); - cp0_battery_info_t out; - std::memcpy(&out, &hal, sizeof(out)); - return out; -} -int cp0_backlight_read(void) { return hal_backlight_read(); } -int cp0_backlight_max(void) { return hal_backlight_max(); } -int cp0_backlight_write(int val) { return hal_backlight_write(val); } -int cp0_volume_read(void) { return hal_volume_read(); } -int cp0_volume_write(int val) { return hal_volume_write(val); } -cp0_wifi_status_t cp0_wifi_get_status(void) -{ - hal_wifi_status_t hal = hal_wifi_get_status(); - cp0_wifi_status_t out; - std::memcpy(&out, &hal, sizeof(out)); - return out; -} -int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps) -{ - return hal_wifi_scan(reinterpret_cast(out), max_aps); -} -int cp0_wifi_connect(const char *ssid, const char *password) { return hal_wifi_connect(ssid, password); } -int cp0_wifi_disconnect(void) { return hal_wifi_disconnect(); } -cp0_bt_status_t cp0_bt_get_status(void) -{ - hal_bt_status_t hal = hal_bt_get_status(); - cp0_bt_status_t out; - std::memcpy(&out, &hal, sizeof(out)); - return out; -} -int cp0_bt_set_power(int on) { return hal_bt_set_power(on); } -int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) -{ - return hal_bt_scan(reinterpret_cast(out), max_devices); -} -void cp0_time_str(char *buf, int buf_size) { hal_time_str(buf, buf_size); } } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp index d3affe33..8efac6c8 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp @@ -1,35 +1,82 @@ -#include "hal_lvgl_bsp.h" +#include "cp0_lvgl_app.h" +#include "cp0_lvgl_file.hpp" -#include -#include -#include +#include +#include #include -#include -std::string cp0_file_path(std::string file) +namespace { +constexpr const char *kAppRoot = "/usr/share/APPLaunch"; + +std::string lower_ext(const std::string &file) { - std::regex pattern(R"(\.(png|wav|ttf)$)", std::regex::icase); - std::smatch m; + const auto dot = file.find_last_of('.'); + if (dot == std::string::npos) { + return ""; + } - std::string root_path = "/usr/share/APPLaunch/"; - bool matched = std::regex_search(file, m, pattern); + std::string ext = file.substr(dot + 1); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return ext; +} - if (matched) { - std::string ext = m[1].str(); +bool is_image_ext(const std::string &ext) +{ + return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg" || ext == "svg"; +} - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { - return std::tolower(c); - }); +bool is_audio_ext(const std::string &ext) +{ + return ext == "wav" || ext == "mp3" || ext == "ogg"; +} - if (ext == "png") { - return "share/images/" + file; - } else if (ext == "wav") { - return root_path + "share/audio/" + file; - } else if (ext == "ttf") { - return root_path + "share/font/" + file; - } +bool is_font_ext(const std::string &ext) +{ + return ext == "ttf" || ext == "otf"; +} +} // namespace + +std::string cp0_file_path(std::string file) +{ + if (file.empty()) { + return ""; } - return ""; // 或者 return ""; 或者 throw,根据你的需求 -} \ No newline at end of file + if (file == "applications") { + return std::string(kAppRoot) + "/applications"; + } + if (file == "lock_file") { + return "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; + } + if (file == "keyboard_device") { + return "/dev/input/by-path/platform-3f804000.i2c-event"; + } + if (file == "keyboard_map") { + return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; + } + if (file == "store_sync_cmd") { + return std::string("python ") + kAppRoot + "/bin/store_cache_sync.py"; + } + + const std::string ext = lower_ext(file); + if (is_image_ext(ext)) { + return "share/images/" + file; + } + if (is_audio_ext(ext)) { + return std::string(kAppRoot) + "/share/audio/" + file; + } + if (is_font_ext(ext)) { + return std::string(kAppRoot) + "/share/font/" + file; + } + + return file; +} + +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::string path; + path = cp0_file_path(file ? std::string(file) : std::string()); + return path.c_str(); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp index e4c8567f..f518bda6 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp @@ -1,21 +1,19 @@ -#include "hal_lvgl_bsp.h" +#include "cp0_lvgl_app.h" +#include "cp0_lvgl_file.hpp" -#include -#include -#include -#include -#include #include #include #include +#include #include -static std::string get_app_root_path() +namespace { +std::string get_app_root_path() { char exe_path[PATH_MAX] = {0}; ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); if (len <= 0) { - return "APPLaunch/"; + return "APPLaunch"; } exe_path[len] = '\0'; @@ -23,33 +21,79 @@ static std::string get_app_root_path() size_t slash = path.find_last_of('/'); std::string exe_dir = (slash == std::string::npos) ? "." : path.substr(0, slash); - return exe_dir + "/APPLaunch/"; + return exe_dir + "/APPLaunch"; } -std::string cp0_file_path(std::string file) +std::string lower_ext(const std::string &file) { - std::regex pattern(R"(\.(png|wav|ttf)$)", std::regex::icase); - std::smatch m; + const auto dot = file.find_last_of('.'); + if (dot == std::string::npos) { + return ""; + } - std::string root_path = get_app_root_path(); - bool matched = std::regex_search(file, m, pattern); + std::string ext = file.substr(dot + 1); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return ext; +} + +bool is_image_ext(const std::string &ext) +{ + return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg" || ext == "svg"; +} - if (matched) { - std::string ext = m[1].str(); +bool is_audio_ext(const std::string &ext) +{ + return ext == "wav" || ext == "mp3" || ext == "ogg"; +} - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { - return std::tolower(c); - }); +bool is_font_ext(const std::string &ext) +{ + return ext == "ttf" || ext == "otf"; +} +} // namespace - if (ext == "png") { - return "share/images/" + file; - } else if (ext == "wav") { - return root_path + "share/audio/" + file; - } else if (ext == "ttf") { - return root_path + "share/font/" + file; - } +std::string cp0_file_path(std::string file) +{ + if (file.empty()) { + return ""; } - return ""; // 或者 return ""; 或者 throw,根据你的需求 + const std::string root_path = get_app_root_path(); + if (file == "applications") { + return root_path + "/applications"; + } + if (file == "lock_file") { + return "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; + } + if (file == "keyboard_device") { + return "/dev/input/by-path/platform-3f804000.i2c-event"; + } + if (file == "keyboard_map") { + return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; + } + if (file == "store_sync_cmd") { + return "python " + root_path + "/bin/store_cache_sync.py"; + } + + const std::string ext = lower_ext(file); + if (is_image_ext(ext)) { + return "share/images/" + file; + } + if (is_audio_ext(ext)) { + return root_path + "/share/audio/" + file; + } + if (is_font_ext(ext)) { + return root_path + "/share/font/" + file; + } + + return file; +} + +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::string path; + path = cp0_file_path(file ? std::string(file) : std::string()); + return path.c_str(); } diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index d49084f9..77015b02 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -8,12 +8,14 @@ #include #include #include +#include #include "ui/ui.h" #include "ui/ui_global_hint.h" #include "keyboard_input.h" #include "battery.h" #include "compat/input_keys.h" #include "cp0_lvgl_app.h" +#include "cp0_lvgl_file.hpp" #include "global_config.h" #if CONFIG_BACKWARD_CPP_ENABLED #define BACKWARD_HAS_DW 1 @@ -169,8 +171,10 @@ static void keypad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) static void lv_linux_indev_init(void) { const char *mouse_device = getenv_default("LV_LINUX_MOUSE_DEVICE", NULL); - const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", cp0_path_keyboard_device()); - const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", cp0_path_keyboard_map()); + const std::string default_keyboard_device = cp0_file_path("keyboard_device"); + const std::string default_keyboard_map = cp0_file_path("keyboard_map"); + const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", default_keyboard_device.c_str()); + const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", default_keyboard_map.c_str()); setenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE", keyboard_device, 1); setenv("APPLAUNCH_LINUX_KEYBOARD_MAP", keyboard_map, 1); @@ -305,7 +309,8 @@ int main(void) setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", 1); setenv("PULSE_SERVER", "unix:/run/user/1000/pulse/native", 1); - lock_file = cp0_path_lock_file(); + static const std::string default_lock_file = cp0_file_path("lock_file"); + lock_file = default_lock_file.c_str(); g_launch_thread_pool = thpool_init(3); lv_init(); printf("[BOOT] lv_init() done\n"); diff --git a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp index f0d18f96..08a452b6 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp +++ b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp @@ -5,6 +5,7 @@ #include #include #include "cp0_lvgl_app.h" +#include "cp0_lvgl_file.hpp" #include #include #include @@ -362,7 +363,8 @@ class app_launch_S void applications_load() { - const char *app_dir = cp0_path_applications_dir(); + const std::string app_dir_path = cp0_file_path("applications"); + const char *app_dir = app_dir_path.c_str(); DIR *dir = opendir(app_dir); if (!dir) { @@ -484,7 +486,8 @@ class app_launch_S // ============================================================ void inotify_init_watch() { - dir_watcher = cp0_dir_watch_start(cp0_path_applications_dir()); + const std::string app_dir_path = cp0_file_path("applications"); + dir_watcher = cp0_dir_watch_start(app_dir_path.c_str()); } // ============================================================ diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 3938d517..b669d4fe 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -15,16 +15,17 @@ #include #include #include "cp0_lvgl_app.h" +#include "cp0_lvgl_file.hpp" #define APP_CONSOLE_EXIT_EVENT (lv_event_code_t)(LV_EVENT_LAST + 1) static inline std::string img_path(const char *name) { - return std::string(cp0_path_images_dir()) + PATH_SEP + name; + return cp0_file_path(name); } static inline std::string audio_path(const char *name) { - return std::string(cp0_path_audio_dir()) + PATH_SEP + name; + return cp0_file_path(name); } class app_ diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index c0e831c2..6b821db3 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -52,7 +52,6 @@ const char *ui_img_camera_png; static char _img_path_buf[16][256]; static void ui_images_init(void) { - const char *d = cp0_path_images_dir(); struct { const char **ptr; const char *name; } tbl[] = { { &ui_img_zero_png, "zero.png" }, { &ui_img_time_png, "time_bg.png" }, @@ -69,7 +68,7 @@ static void ui_images_init(void) }; int n = sizeof(tbl) / sizeof(tbl[0]); for (int i = 0; i < n && i < 16; i++) { - snprintf(_img_path_buf[i], sizeof(_img_path_buf[i]), "%s/%s", d, tbl[i].name); + snprintf(_img_path_buf[i], sizeof(_img_path_buf[i]), "%s", cp0_file_path(tbl[i].name)); *tbl[i].ptr = _img_path_buf[i]; } } @@ -153,7 +152,7 @@ void font_manager_init(void) { static char bold_path[512]; - snprintf(bold_path, sizeof(bold_path), "%s/Montserrat-Bold.ttf", cp0_path_font_dir()); + snprintf(bold_path, sizeof(bold_path), "%s", cp0_file_path("Montserrat-Bold.ttf")); g_font_bold_20 = lv_freetype_font_create( bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 18, LV_FREETYPE_FONT_STYLE_BOLD); @@ -185,7 +184,7 @@ void home_screen_load() lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); static char _startup_snd[256]; - snprintf(_startup_snd, sizeof(_startup_snd), "%s/startup.mp3", cp0_path_images_dir()); + snprintf(_startup_snd, sizeof(_startup_snd), "%s", cp0_file_path("startup.mp3")); cp0_audio_play(_startup_snd); } @@ -221,7 +220,7 @@ void ui_event_logo_over(lv_event_t * e) { static char _gif_path[256]; void start_startup_gif() { - snprintf(_gif_path, sizeof(_gif_path), "%s/logo_output.gif", cp0_path_images_dir()); + snprintf(_gif_path, sizeof(_gif_path), "%s", cp0_file_path("logo_output.gif")); startup_gif = lv_gif_create(NULL); lv_gif_set_src(startup_gif, _gif_path); lv_obj_center(startup_gif); @@ -233,8 +232,12 @@ void ui_init(void) { cp0_paths_init(NULL); ui_images_init(); - font_path = cp0_path_font_regular(); - mono_font_path = cp0_path_font_mono(); + static char regular_font_path[512]; + static char mono_font_path_buf[512]; + snprintf(regular_font_path, sizeof(regular_font_path), "%s", cp0_file_path("AlibabaPuHuiTi-3-55-Regular.ttf")); + snprintf(mono_font_path_buf, sizeof(mono_font_path_buf), "%s", cp0_file_path("LiberationMono-Regular.ttf")); + font_path = regular_font_path; + mono_font_path = mono_font_path_buf; font_manager_init(); LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); @@ -263,7 +266,7 @@ void ui_init(void) #else { char gif_check[256]; - snprintf(gif_check, sizeof(gif_check), "%s/logo_output.gif", cp0_path_images_dir()); + snprintf(gif_check, sizeof(gif_check), "%s", cp0_file_path("logo_output.gif")); FILE *_gif_f = fopen(gif_check, "r"); if (_gif_f) { fclose(_gif_f); start_startup_gif(); } else { home_screen_load(); } @@ -277,20 +280,20 @@ void ui_init(void) char* cimg_path(const char *name) { static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s%s%s", cp0_path_images_dir(), PATH_SEP, name); + snprintf(path_buf, sizeof(path_buf), "%s", cp0_file_path(name)); return path_buf; } char* caudio_path(const char *name) { static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s%s%s", cp0_path_audio_dir(), PATH_SEP, name); + snprintf(path_buf, sizeof(path_buf), "%s", cp0_file_path(name)); return path_buf; } char* cfont_path(const char *name) { static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s%s%s", cp0_path_font_dir(), PATH_SEP, name); + snprintf(path_buf, sizeof(path_buf), "%s", cp0_file_path(name)); return path_buf; -} \ No newline at end of file +} From b0c958ecbc7a6be5dfcbdbf1f86194731a9ed6e0 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 14:24:10 +0800 Subject: [PATCH 11/70] Remove legacy cp0 app path and audio facades Delete unused cp0_app_paths and remove the cp0_path_* declarations after APPLaunch moved path resolution to cp0_file_path. Delete unused cp0_app_audio and remove the cp0_audio_init/play/play_sync/stop/deinit facade declarations. Route APPLaunch audio playback through cp0_signal_audio_api using PlayFile, with a small C wrapper for C callsites. Validated with CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed and residual cp0_audio_*/cp0_path_* searches. --- .../cp0_lvgl/include/cp0_lvgl_app.h | 19 +-------- .../cp0_lvgl/src/cp0/cp0_app_audio.c | 26 ------------ .../cp0_lvgl/src/cp0/cp0_app_paths.c | 42 ------------------- .../cp0_lvgl/src/cp0/cp0_lvgl_app.cpp | 8 ++-- .../ui/components/page_app/ui_app_setup.hpp | 10 ++++- projects/APPLaunch/main/ui/ui.c | 3 +- 6 files changed, 15 insertions(+), 93 deletions(-) delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index ccb4d0d0..2e6f7aa3 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -66,11 +66,7 @@ typedef void *cp0_watcher_t; typedef void *cp0_pty_t; typedef int cp0_pid_t; -void cp0_audio_init(void); -void cp0_audio_play(const char *path); -void cp0_audio_play_sync(const char *path); -void cp0_audio_stop(void); -void cp0_audio_deinit(void); +void cp0_signal_audio_api_play_file(const char *path); void cp0_config_init(void); int cp0_config_get_int(const char *key, int default_val); @@ -79,20 +75,7 @@ const char *cp0_config_get_str(const char *key, const char *default_val); void cp0_config_set_str(const char *key, const char *val); void cp0_config_save(void); -void cp0_paths_init(const char *exe_dir); const char *cp0_file_path_c(const char *file); -const char *cp0_path_data_dir(void); -const char *cp0_path_applications_dir(void); -const char *cp0_path_store_cache_dir(void); -const char *cp0_path_lock_file(void); -const char *cp0_path_font_dir(void); -const char *cp0_path_font_regular(void); -const char *cp0_path_font_mono(void); -const char *cp0_path_keyboard_device(void); -const char *cp0_path_keyboard_map(void); -const char *cp0_path_store_sync_cmd(void); -const char *cp0_path_images_dir(void); -const char *cp0_path_audio_dir(void); int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count); cp0_watcher_t cp0_dir_watch_start(const char *path); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c b/ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c deleted file mode 100644 index c780b87d..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_audio.c +++ /dev/null @@ -1,26 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include -#include - -void cp0_audio_init(void) {} - -void cp0_audio_play_sync(const char *path) -{ - if (!path || access(path, F_OK) != 0) return; - pid_t pid = fork(); - if (pid == 0) { - int devnull = open("/dev/null", O_WRONLY); - if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); } - execlp("aplay", "aplay", "-q", path, (char *)NULL); - execlp("mpv", "mpv", "--no-video", "--really-quiet", path, (char *)NULL); - _exit(127); - } - if (pid > 0) waitpid(pid, NULL, 0); -} - -void cp0_audio_stop(void) {} -void cp0_audio_deinit(void) {} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c b/ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c deleted file mode 100644 index a8e88891..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_paths.c +++ /dev/null @@ -1,42 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include - -/* - * LVGL FS POSIX driver root = "/usr/share/APPLaunch/" (letter 'A'). - * Image paths must be RELATIVE (e.g. "share/images/foo.png") so LVGL - * resolves them as "A:share/images/foo.png" → "/usr/share/APPLaunch/share/images/foo.png". - * Font paths need ABSOLUTE paths because freetype opens them directly via fopen(). - */ -#define APP_PREFIX "/usr/share/APPLaunch" - -static char s_data_dir[512] = APP_PREFIX; -static char s_applications_dir[512] = APP_PREFIX "/applications"; -static char s_store_cache_dir[512] = "/var/cache/APPLaunch/store"; -static char s_lock_file[512] = "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; -static char s_font_dir[512] = APP_PREFIX "/share/font"; -static char s_font_regular[512] = APP_PREFIX "/share/font/AlibabaPuHuiTi-3-55-Regular.ttf"; -static char s_font_mono[512] = APP_PREFIX "/share/font/LiberationMono-Regular.ttf"; -static char s_images_dir[512] = "share/images"; -static char s_audio_dir[512] = "/usr/share/APPLaunch/share/audio"; -static const char *KBD_DEVICE = "/dev/input/by-path/platform-3f804000.i2c-event"; -static const char *KBD_MAP = "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; -static char s_store_sync_cmd[512] = "python " APP_PREFIX "/bin/store_cache_sync.py"; - -void cp0_paths_init(const char *exe_dir) -{ - (void)exe_dir; -} - -const char *cp0_path_data_dir(void) { return s_data_dir; } -const char *cp0_path_applications_dir(void) { return s_applications_dir; } -const char *cp0_path_store_cache_dir(void) { return s_store_cache_dir; } -const char *cp0_path_lock_file(void) { return s_lock_file; } -const char *cp0_path_font_dir(void) { return s_font_dir; } -const char *cp0_path_font_regular(void) { return s_font_regular; } -const char *cp0_path_font_mono(void) { return s_font_mono; } -const char *cp0_path_keyboard_device(void) { return KBD_DEVICE; } -const char *cp0_path_keyboard_map(void) { return KBD_MAP; } -const char *cp0_path_store_sync_cmd(void) { return s_store_sync_cmd; } -const char *cp0_path_images_dir(void) { return s_images_dir; } -const char *cp0_path_audio_dir(void) { return s_audio_dir; } \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp index b27f1ebc..85f3b39d 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp @@ -1,14 +1,16 @@ #include "cp0_lvgl_app.h" #include "hal_lvgl_bsp.h" +#include #include extern "C" { -void cp0_audio_play(const char *path) +void cp0_signal_audio_api_play_file(const char *path) { - if (path && path[0]) - cp0_signal_audio_play(std::string(path)); + if (path && path[0]) { + cp0_signal_audio_api({"PlayFile", std::string(path)}, nullptr); + } } } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp index 3ebc4cb6..8cd91f74 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp @@ -32,6 +32,7 @@ #include #endif #include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" // ============================================================ // System settings screen UISetupPage (Carousel Design) @@ -117,8 +118,13 @@ class UISetupPage : public app_base std::string snd_enter_; std::string snd_back_; - void play_enter() { if (!snd_enter_.empty()) cp0_audio_play(snd_enter_.c_str()); } - void play_back() { if (!snd_back_.empty()) cp0_audio_play(snd_back_.c_str()); } + void play_enter() { play_audio_file(snd_enter_); } + void play_back() { play_audio_file(snd_back_); } + + void play_audio_file(const std::string &path) + { + if (!path.empty()) cp0_signal_audio_api({"PlayFile", path}, nullptr); + } void cache_image_paths() { diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index 6b821db3..ef9ae0cf 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -185,7 +185,7 @@ void home_screen_load() static char _startup_snd[256]; snprintf(_startup_snd, sizeof(_startup_snd), "%s", cp0_file_path("startup.mp3")); - cp0_audio_play(_startup_snd); + cp0_signal_audio_api_play_file(_startup_snd); } void audio_system_init(); @@ -230,7 +230,6 @@ void start_startup_gif() void ui_init(void) { - cp0_paths_init(NULL); ui_images_init(); static char regular_font_path[512]; static char mono_font_path_buf[512]; From b612dc7bfe26c6116fa7ca218490184df64acf19 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 14:41:56 +0800 Subject: [PATCH 12/70] Move keyboard input facades into cp0 lvgl backends Fold the APPLaunch keyboard_input compatibility surface into the real cp0_lvgl keyboard backends so the application can keep using LV_EVENT_KEYBOARD, struct key_item, keyboard_read_thread(), kbd_state_name(), LVGL_HOME_KEY_FLAG, and LVGL_RUN_FLAGE without the standalone cp0_keyboard_input.c file. The cp0 backend now owns the Linux libinput/xkb/TCA8418/repeat implementation plus pause/resume handling. The SDL backend now emits the same struct key_item payload through LV_EVENT_KEYBOARD while retaining SDL text/control key handling, so APPLaunch key consumers do not need to change type or event channel yet. Validation: CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed passed. Also attempted CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed; SDL sources compiled, but final link is blocked by missing host/target SDL2 library (-lSDL2). --- .../cp0_lvgl/src/cp0/cp0_keyboard_input.c | 1065 ----------------- .../cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c | 1010 ++++++++++------ .../cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c | 77 +- 3 files changed, 685 insertions(+), 1467 deletions(-) delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c b/ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c deleted file mode 100644 index a89c30ac..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_keyboard_input.c +++ /dev/null @@ -1,1065 +0,0 @@ -// keyboard_input.c -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#ifdef __linux__ -#include -#include -#endif -#include -#include -#ifdef __linux__ -#include -#include -#include -#include -#include -#include -#else -#include "compat/input_keys.h" -#endif -#include "keyboard_input.h" -#include "lvgl/lvgl.h" - -/* ============================================================ - * Global queue - * ============================================================ */ -struct keyboard_queue_t keyboard_queue; -pthread_mutex_t keyboard_mutex = PTHREAD_MUTEX_INITIALIZER; -volatile int LVGL_HOME_KEY_FLAG = 0; -volatile int LVGL_RUN_FLAGE = 1; -volatile uint32_t LV_EVENT_KEYBOARD; - -static volatile int keyboard_paused_flag = 0; -#ifdef __linux__ -static struct libinput *g_libinput = NULL; -void keyboard_pause(void) { - keyboard_paused_flag = 1; - if (g_libinput) libinput_suspend(g_libinput); - printf("[KBD] keyboard_pause()\n"); -} -void keyboard_resume(void) { - if (g_libinput) libinput_resume(g_libinput); - keyboard_paused_flag = 0; - printf("[KBD] keyboard_resume()\n"); -} -#else -void keyboard_pause(void) { keyboard_paused_flag = 1; } -void keyboard_resume(void) { keyboard_paused_flag = 0; } -#endif - -/* ============================================================ - * Debug: key_state name - * ============================================================ */ -const char *kbd_state_name(int state) -{ - switch (state) { - case 0: return "UP"; - case 1: return "DOWN"; - case 2: return "REPEAT"; - default: return "???"; - } -} - -/* ============================================================ - * Debug: dump all ASCII printable + letter + digit mappings once - * Call on startup so we can see how each key_code maps to utf8. - * ============================================================ */ -#ifdef __linux__ -void kbd_dump_keymap_table(void) -{ - /* Linux evdev KEY_* values we care about: 2..53 covers 1..0 qwerty zxcvbnm */ - static const struct { uint32_t code; const char *name; } keys[] = { - {KEY_1,"1"},{KEY_2,"2"},{KEY_3,"3"},{KEY_4,"4"},{KEY_5,"5"}, - {KEY_6,"6"},{KEY_7,"7"},{KEY_8,"8"},{KEY_9,"9"},{KEY_0,"0"}, - {KEY_Q,"q"},{KEY_W,"w"},{KEY_E,"e"},{KEY_R,"r"},{KEY_T,"t"}, - {KEY_Y,"y"},{KEY_U,"u"},{KEY_I,"i"},{KEY_O,"o"},{KEY_P,"p"}, - {KEY_A,"a"},{KEY_S,"s"},{KEY_D,"d"},{KEY_F,"f"},{KEY_G,"g"}, - {KEY_H,"h"},{KEY_J,"j"},{KEY_K,"k"},{KEY_L,"l"}, - {KEY_Z,"z"},{KEY_X,"x"},{KEY_C,"c"},{KEY_V,"v"},{KEY_B,"b"}, - {KEY_N,"n"},{KEY_M,"m"}, - {KEY_MINUS,"-"},{KEY_EQUAL,"="},{KEY_LEFTBRACE,"["},{KEY_RIGHTBRACE,"]"}, - {KEY_SEMICOLON,";"},{KEY_APOSTROPHE,"'"},{KEY_GRAVE,"`"}, - {KEY_BACKSLASH,"\\"},{KEY_COMMA,","},{KEY_DOT,"."},{KEY_SLASH,"/"}, - {KEY_SPACE,"SPACE"},{KEY_ENTER,"ENTER"},{KEY_ESC,"ESC"}, - {KEY_BACKSPACE,"BS"},{KEY_TAB,"TAB"}, - {KEY_UP,"UP"},{KEY_DOWN,"DOWN"},{KEY_LEFT,"LEFT"},{KEY_RIGHT,"RIGHT"}, - {KEY_HOME,"HOME"},{KEY_END,"END"},{KEY_DELETE,"DEL"},{KEY_INSERT,"INS"}, - {KEY_LEFTSHIFT,"LSHIFT"},{KEY_LEFTCTRL,"LCTRL"},{KEY_LEFTALT,"LALT"}, - }; - printf("[KBD] ==== evdev key_code -> label table ====\n"); - for (size_t i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) { - printf("[KBD] code=%3u %s\n", keys[i].code, keys[i].name); - } - printf("[KBD] ==== end ====\n"); - fflush(stdout); -} -#else -void kbd_dump_keymap_table(void) {} -#endif - - -#if !LV_USE_SDL -/* ============================================================ - * Parameters - * ============================================================ */ -#define EVDEV_KEYCODE_OFFSET 8 -#define REPEAT_DELAY_MS 500 /* delay before first repeat */ -#define REPEAT_RATE_MS 30 /* interval between subsequent repeats */ - -/* ============================================================ - * libinput open/close callbacks - * ============================================================ */ -static int open_restricted(const char *path, int flags, void *user_data) { - int fd = open(path, flags); - if (fd < 0) { - fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); - return -errno; - } - /* Grab the device exclusively. Without this, the kernel VT keyboard - * handler also feeds keystrokes from the integrated TCA8418 keypad to - * the foreground tty — leaking keys into any shell on tty1 / HDMI - * console at the same time APPLaunch is reading them. EBUSY here is - * non-fatal: another grabber already holds it, libinput will read - * normally without the VT-leak protection. */ - if (ioctl(fd, EVIOCGRAB, 1) < 0 && errno != EBUSY) { - fprintf(stderr, "[KBD] EVIOCGRAB %s failed: %s\n", path, strerror(errno)); - } - return fd; -} -static void close_restricted(int fd, void *user_data) { close(fd); } -static const struct libinput_interface interface = { - .open_restricted = open_restricted, - .close_restricted = close_restricted, -}; - -/* ============================================================ - * TCA8418 custom keycode mapping table (same as the original one) - * ============================================================ */ -struct tca8418_keymap_entry { - uint32_t keycode; - const char *sym_name; - const char *utf8; -}; -static const struct tca8418_keymap_entry tca8418_keymap[] = { - { 183, "exclam", "!" }, - - { 184, "at", "@" }, - { 185, "numbersign", "#" }, - { 186, "dollar", "$" }, - { 187, "percent", "%" }, - { 188, "asciicircum", "^" }, - { 189, "ampersand", "&" }, - { 190, "asterisk", "*" }, - { 191, "parenleft", "(" }, - { 192, "parenright", ")" }, - - { 193, "asciitilde", "~" }, - { 194, "grave", "`" }, - { 195, "plus", "+" }, - { 196, "minus", "-" }, - { 197, "slash", "/" }, - { 198, "backslash", "\\" }, - { 199, "braceleft", "{" }, - { 200, "braceright", "}" }, - { 201, "bracketleft", "[" }, - { 202, "bracketright", "]" }, - - { 231, "comma", "," }, - { 232, "period", "." }, - { 233, "bar", "|" }, - { 209, "equal", "=" }, - { 210, "colon", ":" }, - { 211, "semicolon", ";" }, - { 212, "underscore", "_" }, - { 213, "question", "?" }, - - { 214, "less", "<" }, - { 215, "greater", ">" }, - { 216, "apostrophe", "'" }, - { 217, "quotedbl", "\"" }, -}; - - - - - - - - - -#define TCA8418_KEYMAP_SIZE (sizeof(tca8418_keymap)/sizeof(tca8418_keymap[0])) - -static const struct tca8418_keymap_entry * -tca8418_keymap_lookup(uint32_t keycode) { - for (size_t i = 0; i < TCA8418_KEYMAP_SIZE; i++) - if (tca8418_keymap[i].keycode == keycode) - return &tca8418_keymap[i]; - return NULL; -} - - -/* ============================================================ - * Control-key -> terminal control-character mapping table - * When xkbcommon does not produce utf8 for function keys, fill in ANSI escape sequences as fallback - * ============================================================ */ -struct ctrl_key_utf8_entry { - uint32_t keycode; /* KEY_xxx from linux/input.h */ - const char *utf8; /* terminal control character / ANSI escape sequence */ -}; - -static const struct ctrl_key_utf8_entry ctrl_key_utf8_map[] = { - /* ---- single-byte control characters ---- */ - { KEY_ENTER, "\r" }, /* CR (0x0D) */ - { KEY_KPENTER, "\r" }, /* CR keypad Enter */ - { KEY_BACKSPACE, "\x7f" }, /* DEL (0x7F) */ - { KEY_TAB, "\t" }, /* HT (0x09) */ - { KEY_ESC, "\x1b" }, /* ESC (0x1B) */ - - /* ---- ANSI arrow keys ---- */ - { KEY_UP, "\033[A" }, /* CSI A */ - { KEY_DOWN, "\033[B" }, /* CSI B */ - { KEY_RIGHT, "\033[C" }, /* CSI C */ - { KEY_LEFT, "\033[D" }, /* CSI D */ - - /* ---- ANSI editing keys ---- */ - { KEY_HOME, "\033[H" }, /* CSI H (or "\033[1~") */ - { KEY_END, "\033[F" }, /* CSI F (or "\033[4~") */ - { KEY_DELETE, "\033[3~" }, /* SS3 ~ */ - { KEY_INSERT, "\033[2~" }, /* CSI ~ */ - { KEY_PAGEUP, "\033[5~" }, /* CSI ~ */ - { KEY_PAGEDOWN, "\033[6~" }, /* CSI ~ */ - - /* ---- F1-F12 ---- */ - { KEY_F1, "\033OP" }, - { KEY_F2, "\033OQ" }, - { KEY_F3, "\033OR" }, - { KEY_F4, "\033OS" }, - { KEY_F5, "\033[15~"}, - { KEY_F6, "\033[17~"}, - { KEY_F7, "\033[18~"}, - { KEY_F8, "\033[19~"}, - { KEY_F9, "\033[20~"}, - { KEY_F10, "\033[21~"}, - { KEY_F11, "\033[23~"}, - { KEY_F12, "\033[24~"}, -}; - -static const char *ctrl_key_utf8_lookup(uint32_t keycode) { - for (size_t i = 0; i < sizeof(ctrl_key_utf8_map) / sizeof(ctrl_key_utf8_map[0]); i++) - if (ctrl_key_utf8_map[i].keycode == keycode) - return ctrl_key_utf8_map[i].utf8; - return NULL; -} - - - - - -/* ============================================================ - * Keyboard context - * ============================================================ */ -struct kbd_ctx { - struct libinput *li; - struct libinput_device *dev; - - struct xkb_context *ctx; - struct xkb_keymap *keymap; - struct xkb_state *state; - struct xkb_compose_table *compose_table; - struct xkb_compose_state *compose_state; - - /* key repeat */ - int repeat_fd; - bool repeating; - struct key_item repeat_template; /* save the last pressed key for repeat copies */ -}; - -/* ============================================================ - * xkbcommon log callback - * ============================================================ */ -static void uxkb_log(struct xkb_context *ctx, enum xkb_log_level level, - const char *fmt, va_list args) -{ - (void)ctx; (void)level; - vfprintf(stderr, fmt, args); -} - -/* ============================================================ - * modifier bitmask - * ============================================================ */ -static uint32_t get_mods(struct xkb_state *state) { - uint32_t m = 0; - if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0) - m |= KBD_MOD_SHIFT; - if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0) - m |= KBD_MOD_CTRL; - if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0) - m |= KBD_MOD_ALT; - if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0) - m |= KBD_MOD_LOGO; - if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CAPS, XKB_STATE_MODS_EFFECTIVE) > 0) - m |= KBD_MOD_CAPS; - if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_NUM, XKB_STATE_MODS_EFFECTIVE) > 0) - m |= KBD_MOD_NUM; - return m; -} - -/* ============================================================ - * LED update (via libinput; no need to write evdev directly) - * ============================================================ */ -static void update_leds(struct kbd_ctx *kc) { - enum libinput_led leds = 0; - if (xkb_state_led_name_is_active(kc->state, XKB_LED_NAME_NUM) > 0) - leds |= LIBINPUT_LED_NUM_LOCK; - if (xkb_state_led_name_is_active(kc->state, XKB_LED_NAME_CAPS) > 0) - leds |= LIBINPUT_LED_CAPS_LOCK; - if (xkb_state_led_name_is_active(kc->state, XKB_LED_NAME_SCROLL) > 0) - leds |= LIBINPUT_LED_SCROLL_LOCK; - libinput_device_led_update(kc->dev, leds); -} - -/* ============================================================ - * Enqueue - * ============================================================ */ -static void enqueue_key(const struct key_item *src) { - struct key_item *elm = malloc(sizeof(*elm)); - if (!elm) return; - *elm = *src; - elm->flage = 0; // mark as needing free - - /* DEBUG: every raw key event from keyboard thread */ - char utf8_dbg[64] = ""; - int di = 0; - for (int i = 0; i < (int)sizeof(elm->utf8) && elm->utf8[i] && di < 60; i++) { - unsigned char c = (unsigned char)elm->utf8[i]; - if (c >= 0x20 && c < 0x7f) utf8_dbg[di++] = (char)c; - else di += snprintf(utf8_dbg+di, 64-di, "\\x%02x", c); - } - utf8_dbg[di] = '\0'; - printf("[KBD] enqueue code=%u state=%s sym=%s utf8='%s' cp=0x%x mods=0x%x run=%d home_flag=%d\n", - elm->key_code, kbd_state_name(elm->key_state), elm->sym_name, - utf8_dbg, elm->codepoint, elm->mods, LVGL_RUN_FLAGE, LVGL_HOME_KEY_FLAG); - - if(elm->key_code == KEY_ESC) { - LVGL_HOME_KEY_FLAG = elm->key_state; - printf("[KBD] LVGL_HOME_KEY_FLAG := %d\n", LVGL_HOME_KEY_FLAG); - } - - if(LVGL_RUN_FLAGE) - { - pthread_mutex_lock(&keyboard_mutex); - STAILQ_INSERT_TAIL(&keyboard_queue, elm, entries); - pthread_mutex_unlock(&keyboard_mutex); - } - else - { - printf("[KBD] dropped (LVGL_RUN_FLAGE=0, external app running)\n"); - free(elm); - } - -} - -/* ============================================================ - * Key repeat control - * ============================================================ */ -static void repeat_start(struct kbd_ctx *kc) { - struct itimerspec ts = { - .it_interval = { .tv_sec = 0, .tv_nsec = (long)REPEAT_RATE_MS * 1000000L }, - .it_value = { .tv_sec = 0, .tv_nsec = (long)REPEAT_DELAY_MS * 1000000L }, - }; - timerfd_settime(kc->repeat_fd, 0, &ts, NULL); - kc->repeating = true; -} -static void repeat_stop(struct kbd_ctx *kc) { - struct itimerspec ts = {0}; - timerfd_settime(kc->repeat_fd, 0, &ts, NULL); - kc->repeating = false; -} - -/* Encode a UTF-32 code point as UTF-8 and return the byte count */ -static int utf32_to_utf8(uint32_t cp, char *out, size_t n) { - if (n < 1) return 0; - if (cp < 0x80) { - if (n < 2) return 0; - out[0] = (char)cp; out[1] = '\0'; return 1; - } else if (cp < 0x800) { - if (n < 3) return 0; - out[0] = 0xC0 | (cp >> 6); - out[1] = 0x80 | (cp & 0x3F); - out[2] = '\0'; return 2; - } else if (cp < 0x10000) { - if (n < 4) return 0; - out[0] = 0xE0 | (cp >> 12); - out[1] = 0x80 | ((cp >> 6) & 0x3F); - out[2] = 0x80 | (cp & 0x3F); - out[3] = '\0'; return 3; - } else if (cp < 0x110000) { - if (n < 5) return 0; - out[0] = 0xF0 | (cp >> 18); - out[1] = 0x80 | ((cp >> 12) & 0x3F); - out[2] = 0x80 | ((cp >> 6) & 0x3F); - out[3] = 0x80 | (cp & 0x3F); - out[4] = '\0'; return 4; - } - out[0] = '\0'; return 0; -} - -/* ============================================================ - * Core: handle one key event - * ============================================================ */ -static void process_key(struct kbd_ctx *kc, uint32_t code, int pressed) -{ - xkb_keycode_t keycode = code + EVDEV_KEYCODE_OFFSET; - struct key_item item = {0}; - item.key_code = code; - item.key_state = pressed ? KBD_KEY_PRESSED : KBD_KEY_RELEASED; - - /* ---------- 1. TCA8418 custom keycodes first ---------- */ - const struct tca8418_keymap_entry *mapped = tca8418_keymap_lookup(code); - if (mapped) { - xkb_keysym_t sym = xkb_keysym_from_name(mapped->sym_name, - XKB_KEYSYM_NO_FLAGS); - snprintf(item.sym_name, sizeof(item.sym_name), "%s", mapped->sym_name); - snprintf(item.utf8, sizeof(item.utf8), "%s", mapped->utf8); - item.keysym = sym; - item.codepoint = (sym != XKB_KEY_NoSymbol) ? xkb_keysym_to_utf32(sym) : 0; - item.mods = get_mods(kc->state); - - /* repeat handling */ - if (pressed) { - kc->repeat_template = item; - kc->repeat_template.key_state = KBD_KEY_REPEATED; - repeat_start(kc); - } else if (kc->repeating && kc->repeat_template.key_code == code) { - repeat_stop(kc); - } - enqueue_key(&item); - return; - } - - /* ---------- 2. standard xkbcommon flow ---------- */ - const xkb_keysym_t *syms; - int num = xkb_state_key_get_syms(kc->state, keycode, &syms); - xkb_keysym_t one_sym = XKB_KEY_NoSymbol; - - if (num == 1) { - /* handle Lock modifiers (following uterm + libxkbcommon recommendations) */ - one_sym = xkb_state_key_get_one_sym(kc->state, keycode); - } else if (num > 1) { - one_sym = syms[0]; - } - - /* ---------- 3. Compose handling (dead keys, etc.) ---------- */ - enum xkb_compose_status cstatus = XKB_COMPOSE_NOTHING; - bool compose_produced_utf8 = false; - - if (kc->compose_state && pressed) { - xkb_compose_state_feed(kc->compose_state, one_sym); - cstatus = xkb_compose_state_get_status(kc->compose_state); - - if (cstatus == XKB_COMPOSE_COMPOSED) { - xkb_keysym_t csym = xkb_compose_state_get_one_sym(kc->compose_state); - if (csym != XKB_KEY_NoSymbol) { - one_sym = csym; - } - /* get the composed UTF-8 string */ - int n = xkb_compose_state_get_utf8(kc->compose_state, - item.utf8, sizeof(item.utf8)); - if (n > 0) compose_produced_utf8 = true; - - /* If neither keysym nor utf8 is available, treat it as canceled */ - if (csym == XKB_KEY_NoSymbol && !compose_produced_utf8) - cstatus = XKB_COMPOSE_CANCELLED; - } - if (cstatus == XKB_COMPOSE_COMPOSED || cstatus == XKB_COMPOSE_CANCELLED) - xkb_compose_state_reset(kc->compose_state); - } - - /* ---------- 4. update xkb state (must be after get_syms) ---------- */ - enum xkb_state_component changed = 0; - if (pressed) - changed = xkb_state_update_key(kc->state, keycode, XKB_KEY_DOWN); - else - changed = xkb_state_update_key(kc->state, keycode, XKB_KEY_UP); - if (changed & XKB_STATE_LEDS) - update_leds(kc); - - /* ---------- 5. filter events that are composing or canceled ---------- */ - if (cstatus == XKB_COMPOSE_COMPOSING || cstatus == XKB_COMPOSE_CANCELLED) - return; - if (num <= 0 && !compose_produced_utf8) - return; - - /* ---------- 6. fill item ---------- */ - xkb_keysym_get_name(one_sym, item.sym_name, sizeof(item.sym_name)); - item.keysym = one_sym; - item.codepoint = (one_sym != XKB_KEY_NoSymbol) - ? xkb_keysym_to_utf32(one_sym) : 0; - item.mods = get_mods(kc->state); - - /* If compose did not provide utf8, get it from xkb_state */ - if (item.utf8[0] == '\0') { - xkb_state_key_get_utf8(kc->state, keycode, - item.utf8, sizeof(item.utf8)); - if (item.utf8[0] == '\0' && item.codepoint != 0) { - /* get_utf8 filters control characters; fall back to manual encoding */ - utf32_to_utf8(item.codepoint, item.utf8, sizeof(item.utf8)); - } - if (item.codepoint == 0) - item.codepoint = xkb_state_key_get_utf32(kc->state, keycode); - } - - /* ---------- 6.5 control-key fallback mapping ---------- */ - /* xkbcommon does not produce utf8 for function keys (UP/DOWN/ENTER/BACKSPACE, etc.), - * manually fill ANSI/VT100 terminal control characters here for upper layers */ - if (item.utf8[0] == '\0') { - const char *ctrl = ctrl_key_utf8_lookup(code); - if (ctrl) { - snprintf(item.utf8, sizeof(item.utf8), "%s", ctrl); - } - } - - /* ---------- 7. repeat control ---------- */ - if (pressed && xkb_keymap_key_repeats(kc->keymap, keycode)) { - kc->repeat_template = item; - kc->repeat_template.key_state = KBD_KEY_REPEATED; - repeat_start(kc); - } else if (!pressed && kc->repeating && - kc->repeat_template.key_code == code) { - repeat_stop(kc); - } - - /* ---------- 8. Enqueue ---------- */ - enqueue_key(&item); -} - -/* ============================================================ - * xkb initialization (with rmlvo fallback + compose) - * ============================================================ */ -static int init_xkb(struct kbd_ctx *kc, - const char *layout, const char *locale) -{ - kc->ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if (!kc->ctx) { fprintf(stderr, "xkb_context_new failed\n"); return -1; } - xkb_context_set_log_fn(kc->ctx, uxkb_log); - - struct xkb_rule_names rmlvo = { - .rules = "evdev", - .model = NULL, - .layout = layout ? layout : "us", - .variant = NULL, - .options = NULL, - }; - kc->keymap = xkb_keymap_new_from_names(kc->ctx, &rmlvo, - XKB_KEYMAP_COMPILE_NO_FLAGS); - if (!kc->keymap) { - /* empty rmlvo fallback */ - struct xkb_rule_names empty = {0}; - kc->keymap = xkb_keymap_new_from_names(kc->ctx, &empty, - XKB_KEYMAP_COMPILE_NO_FLAGS); - } - if (!kc->keymap) { fprintf(stderr, "failed to create keymap\n"); return -1; } - - kc->state = xkb_state_new(kc->keymap); - if (!kc->state) { fprintf(stderr, "failed to create xkb_state\n"); return -1; } - - /* Compose table */ - if (!locale || !*locale) { - locale = getenv("LC_ALL"); - if (!locale || !*locale) locale = getenv("LC_CTYPE"); - if (!locale || !*locale) locale = getenv("LANG"); - if (!locale || !*locale) locale = "C"; - } - kc->compose_table = xkb_compose_table_new_from_locale( - kc->ctx, locale, XKB_COMPOSE_COMPILE_NO_FLAGS); - if (kc->compose_table) { - kc->compose_state = xkb_compose_state_new(kc->compose_table, - XKB_COMPOSE_STATE_NO_FLAGS); - if (!kc->compose_state) - fprintf(stderr, "Warning: failed to create compose_state; disabling compose\n"); - } else { - fprintf(stderr, "Warning: locale=%s has no compose table\n", locale); - } - return 0; -} - -static void free_xkb(struct kbd_ctx *kc) { - if (kc->compose_state) xkb_compose_state_unref(kc->compose_state); - if (kc->compose_table) xkb_compose_table_unref(kc->compose_table); - if (kc->state) xkb_state_unref(kc->state); - if (kc->keymap) xkb_keymap_unref(kc->keymap); - if (kc->ctx) xkb_context_unref(kc->ctx); -} - -/* Optional: rebuild state on VT wakeup while preserving locked mods/layout (see uxkb_dev_wake_up) */ -static void kbd_wake_up(struct kbd_ctx *kc) { - xkb_mod_mask_t locked_mods = xkb_state_serialize_mods(kc->state, - XKB_STATE_MODS_LOCKED); - xkb_layout_index_t locked_layout = xkb_state_serialize_layout( - kc->state, XKB_STATE_LAYOUT_LOCKED); - xkb_state_unref(kc->state); - kc->state = xkb_state_new(kc->keymap); - if (!kc->state) return; - xkb_state_update_mask(kc->state, 0, 0, locked_mods, 0, 0, locked_layout); - update_leds(kc); - if (kc->compose_state) xkb_compose_state_reset(kc->compose_state); -} - -/* ============================================================ - * Thread main loop - * ============================================================ */ -void *keyboard_read_thread(void *argv) { - STAILQ_INIT(&keyboard_queue); - - const char *device_path = argv ? (const char *)argv - : "/dev/input/by-path/platform-3f804000.i2c-event"; - - struct kbd_ctx kc = {0}; - kc.repeat_fd = -1; - - /* ---------- 1. libinput ---------- */ - kc.li = libinput_path_create_context(&interface, NULL); - if (!kc.li) { fprintf(stderr, "failed to create libinput context\n"); goto out; } - - kc.dev = libinput_path_add_device(kc.li, device_path); - if (!kc.dev) { - fprintf(stderr, "Failed to add device %s (root permissions may be required)\n", device_path); - goto out; - } - if (!libinput_device_has_capability(kc.dev, LIBINPUT_DEVICE_CAP_KEYBOARD)) { - fprintf(stderr, "%s is not a keyboard device\n", device_path); - goto out; - } - - /* ---------- 2. xkbcommon ---------- */ - if (init_xkb(&kc, "us", NULL) < 0) goto out; - - /* ---------- 3. key repeat timerfd ---------- */ - kc.repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (kc.repeat_fd < 0) { perror("timerfd_create"); goto out; } - - /* ---------- 4. event loop ---------- */ - int li_fd = libinput_get_fd(kc.li); - struct pollfd pfds[2] = { - { .fd = li_fd, .events = POLLIN }, - { .fd = kc.repeat_fd, .events = POLLIN }, - }; - - g_libinput = kc.li; - printf("Start listening for keyboard input (%s)\n", device_path); - libinput_dispatch(kc.li); - - while (1) { - if (keyboard_paused_flag) { - usleep(50000); - continue; - } - int pr = poll(pfds, 2, 100); - if (pr < 0) { - if (errno == EINTR) continue; - perror("poll"); break; - } - if (pr == 0) continue; - - /* keyboard event */ - if (pfds[0].revents & POLLIN) { - libinput_dispatch(kc.li); - struct libinput_event *ev; - while ((ev = libinput_get_event(kc.li)) != NULL) { - if (libinput_event_get_type(ev) == LIBINPUT_EVENT_KEYBOARD_KEY) { - struct libinput_event_keyboard *kev = - libinput_event_get_keyboard_event(ev); - uint32_t code = libinput_event_keyboard_get_key(kev); - enum libinput_key_state ks = - libinput_event_keyboard_get_key_state(kev); - process_key(&kc, code, - ks == LIBINPUT_KEY_STATE_PRESSED ? 1 : 0); - } - libinput_event_destroy(ev); - } - } - - /* repeat timer triggered */ - if (pfds[1].revents & POLLIN) { - uint64_t exp; - while (read(kc.repeat_fd, &exp, sizeof(exp)) == sizeof(exp)) { - if (kc.repeating) { - /* refresh mods (prevents Shift, etc. from changing during repeat) */ - kc.repeat_template.mods = get_mods(kc.state); - enqueue_key(&kc.repeat_template); - } - } - } - } - -out: - if (kc.repeat_fd >= 0) close(kc.repeat_fd); - free_xkb(&kc); - if (kc.li) libinput_unref(kc.li); - return NULL; -} -#else - -#include "ui/ui.h" -#include "compat/input_keys.h" - -/* SDL scancode -> Linux keycode mapping for key_item.key_code */ -static uint32_t sdl_scancode_to_linux_keycode(uint32_t sc) -{ - switch(sc) { - case 4: return KEY_A; case 5: return KEY_B; - case 6: return KEY_C; case 7: return KEY_D; - case 8: return KEY_E; case 9: return KEY_F; - case 10: return KEY_G; case 11: return KEY_H; - case 12: return KEY_I; case 13: return KEY_J; - case 14: return KEY_K; case 15: return KEY_L; - case 16: return KEY_M; case 17: return KEY_N; - case 18: return KEY_O; case 19: return KEY_P; - case 20: return KEY_Q; case 21: return KEY_R; - case 22: return KEY_S; case 23: return KEY_T; - case 24: return KEY_U; case 25: return KEY_V; - case 26: return KEY_W; case 27: return KEY_X; - case 28: return KEY_Y; case 29: return KEY_Z; - case 30: return KEY_1; case 31: return KEY_2; - case 32: return KEY_3; case 33: return KEY_4; - case 34: return KEY_5; case 35: return KEY_6; - case 36: return KEY_7; case 37: return KEY_8; - case 38: return KEY_9; case 39: return KEY_0; - case 40: return KEY_ENTER; - case 41: return KEY_ESC; - case 42: return KEY_BACKSPACE; - case 43: return KEY_TAB; - case 44: return KEY_SPACE; - case 79: return KEY_RIGHT; - case 80: return KEY_LEFT; - case 81: return KEY_DOWN; - case 82: return KEY_UP; - case 76: return KEY_DELETE; - case 74: return KEY_HOME; - case 77: return KEY_END; - case 75: return KEY_PAGEUP; - case 78: return KEY_PAGEDOWN; - case 225: return KEY_LEFTSHIFT; - case 224: return KEY_LEFTCTRL; - case 226: return KEY_LEFTALT; - case 58: return KEY_F1; case 59: return KEY_F2; - case 60: return KEY_F3; case 61: return KEY_F4; - case 62: return KEY_F5; case 63: return KEY_F6; - case 64: return KEY_F7; case 65: return KEY_F8; - case 66: return KEY_F9; case 67: return KEY_F10; - case 68: return KEY_F11; case 69: return KEY_F12; - default: return sc; - } -} - -#include "lvgl/src/drivers/sdl/lv_sdl_keyboard.h" -#include "lvgl/src/core/lv_group.h" -#include "lvgl/src/core/lv_obj.h" -#include "lvgl/src/core/lv_obj_event.h" -#include "lvgl/src/display/lv_display.h" -#include "lvgl/src/stdlib/lv_string.h" -#include "lvgl/src/misc/lv_text_private.h" -#include "lvgl/src/drivers/sdl/lv_sdl_private.h" - -#include -#include - -/* This must match the header that actually defines key_item in your project */ -// #include "your_key_item.h" /* contains the struct key_item declaration */ - -/* modifier bitmask, align this with KBD_MOD_* */ -#ifndef KBD_MOD_SHIFT -#define KBD_MOD_SHIFT (1u << 0) -#define KBD_MOD_CTRL (1u << 1) -#define KBD_MOD_ALT (1u << 2) -#define KBD_MOD_LOGO (1u << 3) -#endif - -/********************** - * TYPEDEFS - **********************/ -typedef struct { - char buf[KEYBOARD_BUFFER_SIZE]; - bool dummy_read; - - uint32_t cur_scancode; - uint32_t cur_keysym; - uint32_t cur_mods; - char cur_sym_name[65]; - bool cur_valid; - - /* Added: record the most recently pressed character for reuse on release */ - uint32_t last_codepoint; - char last_utf8[8]; - size_t last_utf8_len; -} lv_sdl_keyboard_t; - -/********************** - * STATIC PROTOTYPES - **********************/ -static void sdl_keyboard_read(lv_indev_t * indev, lv_indev_data_t * data); -static uint32_t keycode_to_ctrl_key(SDL_Keycode sdl_key); -static void release_indev_cb(lv_event_t * e); -static void send_key_item_event(lv_sdl_keyboard_t * dev, - uint32_t codepoint, - const char * utf8, size_t utf8_len, - int key_state); - -/********************** - * GLOBAL FUNCTIONS - **********************/ -lv_indev_t * lv_sdl_keyboard_create(void) -{ - lv_sdl_keyboard_t * dsc = lv_malloc_zeroed(sizeof(lv_sdl_keyboard_t)); - LV_ASSERT_MALLOC(dsc); - if(dsc == NULL) return NULL; - - lv_indev_t * indev = lv_indev_create(); - LV_ASSERT_MALLOC(indev); - if(indev == NULL) { - lv_free(dsc); - return NULL; - } - - lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD); - lv_indev_set_read_cb(indev, sdl_keyboard_read); - lv_indev_set_driver_data(indev, dsc); - lv_indev_set_mode(indev, LV_INDEV_MODE_EVENT); - lv_indev_add_event_cb(indev, release_indev_cb, LV_EVENT_DELETE, indev); - return indev; -} - -/********************** - * STATIC FUNCTIONS - **********************/ - -/* Dispatch a key_item event to the active screen */ -static void send_key_item_event(lv_sdl_keyboard_t * dev, - uint32_t codepoint, - const char * utf8, size_t utf8_len, - int key_state) -{ - struct key_item * elm = (struct key_item *)calloc(1, sizeof(struct key_item)); - if(elm == NULL) return; - - elm->key_code = dev->cur_scancode; - elm->keysym = dev->cur_keysym; - elm->codepoint = codepoint; - elm->mods = dev->cur_mods; - elm->key_state = key_state; /* 0=released, 1=pressed */ - - /* sym_name */ - if(dev->cur_sym_name[0]) { - strncpy(elm->sym_name, dev->cur_sym_name, sizeof(elm->sym_name) - 1); - elm->sym_name[sizeof(elm->sym_name) - 1] = '\0'; - } - - /* utf8 */ - if(utf8 && utf8_len) { - size_t n = utf8_len < sizeof(elm->utf8) - 1 ? utf8_len : sizeof(elm->utf8) - 1; - memcpy(elm->utf8, utf8, n); - elm->utf8[n] = '\0'; - } - - elm->flage = 1; /* mark that the caller needs to free it (keeps the original semantics) */ - - lv_obj_t * root = lv_screen_active(); - if(root) { - lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); - } - - /* The event is dispatched synchronously; free it immediately here */ - free(elm); -} - -static void sdl_keyboard_read(lv_indev_t * indev, lv_indev_data_t * data) -{ - lv_sdl_keyboard_t * dev = lv_indev_get_driver_data(indev); - const size_t len = lv_strlen(dev->buf); - data->continue_reading = false; - - /* release event */ - if(dev->dummy_read) { - dev->dummy_read = false; - data->state = LV_INDEV_STATE_RELEASED; - /* include key as well, matching the press event */ - data->key = dev->last_codepoint; - - if(dev->cur_valid) { - /* send release using cached press information */ - send_key_item_event(dev, - dev->last_codepoint, - dev->last_utf8_len ? dev->last_utf8 : NULL, - dev->last_utf8_len, - 0); - dev->cur_valid = false; - } - - /* clear cache */ - dev->last_codepoint = 0; - dev->last_utf8[0] = '\0'; - dev->last_utf8_len = 0; - } - /* press event */ - else if(len > 0) { - dev->dummy_read = true; - data->state = LV_INDEV_STATE_PRESSED; - data->key = 0; - - uint32_t utf8_len = lv_text_encoded_size(dev->buf); - if(utf8_len == 0) utf8_len = 1; - - /* decode the Unicode code point correctly */ - uint32_t i = 0; - uint32_t codepoint = lv_text_encoded_next(dev->buf, &i); - if(codepoint == 0) { - /* control characters (LV_KEY_*, etc.) can be read byte by byte */ - codepoint = (uint8_t)dev->buf[0]; - } - data->key = codepoint; - - /* cache this press information for release */ - dev->last_codepoint = codepoint; - size_t n = utf8_len < sizeof(dev->last_utf8) - 1 ? utf8_len - : sizeof(dev->last_utf8) - 1; - memcpy(dev->last_utf8, dev->buf, n); - dev->last_utf8[n] = '\0'; - dev->last_utf8_len = n; - - /* dispatch press event */ - send_key_item_event(dev, codepoint, dev->buf, utf8_len, 1); - - /* consume processed bytes */ - lv_memmove(dev->buf, dev->buf + utf8_len, len - utf8_len + 1); - } - else { - data->state = LV_INDEV_STATE_RELEASED; - } -} - -static void release_indev_cb(lv_event_t * e) -{ - lv_indev_t * indev = (lv_indev_t *)lv_event_get_user_data(e); - lv_sdl_keyboard_t * dev = lv_indev_get_driver_data(indev); - if(dev) { - lv_indev_set_driver_data(indev, NULL); - lv_indev_set_read_cb(indev, NULL); - lv_free(dev); - LV_LOG_INFO("done"); - } -} - -void lv_sdl_keyboard_handler(SDL_Event * event) -{ - uint32_t win_id = UINT32_MAX; - switch(event->type) { - case SDL_KEYDOWN: win_id = event->key.windowID; break; - case SDL_TEXTINPUT: win_id = event->text.windowID; break; - default: return; - } - - lv_display_t * disp = lv_sdl_get_disp_from_win_id(win_id); - - lv_indev_t * indev = lv_indev_get_next(NULL); - while(indev) { - if(lv_indev_get_read_cb(indev) == sdl_keyboard_read) { - if(disp == NULL || lv_indev_get_display(indev) == disp) break; - } - indev = lv_indev_get_next(indev); - } - if(indev == NULL) return; - - lv_sdl_keyboard_t * dsc = lv_indev_get_driver_data(indev); - - switch(event->type) { - case SDL_KEYDOWN: { - SDL_Keycode sym = event->key.keysym.sym; - SDL_Scancode sc = event->key.keysym.scancode; - Uint16 md = event->key.keysym.mod; - - /* fill metadata for this event — convert SDL scancode to Linux keycode */ - dsc->cur_scancode = sdl_scancode_to_linux_keycode(sc); - dsc->cur_keysym = (uint32_t)sym; - dsc->cur_mods = 0; - if(md & KMOD_SHIFT) dsc->cur_mods |= KBD_MOD_SHIFT; - if(md & KMOD_CTRL) dsc->cur_mods |= KBD_MOD_CTRL; - if(md & KMOD_ALT) dsc->cur_mods |= KBD_MOD_ALT; - if(md & KMOD_GUI) dsc->cur_mods |= KBD_MOD_LOGO; - - const char * kname = SDL_GetKeyName(sym); - if(kname) { - strncpy(dsc->cur_sym_name, kname, sizeof(dsc->cur_sym_name) - 1); - dsc->cur_sym_name[sizeof(dsc->cur_sym_name) - 1] = '\0'; - } - else { - dsc->cur_sym_name[0] = '\0'; - } - dsc->cur_valid = true; - - /* control keys -> LV_KEY_* */ - const uint32_t ctrl_key = keycode_to_ctrl_key(sym); - if(ctrl_key == '\0') return; /* normal characters are handled by SDL_TEXTINPUT */ - - const size_t blen = lv_strlen(dsc->buf); - if(blen < KEYBOARD_BUFFER_SIZE - 1) { - dsc->buf[blen] = ctrl_key; - dsc->buf[blen + 1] = '\0'; - } - break; - } - - case SDL_TEXTINPUT: { - /* If KEYDOWN did not fill it earlier, fill the basic fields here */ - if(!dsc->cur_valid) { - dsc->cur_scancode = 0; - dsc->cur_keysym = 0; - dsc->cur_mods = 0; - dsc->cur_sym_name[0] = '\0'; - dsc->cur_valid = true; - } - const size_t total = lv_strlen(dsc->buf) + lv_strlen(event->text.text); - if(total < KEYBOARD_BUFFER_SIZE - 1) - lv_strcat(dsc->buf, event->text.text); - break; - } - - default: break; - } - - size_t len = lv_strlen(dsc->buf); - while(len) { - lv_indev_read(indev); /* press -> send a key_item(state=1) */ - lv_indev_read(indev); /* dummy release -> send a key_item(state=0) */ - len--; - } -} - -static uint32_t keycode_to_ctrl_key(SDL_Keycode sdl_key) -{ - switch(sdl_key) { - case SDLK_RIGHT: - case SDLK_KP_PLUS: return LV_KEY_RIGHT; - case SDLK_LEFT: - case SDLK_KP_MINUS: return LV_KEY_LEFT; - case SDLK_UP: return LV_KEY_UP; - case SDLK_DOWN: return LV_KEY_DOWN; - case SDLK_ESCAPE: return LV_KEY_ESC; - case SDLK_BACKSPACE: return LV_KEY_BACKSPACE; - case SDLK_DELETE: return LV_KEY_DEL; - case SDLK_KP_ENTER: - case '\r': return LV_KEY_ENTER; - case SDLK_TAB: return 15; - case SDLK_PAGEDOWN: return LV_KEY_NEXT; - case SDLK_PAGEUP: return LV_KEY_PREV; - case SDLK_HOME: return LV_KEY_HOME; - case SDLK_END: return LV_KEY_END; - default: return '\0'; - } -} - -#endif \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c index 823a68fb..11f68e07 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c @@ -1,459 +1,711 @@ -#include "hal_lvgl_bsp.h" -#include "lvgl/lvgl.h" -#include "commount.h" -#include -#include -#include -#include -#include -#include -#include +// cp0_lvgl_keyboard.c +#define _GNU_SOURCE #include #include #include +#include +#include #include +#ifdef __linux__ +#include +#include +#endif +#include +#include +#ifdef __linux__ +#include +#include +#include +#include #include -#include "cp0_lvgl.h" - - -#define CP0_DEFAULT_INPUT_SEAT "seat-cardputer-zero" -#define CP0_KEY_QUEUE_SIZE 10 -#define CP0_EVDEV_KEYCODE_OFFSET 8 +#include +#else +#include "compat/input_keys.h" +#endif +#include "keyboard_input.h" +#include "lvgl/lvgl.h" -#define LV_C_EVENT_KEYBOARD lv_c_event[CP0_C_EVENT_KEYBOARD] +/* ============================================================ + * Global queue + * ============================================================ */ +struct keyboard_queue_t keyboard_queue; +pthread_mutex_t keyboard_mutex = PTHREAD_MUTEX_INITIALIZER; +volatile int LVGL_HOME_KEY_FLAG = 0; +volatile int LVGL_RUN_FLAGE = 1; +volatile uint32_t LV_EVENT_KEYBOARD; + +static volatile int keyboard_paused_flag = 0; +#ifdef __linux__ +static struct libinput *g_libinput = NULL; +void keyboard_pause(void) { + keyboard_paused_flag = 1; + if (g_libinput) libinput_suspend(g_libinput); + printf("[KBD] keyboard_pause()\n"); +} +void keyboard_resume(void) { + if (g_libinput) libinput_resume(g_libinput); + keyboard_paused_flag = 0; + printf("[KBD] keyboard_resume()\n"); +} +#else +void keyboard_pause(void) { keyboard_paused_flag = 1; } +void keyboard_resume(void) { keyboard_paused_flag = 0; } +#endif + +/* ============================================================ + * Debug: key_state name + * ============================================================ */ +const char *kbd_state_name(int state) +{ + switch (state) { + case 0: return "UP"; + case 1: return "DOWN"; + case 2: return "REPEAT"; + default: return "???"; + } +} -typedef struct { - uint32_t code; - uint32_t key; -} cp0_keymap_entry_t; +/* ============================================================ + * Debug: dump all ASCII printable + letter + digit mappings once + * Call on startup so we can see how each key_code maps to utf8. + * ============================================================ */ +#ifdef __linux__ +void kbd_dump_keymap_table(void) +{ + /* Linux evdev KEY_* values we care about: 2..53 covers 1..0 qwerty zxcvbnm */ + static const struct { uint32_t code; const char *name; } keys[] = { + {KEY_1,"1"},{KEY_2,"2"},{KEY_3,"3"},{KEY_4,"4"},{KEY_5,"5"}, + {KEY_6,"6"},{KEY_7,"7"},{KEY_8,"8"},{KEY_9,"9"},{KEY_0,"0"}, + {KEY_Q,"q"},{KEY_W,"w"},{KEY_E,"e"},{KEY_R,"r"},{KEY_T,"t"}, + {KEY_Y,"y"},{KEY_U,"u"},{KEY_I,"i"},{KEY_O,"o"},{KEY_P,"p"}, + {KEY_A,"a"},{KEY_S,"s"},{KEY_D,"d"},{KEY_F,"f"},{KEY_G,"g"}, + {KEY_H,"h"},{KEY_J,"j"},{KEY_K,"k"},{KEY_L,"l"}, + {KEY_Z,"z"},{KEY_X,"x"},{KEY_C,"c"},{KEY_V,"v"},{KEY_B,"b"}, + {KEY_N,"n"},{KEY_M,"m"}, + {KEY_MINUS,"-"},{KEY_EQUAL,"="},{KEY_LEFTBRACE,"["},{KEY_RIGHTBRACE,"]"}, + {KEY_SEMICOLON,";"},{KEY_APOSTROPHE,"'"},{KEY_GRAVE,"`"}, + {KEY_BACKSLASH,"\\"},{KEY_COMMA,","},{KEY_DOT,"."},{KEY_SLASH,"/"}, + {KEY_SPACE,"SPACE"},{KEY_ENTER,"ENTER"},{KEY_ESC,"ESC"}, + {KEY_BACKSPACE,"BS"},{KEY_TAB,"TAB"}, + {KEY_UP,"UP"},{KEY_DOWN,"DOWN"},{KEY_LEFT,"LEFT"},{KEY_RIGHT,"RIGHT"}, + {KEY_HOME,"HOME"},{KEY_END,"END"},{KEY_DELETE,"DEL"},{KEY_INSERT,"INS"}, + {KEY_LEFTSHIFT,"LSHIFT"},{KEY_LEFTCTRL,"LCTRL"},{KEY_LEFTALT,"LALT"}, + }; + printf("[KBD] ==== evdev key_code -> label table ====\n"); + for (size_t i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) { + printf("[KBD] code=%3u %s\n", keys[i].code, keys[i].name); + } + printf("[KBD] ==== end ====\n"); + fflush(stdout); +} +#else +void kbd_dump_keymap_table(void) {} +#endif + + +#if !LV_USE_SDL +/* ============================================================ + * Parameters + * ============================================================ */ +#define EVDEV_KEYCODE_OFFSET 8 +#define REPEAT_DELAY_MS 500 /* delay before first repeat */ +#define REPEAT_RATE_MS 30 /* interval between subsequent repeats */ + +/* ============================================================ + * libinput open/close callbacks + * ============================================================ */ +static int open_restricted(const char *path, int flags, void *user_data) { + int fd = open(path, flags); + if (fd < 0) { + fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); + return -errno; + } + /* Grab the device exclusively. Without this, the kernel VT keyboard + * handler also feeds keystrokes from the integrated TCA8418 keypad to + * the foreground tty — leaking keys into any shell on tty1 / HDMI + * console at the same time APPLaunch is reading them. EBUSY here is + * non-fatal: another grabber already holds it, libinput will read + * normally without the VT-leak protection. */ + if (ioctl(fd, EVIOCGRAB, 1) < 0 && errno != EBUSY) { + fprintf(stderr, "[KBD] EVIOCGRAB %s failed: %s\n", path, strerror(errno)); + } + return fd; +} +static void close_restricted(int fd, void *user_data) { close(fd); } +static const struct libinput_interface interface = { + .open_restricted = open_restricted, + .close_restricted = close_restricted, +}; -typedef struct { - uint32_t keycode; +/* ============================================================ + * TCA8418 custom keycode mapping table (same as the original one) + * ============================================================ */ +struct tca8418_keymap_entry { + uint32_t keycode; + const char *sym_name; const char *utf8; -} cp0_ctrl_key_utf8_entry_t; - -static const cp0_keymap_entry_t cp0_tca8418_keymap[64] = { - {183, '!'}, {184, '@'}, {185, '#'}, {186, '$'}, {187, '%'}, {188, '^'}, - {189, '&'}, {190, '*'}, {191, '('}, {192, ')'}, {193, '~'}, {194, '`'}, - {195, '+'}, {196, '-'}, {197, '/'}, {198, '\\'}, {199, '{'}, {200, '}'}, - {201, '['}, {202, ']'}, {231, ','}, {232, '.'}, {233, '|'}, {209, '='}, - {210, ':'}, {211, ';'}, {212, '_'}, {213, '?'}, {214, '<'}, {215, '>'}, - {216, '\''}, {217, '"'}, {0,0}, }; - -static const cp0_ctrl_key_utf8_entry_t cp0_ctrl_key_utf8_map[] = { - {KEY_ENTER, "\r"}, {KEY_KPENTER, "\r"}, {KEY_BACKSPACE, "\x7f"}, - {KEY_TAB, "\t"}, {KEY_ESC, "\x1b"}, {KEY_UP, "\033[A"}, - {KEY_DOWN, "\033[B"}, {KEY_RIGHT, "\033[C"}, {KEY_LEFT, "\033[D"}, - {KEY_HOME, "\033[H"}, {KEY_END, "\033[F"}, {KEY_DELETE, "\033[3~"}, - {KEY_INSERT, "\033[2~"}, {KEY_PAGEUP, "\033[5~"}, {KEY_PAGEDOWN, "\033[6~"}, - {KEY_F1, "\033OP"}, {KEY_F2, "\033OQ"}, {KEY_F3, "\033OR"}, - {KEY_F4, "\033OS"}, {KEY_F5, "\033[15~"}, {KEY_F6, "\033[17~"}, - {KEY_F7, "\033[18~"}, {KEY_F8, "\033[19~"}, {KEY_F9, "\033[20~"}, - {KEY_F10, "\033[21~"}, {KEY_F11, "\033[23~"}, {KEY_F12, "\033[24~"}, +static const struct tca8418_keymap_entry tca8418_keymap[] = { + { 183, "exclam", "!" }, + + { 184, "at", "@" }, + { 185, "numbersign", "#" }, + { 186, "dollar", "$" }, + { 187, "percent", "%" }, + { 188, "asciicircum", "^" }, + { 189, "ampersand", "&" }, + { 190, "asterisk", "*" }, + { 191, "parenleft", "(" }, + { 192, "parenright", ")" }, + + { 193, "asciitilde", "~" }, + { 194, "grave", "`" }, + { 195, "plus", "+" }, + { 196, "minus", "-" }, + { 197, "slash", "/" }, + { 198, "backslash", "\\" }, + { 199, "braceleft", "{" }, + { 200, "braceright", "}" }, + { 201, "bracketleft", "[" }, + { 202, "bracketright", "]" }, + + { 231, "comma", "," }, + { 232, "period", "." }, + { 233, "bar", "|" }, + { 209, "equal", "=" }, + { 210, "colon", ":" }, + { 211, "semicolon", ";" }, + { 212, "underscore", "_" }, + { 213, "question", "?" }, + + { 214, "less", "<" }, + { 215, "greater", ">" }, + { 216, "apostrophe", "'" }, + { 217, "quotedbl", "\"" }, }; -typedef struct { - struct udev *udev; - struct libinput *li; - pthread_t thread; - pthread_mutex_t lock; - - struct xkb_context *xkb_ctx; - struct xkb_keymap *xkb_keymap; - struct xkb_state *xkb_state; - - lv_point_t pointer_point; - lv_indev_state_t pointer_state; - lv_coord_t hor_res; - lv_coord_t ver_res; - - cp0_key_event_t key_queue[CP0_KEY_QUEUE_SIZE]; - size_t key_head; - size_t key_tail; - cp0_key_event_t last_key; -} cp0_input_ctx_t; - -static cp0_input_ctx_t g_input_ctx = { - .lock = PTHREAD_MUTEX_INITIALIZER, - .pointer_state = LV_INDEV_STATE_RELEASED, - .last_key = {.key_state = LV_INDEV_STATE_RELEASED}, -}; -static const char *getenv_default(const char *name, const char *dflt) -{ - const char *value = getenv(name); - return (value && value[0] != '\0') ? value : dflt; -} -static int open_restricted(const char *path, int flags, void *user_data) -{ - (void)user_data; - int fd = open(path, flags); - return fd < 0 ? -errno : fd; -} -static void close_restricted(int fd, void *user_data) -{ - (void)user_data; - close(fd); -} -static const struct libinput_interface cp0_libinput_interface = { - .open_restricted = open_restricted, - .close_restricted = close_restricted, -}; -static void cp0_key_queue_push(cp0_input_ctx_t *ctx, const cp0_key_event_t *event) -{ - size_t next_tail = (ctx->key_tail + 1) % CP0_KEY_QUEUE_SIZE; - if (next_tail == ctx->key_head) - ctx->key_head = (ctx->key_head + 1) % CP0_KEY_QUEUE_SIZE; - ctx->key_queue[ctx->key_tail] = *event; - ctx->key_tail = next_tail; -} -static int cp0_key_queue_pop(cp0_input_ctx_t *ctx, cp0_key_event_t *event) -{ - if (ctx->key_head == ctx->key_tail) - return 0; - *event = ctx->key_queue[ctx->key_head]; - ctx->key_head = (ctx->key_head + 1) % CP0_KEY_QUEUE_SIZE; - return 1; -} +#define TCA8418_KEYMAP_SIZE (sizeof(tca8418_keymap)/sizeof(tca8418_keymap[0])) -static const cp0_keymap_entry_t *cp0_tca8418_key_lookup(uint32_t code) -{ - for (size_t i = 0;; i++) { - if (cp0_tca8418_keymap[i].code == 0 && cp0_tca8418_keymap[i].key == 0) - break; - if (cp0_tca8418_keymap[i].code == code) - return &cp0_tca8418_keymap[i]; - } +static const struct tca8418_keymap_entry * +tca8418_keymap_lookup(uint32_t keycode) { + for (size_t i = 0; i < TCA8418_KEYMAP_SIZE; i++) + if (tca8418_keymap[i].keycode == keycode) + return &tca8418_keymap[i]; return NULL; } -static uint32_t cp0_utf8_first_codepoint(const char *s) -{ - const unsigned char *p = (const unsigned char *)s; - if (p[0] == '\0') - return 0; - if (p[0] < 0x80) - return p[0]; - if ((p[0] & 0xe0) == 0xc0 && (p[1] & 0xc0) == 0x80) - return ((uint32_t)(p[0] & 0x1f) << 6) | (uint32_t)(p[1] & 0x3f); - if ((p[0] & 0xf0) == 0xe0 && (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80) - return ((uint32_t)(p[0] & 0x0f) << 12) | ((uint32_t)(p[1] & 0x3f) << 6) | - (uint32_t)(p[2] & 0x3f); - if ((p[0] & 0xf8) == 0xf0 && (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80 && - (p[3] & 0xc0) == 0x80) - return ((uint32_t)(p[0] & 0x07) << 18) | ((uint32_t)(p[1] & 0x3f) << 12) | - ((uint32_t)(p[2] & 0x3f) << 6) | (uint32_t)(p[3] & 0x3f); - return 0; + +/* ============================================================ + * Control-key -> terminal control-character mapping table + * When xkbcommon does not produce utf8 for function keys, fill in ANSI escape sequences as fallback + * ============================================================ */ +struct ctrl_key_utf8_entry { + uint32_t keycode; /* KEY_xxx from linux/input.h */ + const char *utf8; /* terminal control character / ANSI escape sequence */ +}; + +static const struct ctrl_key_utf8_entry ctrl_key_utf8_map[] = { + /* ---- single-byte control characters ---- */ + { KEY_ENTER, "\r" }, /* CR (0x0D) */ + { KEY_KPENTER, "\r" }, /* CR keypad Enter */ + { KEY_BACKSPACE, "\x7f" }, /* DEL (0x7F) */ + { KEY_TAB, "\t" }, /* HT (0x09) */ + { KEY_ESC, "\x1b" }, /* ESC (0x1B) */ + + /* ---- ANSI arrow keys ---- */ + { KEY_UP, "\033[A" }, /* CSI A */ + { KEY_DOWN, "\033[B" }, /* CSI B */ + { KEY_RIGHT, "\033[C" }, /* CSI C */ + { KEY_LEFT, "\033[D" }, /* CSI D */ + + /* ---- ANSI editing keys ---- */ + { KEY_HOME, "\033[H" }, /* CSI H (or "\033[1~") */ + { KEY_END, "\033[F" }, /* CSI F (or "\033[4~") */ + { KEY_DELETE, "\033[3~" }, /* SS3 ~ */ + { KEY_INSERT, "\033[2~" }, /* CSI ~ */ + { KEY_PAGEUP, "\033[5~" }, /* CSI ~ */ + { KEY_PAGEDOWN, "\033[6~" }, /* CSI ~ */ + + /* ---- F1-F12 ---- */ + { KEY_F1, "\033OP" }, + { KEY_F2, "\033OQ" }, + { KEY_F3, "\033OR" }, + { KEY_F4, "\033OS" }, + { KEY_F5, "\033[15~"}, + { KEY_F6, "\033[17~"}, + { KEY_F7, "\033[18~"}, + { KEY_F8, "\033[19~"}, + { KEY_F9, "\033[20~"}, + { KEY_F10, "\033[21~"}, + { KEY_F11, "\033[23~"}, + { KEY_F12, "\033[24~"}, +}; + +static const char *ctrl_key_utf8_lookup(uint32_t keycode) { + for (size_t i = 0; i < sizeof(ctrl_key_utf8_map) / sizeof(ctrl_key_utf8_map[0]); i++) + if (ctrl_key_utf8_map[i].keycode == keycode) + return ctrl_key_utf8_map[i].utf8; + return NULL; } -static uint32_t cp0_keycode_to_lv_key(cp0_input_ctx_t *ctx, uint32_t code) -{ - const cp0_keymap_entry_t *mapped = cp0_tca8418_key_lookup(code); - if (mapped != NULL) - return mapped->key; - - switch (code) { - case KEY_BACKSPACE: - return LV_KEY_BACKSPACE; - case KEY_ENTER: - case KEY_KPENTER: - return LV_KEY_ENTER; - case KEY_UP: - return LV_KEY_UP; - case KEY_DOWN: - return LV_KEY_DOWN; - case KEY_LEFT: - return LV_KEY_LEFT; - case KEY_RIGHT: - return LV_KEY_RIGHT; - case KEY_TAB: - return LV_KEY_NEXT; - case KEY_HOME: - return LV_KEY_HOME; - case KEY_END: - return LV_KEY_END; - case KEY_DELETE: - return LV_KEY_DEL; - case KEY_ESC: - return LV_KEY_ESC; - default: - break; - } - if (ctx->xkb_state == NULL) - return 0; - xkb_keycode_t keycode = code + CP0_EVDEV_KEYCODE_OFFSET; - char utf8[8] = {0}; - if (xkb_state_key_get_utf8(ctx->xkb_state, keycode, utf8, sizeof(utf8)) > 0) - return cp0_utf8_first_codepoint(utf8); - xkb_keysym_t sym = xkb_state_key_get_one_sym(ctx->xkb_state, keycode); - uint32_t codepoint = xkb_keysym_to_utf32(sym); - return codepoint >= 0x20 ? codepoint : 0; -} -static const char *cp0_ctrl_key_utf8_lookup(uint32_t code) +/* ============================================================ + * Keyboard context + * ============================================================ */ +struct kbd_ctx { + struct libinput *li; + struct libinput_device *dev; + + struct xkb_context *ctx; + struct xkb_keymap *keymap; + struct xkb_state *state; + struct xkb_compose_table *compose_table; + struct xkb_compose_state *compose_state; + + /* key repeat */ + int repeat_fd; + bool repeating; + struct key_item repeat_template; /* save the last pressed key for repeat copies */ +}; + +/* ============================================================ + * xkbcommon log callback + * ============================================================ */ +static void uxkb_log(struct xkb_context *ctx, enum xkb_log_level level, + const char *fmt, va_list args) { - for (size_t i = 0; i < sizeof(cp0_ctrl_key_utf8_map) / sizeof(cp0_ctrl_key_utf8_map[0]); i++) { - if (cp0_ctrl_key_utf8_map[i].keycode == code) - return cp0_ctrl_key_utf8_map[i].utf8; - } - return NULL; + (void)ctx; (void)level; + vfprintf(stderr, fmt, args); } -static void cp0_fill_key_event(cp0_input_ctx_t *ctx, cp0_key_event_t *event, uint32_t code, - lv_indev_state_t state) -{ - memset(event, 0, sizeof(*event)); - event->key_code = code; - event->key_state = state; - event->lv_key = cp0_keycode_to_lv_key(ctx, code); - - if (ctx->xkb_state != NULL) { - xkb_keycode_t keycode = code + CP0_EVDEV_KEYCODE_OFFSET; - xkb_keysym_t sym = xkb_state_key_get_one_sym(ctx->xkb_state, keycode); - event->keysym = sym; - event->codepoint = xkb_keysym_to_utf32(sym); - xkb_keysym_get_name(sym, event->sym_name, sizeof(event->sym_name)); - xkb_state_key_get_utf8(ctx->xkb_state, keycode, event->utf8, sizeof(event->utf8)); - } +/* ============================================================ + * modifier bitmask + * ============================================================ */ +static uint32_t get_mods(struct xkb_state *state) { + uint32_t m = 0; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0) + m |= KBD_MOD_SHIFT; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0) + m |= KBD_MOD_CTRL; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0) + m |= KBD_MOD_ALT; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0) + m |= KBD_MOD_LOGO; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CAPS, XKB_STATE_MODS_EFFECTIVE) > 0) + m |= KBD_MOD_CAPS; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_NUM, XKB_STATE_MODS_EFFECTIVE) > 0) + m |= KBD_MOD_NUM; + return m; +} + +/* ============================================================ + * LED update (via libinput; no need to write evdev directly) + * ============================================================ */ +static void update_leds(struct kbd_ctx *kc) { + enum libinput_led leds = 0; + if (xkb_state_led_name_is_active(kc->state, XKB_LED_NAME_NUM) > 0) + leds |= LIBINPUT_LED_NUM_LOCK; + if (xkb_state_led_name_is_active(kc->state, XKB_LED_NAME_CAPS) > 0) + leds |= LIBINPUT_LED_CAPS_LOCK; + if (xkb_state_led_name_is_active(kc->state, XKB_LED_NAME_SCROLL) > 0) + leds |= LIBINPUT_LED_SCROLL_LOCK; + libinput_device_led_update(kc->dev, leds); +} - if (event->utf8[0] == '\0') { - const char *ctrl_utf8 = cp0_ctrl_key_utf8_lookup(code); - if (ctrl_utf8 != NULL) - snprintf(event->utf8, sizeof(event->utf8), "%s", ctrl_utf8); +/* ============================================================ + * Enqueue + * ============================================================ */ +static void enqueue_key(const struct key_item *src) { + struct key_item *elm = malloc(sizeof(*elm)); + if (!elm) return; + *elm = *src; + elm->flage = 0; // mark as needing free + + /* DEBUG: every raw key event from keyboard thread */ + char utf8_dbg[64] = ""; + int di = 0; + for (int i = 0; i < (int)sizeof(elm->utf8) && elm->utf8[i] && di < 60; i++) { + unsigned char c = (unsigned char)elm->utf8[i]; + if (c >= 0x20 && c < 0x7f) utf8_dbg[di++] = (char)c; + else di += snprintf(utf8_dbg+di, 64-di, "\\x%02x", c); + } + utf8_dbg[di] = '\0'; + printf("[KBD] enqueue code=%u state=%s sym=%s utf8='%s' cp=0x%x mods=0x%x run=%d home_flag=%d\n", + elm->key_code, kbd_state_name(elm->key_state), elm->sym_name, + utf8_dbg, elm->codepoint, elm->mods, LVGL_RUN_FLAGE, LVGL_HOME_KEY_FLAG); + + if(elm->key_code == KEY_ESC) { + LVGL_HOME_KEY_FLAG = elm->key_state; + printf("[KBD] LVGL_HOME_KEY_FLAG := %d\n", LVGL_HOME_KEY_FLAG); } - if (event->utf8[0] == '\0') { - const cp0_keymap_entry_t *mapped = cp0_tca8418_key_lookup(code); - if (mapped != NULL) - snprintf(event->utf8, sizeof(event->utf8), "%c", (char)mapped->key); + if(LVGL_RUN_FLAGE) + { + pthread_mutex_lock(&keyboard_mutex); + STAILQ_INSERT_TAIL(&keyboard_queue, elm, entries); + pthread_mutex_unlock(&keyboard_mutex); + } + else + { + printf("[KBD] dropped (LVGL_RUN_FLAGE=0, external app running)\n"); + free(elm); } -} -static void cp0_send_keyboard_event(const cp0_key_event_t *event) -{ - lv_obj_t *root = lv_display_get_screen_active(NULL); - if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_KEYBOARD], (void *)event); } -static int cp0_init_xkb(cp0_input_ctx_t *ctx) -{ - ctx->xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if (ctx->xkb_ctx == NULL) - return -1; - - struct xkb_rule_names names = { - .rules = getenv_default("LV_LINUX_XKB_RULES", "evdev"), - .model = getenv_default("LV_LINUX_XKB_MODEL", "pc101"), - .layout = getenv_default("LV_LINUX_XKB_LAYOUT", "us"), - .variant = getenv("LV_LINUX_XKB_VARIANT"), - .options = getenv("LV_LINUX_XKB_OPTIONS"), +/* ============================================================ + * Key repeat control + * ============================================================ */ +static void repeat_start(struct kbd_ctx *kc) { + struct itimerspec ts = { + .it_interval = { .tv_sec = 0, .tv_nsec = (long)REPEAT_RATE_MS * 1000000L }, + .it_value = { .tv_sec = 0, .tv_nsec = (long)REPEAT_DELAY_MS * 1000000L }, }; + timerfd_settime(kc->repeat_fd, 0, &ts, NULL); + kc->repeating = true; +} +static void repeat_stop(struct kbd_ctx *kc) { + struct itimerspec ts = {0}; + timerfd_settime(kc->repeat_fd, 0, &ts, NULL); + kc->repeating = false; +} - ctx->xkb_keymap = xkb_keymap_new_from_names(ctx->xkb_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); - if (ctx->xkb_keymap == NULL) { - struct xkb_rule_names fallback = {0}; - ctx->xkb_keymap = xkb_keymap_new_from_names(ctx->xkb_ctx, &fallback, XKB_KEYMAP_COMPILE_NO_FLAGS); +/* Encode a UTF-32 code point as UTF-8 and return the byte count */ +static int utf32_to_utf8(uint32_t cp, char *out, size_t n) { + if (n < 1) return 0; + if (cp < 0x80) { + if (n < 2) return 0; + out[0] = (char)cp; out[1] = '\0'; return 1; + } else if (cp < 0x800) { + if (n < 3) return 0; + out[0] = 0xC0 | (cp >> 6); + out[1] = 0x80 | (cp & 0x3F); + out[2] = '\0'; return 2; + } else if (cp < 0x10000) { + if (n < 4) return 0; + out[0] = 0xE0 | (cp >> 12); + out[1] = 0x80 | ((cp >> 6) & 0x3F); + out[2] = 0x80 | (cp & 0x3F); + out[3] = '\0'; return 3; + } else if (cp < 0x110000) { + if (n < 5) return 0; + out[0] = 0xF0 | (cp >> 18); + out[1] = 0x80 | ((cp >> 12) & 0x3F); + out[2] = 0x80 | ((cp >> 6) & 0x3F); + out[3] = 0x80 | (cp & 0x3F); + out[4] = '\0'; return 4; } - if (ctx->xkb_keymap == NULL) - return -1; - - ctx->xkb_state = xkb_state_new(ctx->xkb_keymap); - return ctx->xkb_state == NULL ? -1 : 0; + out[0] = '\0'; return 0; } -static void cp0_handle_pointer_event(cp0_input_ctx_t *ctx, struct libinput_event *event) +/* ============================================================ + * Core: handle one key event + * ============================================================ */ +static void process_key(struct kbd_ctx *kc, uint32_t code, int pressed) { - struct libinput_event_pointer *pointer_event = NULL; - struct libinput_event_touch *touch_event = NULL; - enum libinput_event_type type = libinput_event_get_type(event); - lv_coord_t hor_res = ctx->hor_res; - lv_coord_t ver_res = ctx->ver_res; - if (hor_res <= 0 || ver_res <= 0) + xkb_keycode_t keycode = code + EVDEV_KEYCODE_OFFSET; + struct key_item item = {0}; + item.key_code = code; + item.key_state = pressed ? KBD_KEY_PRESSED : KBD_KEY_RELEASED; + + /* ---------- 1. TCA8418 custom keycodes first ---------- */ + const struct tca8418_keymap_entry *mapped = tca8418_keymap_lookup(code); + if (mapped) { + xkb_keysym_t sym = xkb_keysym_from_name(mapped->sym_name, + XKB_KEYSYM_NO_FLAGS); + snprintf(item.sym_name, sizeof(item.sym_name), "%s", mapped->sym_name); + snprintf(item.utf8, sizeof(item.utf8), "%s", mapped->utf8); + item.keysym = sym; + item.codepoint = (sym != XKB_KEY_NoSymbol) ? xkb_keysym_to_utf32(sym) : 0; + item.mods = get_mods(kc->state); + + /* repeat handling */ + if (pressed) { + kc->repeat_template = item; + kc->repeat_template.key_state = KBD_KEY_REPEATED; + repeat_start(kc); + } else if (kc->repeating && kc->repeat_template.key_code == code) { + repeat_stop(kc); + } + enqueue_key(&item); return; + } + + /* ---------- 2. standard xkbcommon flow ---------- */ + const xkb_keysym_t *syms; + int num = xkb_state_key_get_syms(kc->state, keycode, &syms); + xkb_keysym_t one_sym = XKB_KEY_NoSymbol; - pthread_mutex_lock(&ctx->lock); - switch (type) { - case LIBINPUT_EVENT_POINTER_MOTION: - pointer_event = libinput_event_get_pointer_event(event); - ctx->pointer_point.x = (lv_coord_t)LV_CLAMP(0, ctx->pointer_point.x + - libinput_event_pointer_get_dx(pointer_event), - hor_res - 1); - ctx->pointer_point.y = (lv_coord_t)LV_CLAMP(0, ctx->pointer_point.y + - libinput_event_pointer_get_dy(pointer_event), - ver_res - 1); - break; - case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: - pointer_event = libinput_event_get_pointer_event(event); - ctx->pointer_point.x = - (lv_coord_t)libinput_event_pointer_get_absolute_x_transformed(pointer_event, hor_res); - ctx->pointer_point.y = - (lv_coord_t)libinput_event_pointer_get_absolute_y_transformed(pointer_event, ver_res); - break; - case LIBINPUT_EVENT_POINTER_BUTTON: - pointer_event = libinput_event_get_pointer_event(event); - ctx->pointer_state = - libinput_event_pointer_get_button_state(pointer_event) == LIBINPUT_BUTTON_STATE_PRESSED - ? LV_INDEV_STATE_PRESSED - : LV_INDEV_STATE_RELEASED; - break; - case LIBINPUT_EVENT_TOUCH_DOWN: - case LIBINPUT_EVENT_TOUCH_MOTION: - touch_event = libinput_event_get_touch_event(event); - ctx->pointer_point.x = (lv_coord_t)libinput_event_touch_get_x_transformed(touch_event, hor_res); - ctx->pointer_point.y = (lv_coord_t)libinput_event_touch_get_y_transformed(touch_event, ver_res); - ctx->pointer_state = LV_INDEV_STATE_PRESSED; - break; - case LIBINPUT_EVENT_TOUCH_UP: - ctx->pointer_state = LV_INDEV_STATE_RELEASED; - break; - default: - break; + if (num == 1) { + /* handle Lock modifiers (following uterm + libxkbcommon recommendations) */ + one_sym = xkb_state_key_get_one_sym(kc->state, keycode); + } else if (num > 1) { + one_sym = syms[0]; } - pthread_mutex_unlock(&ctx->lock); -} -static void cp0_handle_keyboard_event(cp0_input_ctx_t *ctx, struct libinput_event *event) -{ - if (libinput_event_get_type(event) != LIBINPUT_EVENT_KEYBOARD_KEY) - return; + /* ---------- 3. Compose handling (dead keys, etc.) ---------- */ + enum xkb_compose_status cstatus = XKB_COMPOSE_NOTHING; + bool compose_produced_utf8 = false; + + if (kc->compose_state && pressed) { + xkb_compose_state_feed(kc->compose_state, one_sym); + cstatus = xkb_compose_state_get_status(kc->compose_state); + + if (cstatus == XKB_COMPOSE_COMPOSED) { + xkb_keysym_t csym = xkb_compose_state_get_one_sym(kc->compose_state); + if (csym != XKB_KEY_NoSymbol) { + one_sym = csym; + } + /* get the composed UTF-8 string */ + int n = xkb_compose_state_get_utf8(kc->compose_state, + item.utf8, sizeof(item.utf8)); + if (n > 0) compose_produced_utf8 = true; + + /* If neither keysym nor utf8 is available, treat it as canceled */ + if (csym == XKB_KEY_NoSymbol && !compose_produced_utf8) + cstatus = XKB_COMPOSE_CANCELLED; + } + if (cstatus == XKB_COMPOSE_COMPOSED || cstatus == XKB_COMPOSE_CANCELLED) + xkb_compose_state_reset(kc->compose_state); + } - struct libinput_event_keyboard *keyboard_event = libinput_event_get_keyboard_event(event); - uint32_t code = libinput_event_keyboard_get_key(keyboard_event); - enum libinput_key_state key_state = libinput_event_keyboard_get_key_state(keyboard_event); - lv_indev_state_t state = - key_state == LIBINPUT_KEY_STATE_PRESSED ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + /* ---------- 4. update xkb state (must be after get_syms) ---------- */ + enum xkb_state_component changed = 0; + if (pressed) + changed = xkb_state_update_key(kc->state, keycode, XKB_KEY_DOWN); + else + changed = xkb_state_update_key(kc->state, keycode, XKB_KEY_UP); + if (changed & XKB_STATE_LEDS) + update_leds(kc); + + /* ---------- 5. filter events that are composing or canceled ---------- */ + if (cstatus == XKB_COMPOSE_COMPOSING || cstatus == XKB_COMPOSE_CANCELLED) + return; + if (num <= 0 && !compose_produced_utf8) + return; - cp0_key_event_t event_data; - cp0_fill_key_event(ctx, &event_data, code, state); + /* ---------- 6. fill item ---------- */ + xkb_keysym_get_name(one_sym, item.sym_name, sizeof(item.sym_name)); + item.keysym = one_sym; + item.codepoint = (one_sym != XKB_KEY_NoSymbol) + ? xkb_keysym_to_utf32(one_sym) : 0; + item.mods = get_mods(kc->state); + + /* If compose did not provide utf8, get it from xkb_state */ + if (item.utf8[0] == '\0') { + xkb_state_key_get_utf8(kc->state, keycode, + item.utf8, sizeof(item.utf8)); + if (item.utf8[0] == '\0' && item.codepoint != 0) { + /* get_utf8 filters control characters; fall back to manual encoding */ + utf32_to_utf8(item.codepoint, item.utf8, sizeof(item.utf8)); + } + if (item.codepoint == 0) + item.codepoint = xkb_state_key_get_utf32(kc->state, keycode); + } + + /* ---------- 6.5 control-key fallback mapping ---------- */ + /* xkbcommon does not produce utf8 for function keys (UP/DOWN/ENTER/BACKSPACE, etc.), + * manually fill ANSI/VT100 terminal control characters here for upper layers */ + if (item.utf8[0] == '\0') { + const char *ctrl = ctrl_key_utf8_lookup(code); + if (ctrl) { + snprintf(item.utf8, sizeof(item.utf8), "%s", ctrl); + } + } - if (ctx->xkb_state != NULL) { - xkb_state_update_key(ctx->xkb_state, code + CP0_EVDEV_KEYCODE_OFFSET, - key_state == LIBINPUT_KEY_STATE_PRESSED ? XKB_KEY_DOWN : XKB_KEY_UP); + /* ---------- 7. repeat control ---------- */ + if (pressed && xkb_keymap_key_repeats(kc->keymap, keycode)) { + kc->repeat_template = item; + kc->repeat_template.key_state = KBD_KEY_REPEATED; + repeat_start(kc); + } else if (!pressed && kc->repeating && + kc->repeat_template.key_code == code) { + repeat_stop(kc); } - pthread_mutex_lock(&ctx->lock); - cp0_key_queue_push(ctx, &event_data); - pthread_mutex_unlock(&ctx->lock); + /* ---------- 8. Enqueue ---------- */ + enqueue_key(&item); } -static void *cp0_input_thread(void *arg) +/* ============================================================ + * xkb initialization (with rmlvo fallback + compose) + * ============================================================ */ +static int init_xkb(struct kbd_ctx *kc, + const char *layout, const char *locale) { - cp0_input_ctx_t *ctx = (cp0_input_ctx_t *)arg; - struct pollfd fds = { - .fd = libinput_get_fd(ctx->li), - .events = POLLIN, + kc->ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (!kc->ctx) { fprintf(stderr, "xkb_context_new failed\n"); return -1; } + xkb_context_set_log_fn(kc->ctx, uxkb_log); + + struct xkb_rule_names rmlvo = { + .rules = "evdev", + .model = NULL, + .layout = layout ? layout : "us", + .variant = NULL, + .options = NULL, }; + kc->keymap = xkb_keymap_new_from_names(kc->ctx, &rmlvo, + XKB_KEYMAP_COMPILE_NO_FLAGS); + if (!kc->keymap) { + /* empty rmlvo fallback */ + struct xkb_rule_names empty = {0}; + kc->keymap = xkb_keymap_new_from_names(kc->ctx, &empty, + XKB_KEYMAP_COMPILE_NO_FLAGS); + } + if (!kc->keymap) { fprintf(stderr, "failed to create keymap\n"); return -1; } - while (1) { - if (poll(&fds, 1, -1) <= 0) - continue; + kc->state = xkb_state_new(kc->keymap); + if (!kc->state) { fprintf(stderr, "failed to create xkb_state\n"); return -1; } - libinput_dispatch(ctx->li); - struct libinput_event *event = NULL; - while ((event = libinput_get_event(ctx->li)) != NULL) { - cp0_handle_pointer_event(ctx, event); - cp0_handle_keyboard_event(ctx, event); - libinput_event_destroy(event); - } + /* Compose table */ + if (!locale || !*locale) { + locale = getenv("LC_ALL"); + if (!locale || !*locale) locale = getenv("LC_CTYPE"); + if (!locale || !*locale) locale = getenv("LANG"); + if (!locale || !*locale) locale = "C"; } - - return NULL; + kc->compose_table = xkb_compose_table_new_from_locale( + kc->ctx, locale, XKB_COMPOSE_COMPILE_NO_FLAGS); + if (kc->compose_table) { + kc->compose_state = xkb_compose_state_new(kc->compose_table, + XKB_COMPOSE_STATE_NO_FLAGS); + if (!kc->compose_state) + fprintf(stderr, "Warning: failed to create compose_state; disabling compose\n"); + } else { + fprintf(stderr, "Warning: locale=%s has no compose table\n", locale); + } + return 0; } -static void cp0_pointer_read_cb(lv_indev_t *indev, lv_indev_data_t *data) -{ - (void)indev; - cp0_input_ctx_t *ctx = &g_input_ctx; - - pthread_mutex_lock(&ctx->lock); - data->point = ctx->pointer_point; - data->state = ctx->pointer_state; - pthread_mutex_unlock(&ctx->lock); +static void free_xkb(struct kbd_ctx *kc) { + if (kc->compose_state) xkb_compose_state_unref(kc->compose_state); + if (kc->compose_table) xkb_compose_table_unref(kc->compose_table); + if (kc->state) xkb_state_unref(kc->state); + if (kc->keymap) xkb_keymap_unref(kc->keymap); + if (kc->ctx) xkb_context_unref(kc->ctx); } -static void cp0_keyboard_read_cb(lv_indev_t *indev, lv_indev_data_t *data) -{ - (void)indev; - cp0_input_ctx_t *ctx = &g_input_ctx; - bool has_event = false; - bool continue_reading = false; - cp0_key_event_t event; - - pthread_mutex_lock(&ctx->lock); - event = ctx->last_key; - if (cp0_key_queue_pop(ctx, &event)) { - ctx->last_key = event; - has_event = true; - } - data->key = event.lv_key; - data->state = (lv_indev_state_t)event.key_state; - continue_reading = ctx->key_head != ctx->key_tail; - data->continue_reading = continue_reading; - pthread_mutex_unlock(&ctx->lock); - - if (has_event) - cp0_send_keyboard_event(&event); +/* Optional: rebuild state on VT wakeup while preserving locked mods/layout (see uxkb_dev_wake_up) */ +static void kbd_wake_up(struct kbd_ctx *kc) { + xkb_mod_mask_t locked_mods = xkb_state_serialize_mods(kc->state, + XKB_STATE_MODS_LOCKED); + xkb_layout_index_t locked_layout = xkb_state_serialize_layout( + kc->state, XKB_STATE_LAYOUT_LOCKED); + xkb_state_unref(kc->state); + kc->state = xkb_state_new(kc->keymap); + if (!kc->state) return; + xkb_state_update_mask(kc->state, 0, 0, locked_mods, 0, 0, locked_layout); + update_leds(kc); + if (kc->compose_state) xkb_compose_state_reset(kc->compose_state); } +/* ============================================================ + * Thread main loop + * ============================================================ */ +void *keyboard_read_thread(void *argv) { + STAILQ_INIT(&keyboard_queue); -void init_input() -{ - cp0_input_ctx_t *ctx = &g_input_ctx; - const char *seat = getenv_default("LV_LINUX_INPUT_SEAT", CP0_DEFAULT_INPUT_SEAT); - lv_display_t *disp = lv_display_get_default(); + const char *device_path = argv ? (const char *)argv + : "/dev/input/by-path/platform-3f804000.i2c-event"; - if (disp != NULL) { - ctx->hor_res = lv_display_get_horizontal_resolution(disp); - ctx->ver_res = lv_display_get_vertical_resolution(disp); - } + struct kbd_ctx kc = {0}; + kc.repeat_fd = -1; - if (cp0_init_xkb(ctx) != 0) - fprintf(stderr, "cp0_lvgl: xkb init failed, keyboard text input will be limited\n"); + /* ---------- 1. libinput ---------- */ + kc.li = libinput_path_create_context(&interface, NULL); + if (!kc.li) { fprintf(stderr, "failed to create libinput context\n"); goto out; } - ctx->udev = udev_new(); - if (ctx->udev == NULL) { - fprintf(stderr, "cp0_lvgl: udev_new failed\n"); - return; + kc.dev = libinput_path_add_device(kc.li, device_path); + if (!kc.dev) { + fprintf(stderr, "Failed to add device %s (root permissions may be required)\n", device_path); + goto out; } - - ctx->li = libinput_udev_create_context(&cp0_libinput_interface, NULL, ctx->udev); - if (ctx->li == NULL) { - fprintf(stderr, "cp0_lvgl: libinput_udev_create_context failed\n"); - return; + if (!libinput_device_has_capability(kc.dev, LIBINPUT_DEVICE_CAP_KEYBOARD)) { + fprintf(stderr, "%s is not a keyboard device\n", device_path); + goto out; } - if (libinput_udev_assign_seat(ctx->li, seat) != 0) { - fprintf(stderr, "cp0_lvgl: failed to assign input seat %s\n", seat); - return; - } + /* ---------- 2. xkbcommon ---------- */ + if (init_xkb(&kc, "us", NULL) < 0) goto out; - lv_indev_t *pointer = lv_indev_create(); - if (pointer != NULL) { - lv_indev_set_type(pointer, LV_INDEV_TYPE_POINTER); - lv_indev_set_read_cb(pointer, cp0_pointer_read_cb); - } + /* ---------- 3. key repeat timerfd ---------- */ + kc.repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (kc.repeat_fd < 0) { perror("timerfd_create"); goto out; } - lv_indev_t *keyboard = lv_indev_create(); - if (keyboard != NULL) { - lv_indev_set_type(keyboard, LV_INDEV_TYPE_KEYPAD); - lv_indev_set_read_cb(keyboard, cp0_keyboard_read_cb); - } + /* ---------- 4. event loop ---------- */ + int li_fd = libinput_get_fd(kc.li); + struct pollfd pfds[2] = { + { .fd = li_fd, .events = POLLIN }, + { .fd = kc.repeat_fd, .events = POLLIN }, + }; - if (pthread_create(&ctx->thread, NULL, cp0_input_thread, ctx) != 0) { - fprintf(stderr, "cp0_lvgl: failed to start input thread\n"); - return; + g_libinput = kc.li; + printf("Start listening for keyboard input (%s)\n", device_path); + libinput_dispatch(kc.li); + + while (1) { + if (keyboard_paused_flag) { + usleep(50000); + continue; + } + int pr = poll(pfds, 2, 100); + if (pr < 0) { + if (errno == EINTR) continue; + perror("poll"); break; + } + if (pr == 0) continue; + + /* keyboard event */ + if (pfds[0].revents & POLLIN) { + libinput_dispatch(kc.li); + struct libinput_event *ev; + while ((ev = libinput_get_event(kc.li)) != NULL) { + if (libinput_event_get_type(ev) == LIBINPUT_EVENT_KEYBOARD_KEY) { + struct libinput_event_keyboard *kev = + libinput_event_get_keyboard_event(ev); + uint32_t code = libinput_event_keyboard_get_key(kev); + enum libinput_key_state ks = + libinput_event_keyboard_get_key_state(kev); + process_key(&kc, code, + ks == LIBINPUT_KEY_STATE_PRESSED ? 1 : 0); + } + libinput_event_destroy(ev); + } + } + + /* repeat timer triggered */ + if (pfds[1].revents & POLLIN) { + uint64_t exp; + while (read(kc.repeat_fd, &exp, sizeof(exp)) == sizeof(exp)) { + if (kc.repeating) { + /* refresh mods (prevents Shift, etc. from changing during repeat) */ + kc.repeat_template.mods = get_mods(kc.state); + enqueue_key(&kc.repeat_template); + } + } + } } - pthread_detach(ctx->thread); + +out: + if (kc.repeat_fd >= 0) close(kc.repeat_fd); + free_xkb(&kc); + if (kc.li) libinput_unref(kc.li); + return NULL; } + +/* APPLaunch still installs its own LVGL keypad read callback, so this + * compatibility symbol intentionally does not create another input device. */ +void init_input(void) {} + +#endif diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c index 65a2372f..32a8a236 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c @@ -1,4 +1,5 @@ #include "hal_lvgl_bsp.h" +#include "keyboard_input.h" #include "commount.h" #include "lvgl/lvgl.h" #include "sdl_lvgl.h" @@ -18,17 +19,13 @@ #define KEYBOARD_BUFFER_SIZE 32 #endif -#define CP0_KBD_MOD_SHIFT (1u << 0) -#define CP0_KBD_MOD_CTRL (1u << 1) -#define CP0_KBD_MOD_ALT (1u << 2) -#define CP0_KBD_MOD_LOGO (1u << 3) typedef struct { char buf[KEYBOARD_BUFFER_SIZE]; bool dummy_read; - cp0_key_event_t current; - cp0_key_event_t last; + struct key_item current; + struct key_item last; size_t last_utf8_len; bool current_valid; } cp0_sdl_keyboard_t; @@ -347,11 +344,51 @@ static uint32_t cp0_sdl_scancode_to_linux_key(SDL_Scancode scancode) } } -static void cp0_send_keyboard_event(const cp0_key_event_t *event) +struct keyboard_queue_t keyboard_queue; +pthread_mutex_t keyboard_mutex = PTHREAD_MUTEX_INITIALIZER; +volatile int LVGL_HOME_KEY_FLAG = 0; +volatile int LVGL_RUN_FLAGE = 1; +volatile uint32_t LV_EVENT_KEYBOARD; + +void keyboard_pause(void) {} +void keyboard_resume(void) {} +void *keyboard_read_thread(void *argv) +{ + (void)argv; + return NULL; +} +void kbd_dump_keymap_table(void) {} + +const char *kbd_state_name(int state) +{ + switch (state) { + case KBD_KEY_RELEASED: + return "UP"; + case KBD_KEY_PRESSED: + return "DOWN"; + case KBD_KEY_REPEATED: + return "REPEAT"; + default: + return "???"; + } +} + +static void cp0_send_keyboard_event(const struct key_item *event) { - lv_obj_t *root = lv_display_get_screen_active(NULL); + struct key_item *elm = calloc(1, sizeof(*elm)); + if (elm == NULL) + return; + *elm = *event; + elm->flage = 1; + + if (elm->key_code == KEY_ESC) + LVGL_HOME_KEY_FLAG = elm->key_state; + + lv_obj_t *root = lv_screen_active(); if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_KEYBOARD], (void *)event); + lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); + + free(elm); } static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEvent *event) @@ -366,15 +403,16 @@ static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEve kbd->current.keysym = (uint32_t)sym; kbd->current.mods = 0; kbd->current.key_state = LV_INDEV_STATE_PRESSED; - kbd->current.lv_key = cp0_sdl_ctrl_to_lv_key(sym); + uint32_t lv_key = cp0_sdl_ctrl_to_lv_key(sym); + kbd->current.codepoint = lv_key; if (mods & KMOD_SHIFT) - kbd->current.mods |= CP0_KBD_MOD_SHIFT; + kbd->current.mods |= KBD_MOD_SHIFT; if (mods & KMOD_CTRL) - kbd->current.mods |= CP0_KBD_MOD_CTRL; + kbd->current.mods |= KBD_MOD_CTRL; if (mods & KMOD_ALT) - kbd->current.mods |= CP0_KBD_MOD_ALT; + kbd->current.mods |= KBD_MOD_ALT; if (mods & KMOD_GUI) - kbd->current.mods |= CP0_KBD_MOD_LOGO; + kbd->current.mods |= KBD_MOD_LOGO; if (name != NULL) snprintf(kbd->current.sym_name, sizeof(kbd->current.sym_name), "%s", name); @@ -393,17 +431,10 @@ static void cp0_sdl_set_text_key(cp0_sdl_keyboard_t *kbd, const char *utf8, size kbd->current_valid = true; } - if (kbd->current.lv_key != 0 && len == 1 && (uint8_t)utf8[0] == (uint8_t)kbd->current.lv_key) { - if (kbd->current.codepoint == 0) - kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); - return; - } - size_t n = len < sizeof(kbd->current.utf8) - 1 ? len : sizeof(kbd->current.utf8) - 1; memcpy(kbd->current.utf8, utf8, n); kbd->current.utf8[n] = '\0'; kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); - kbd->current.lv_key = kbd->current.codepoint; } static void cp0_sdl_enqueue_text(cp0_sdl_keyboard_t *kbd, const char *text) @@ -447,7 +478,7 @@ static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data) if (kbd->dummy_read) { kbd->dummy_read = false; kbd->last.key_state = LV_INDEV_STATE_RELEASED; - data->key = kbd->last.lv_key; + data->key = kbd->last.codepoint; data->state = LV_INDEV_STATE_RELEASED; cp0_send_keyboard_event(&kbd->last); memset(&kbd->last, 0, sizeof(kbd->last)); @@ -469,7 +500,7 @@ static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data) kbd->last = kbd->current; kbd->last_utf8_len = char_len; - data->key = kbd->current.lv_key; + data->key = kbd->current.codepoint; data->state = LV_INDEV_STATE_PRESSED; kbd->dummy_read = true; cp0_send_keyboard_event(&kbd->current); From 82f765c7b6d5be2515db4df539705e820175fde5 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 15:23:23 +0800 Subject: [PATCH 13/70] Centralize APPLaunch hardware init in cp0_lvgl Route APPLaunch startup through cp0_lvgl_init() so cp0_lvgl owns framebuffer, input, audio, battery, and camera initialization. The launcher now only prepares app-specific environment values before calling the cp0_lvgl entry point. Move the Linux keypad LVGL read callback and evdev key conversion into cp0_lvgl_keyboard.c. init_input() now registers the keyboard event, starts keyboard_read_thread(), and creates the LVGL keypad/pointer input devices while preserving the existing LV_EVENT_KEYBOARD and struct key_item channel used by APPLaunch UI code. Move framebuffer device discovery and force-refresh setup into cp0_lvgl_freambuffer.c, removing the duplicate get_st7789v_fbdev and display setup code from main.cpp. Replace discrete APPLaunch battery/DELL event globals with lv_c_event slots. LV_EVENT_BATTERY and LV_EVENT_DELL_CPP_DATA now map to CP0_C_EVENT_BATTERY and CP0_C_EVENT_DELL_CPP_DATA, and battery payloads are unified as cp0_battery_info_t pointers. Validation: reviewed README_ZH.md build instructions, then ran export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed successfully. --- .../cp0_lvgl/include/hal_lvgl_bsp.h | 1 + ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 2 +- .../cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp | 7 +- .../cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c | 80 +++--- .../cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c | 123 ++++++++- projects/APPLaunch/main/src/app_battery.c | 4 +- projects/APPLaunch/main/src/main.cpp | 249 +----------------- projects/APPLaunch/main/ui/ui.h | 12 +- 8 files changed, 183 insertions(+), 295 deletions(-) diff --git a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h index 5071ac20..a549729a 100644 --- a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h +++ b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h @@ -9,6 +9,7 @@ typedef enum { CP0_C_EVENT_BATTERY, CP0_C_EVENT_NETWORK, CP0_C_EVENT_DATATIME, + CP0_C_EVENT_DELL_CPP_DATA, CP0_C_EVENT_END, } CP0_C_EVENT_t; diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index e87806c6..d5441d2f 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -6,7 +6,7 @@ void cp0_lvgl_init(void) { init_lvgl_event(); init_freambuffer_disp(); - // init_input(); + init_input(); init_audio(); init_battery(); init_camera(); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp index 3be8a532..85d908eb 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp @@ -1,6 +1,7 @@ #include "hal_lvgl_bsp.h" #include "lvgl/lvgl.h" #include "cp0_lvgl.h" +#include "cp0_lvgl_app.h" #include #include #include @@ -18,13 +19,13 @@ class BatterySystem { if (lv_c_event[CP0_C_EVENT_BATTERY] != 0) { - int capacity = get_battery_value(); - if (capacity >= 0) + cp0_battery_info_t info = cp0_battery_read(); + if (info.valid) { // lv_lock(); lv_obj_t *root = lv_display_get_screen_active(NULL); if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_BATTERY], (void *)&capacity); + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_BATTERY], (void *)&info); // lv_unlock(); } } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c index dfd754b5..a381b9b2 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c @@ -1,59 +1,63 @@ #include "hal_lvgl_bsp.h" #include "lvgl/lvgl.h" #include "commount.h" -#include -#include -#include -#include -#include -#include -#include #include #include #include #include -#include #include "cp0_lvgl.h" +static int find_st7789v_fbdev(char *dev_path, size_t buf_size) +{ + if (dev_path == NULL || buf_size == 0) + return -1; + + if (access("/dev/fb_lcd", F_OK) == 0) { + snprintf(dev_path, buf_size, "/dev/fb_lcd"); + return 0; + } + + FILE *fp = fopen("/proc/fb", "r"); + if (fp != NULL) { + char line[256]; + int fb_num = -1; + while (fgets(line, sizeof(line), fp) != NULL) { + if (strstr(line, "fb_st7789v") != NULL || strstr(line, "st7789") != NULL) { + if (sscanf(line, "%d", &fb_num) == 1) + break; + } + } + fclose(fp); + if (fb_num >= 0) { + snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); + return 0; + } + } + + snprintf(dev_path, buf_size, "/dev/fb0"); + return 0; +} void init_freambuffer_disp() { lv_display_t *disp = lv_linux_fbdev_create(); - if (disp == NULL) - { + if (disp == NULL) { printf("Failed to create fbdev display!\n"); return; } - const char *device = getenv("LV_LINUX_FBDEV_DEVICE"); - char fbdev[32] = {0}; - if (device == NULL) - while (0) - { - FILE *fp = popen("grep st7789 /proc/fb | awk '{print $1}'", "r"); - if (fp == NULL) - { - perror("popen failed"); - break; - } - - char fb_num[32] = {0}; - if (fgets(fb_num, sizeof(fb_num), fp) == NULL) - { - fprintf(stderr, "st7789 framebuffer not found in /proc/fb\n"); - pclose(fp); - break; - } - pclose(fp); - fb_num[strcspn(fb_num, "\r\n")] = '\0'; - snprintf(fbdev, sizeof(fbdev), "/dev/fb%s", fb_num); - device = fbdev; - } - if (device == NULL) - { - snprintf(fbdev, sizeof(fbdev), "/dev/fb%d", 0); + const char *device = getenv("LV_LINUX_FBDEV_DEVICE"); + char fbdev[64] = {0}; + if (device == NULL || device[0] == '\0') { + find_st7789v_fbdev(fbdev, sizeof(fbdev)); device = fbdev; } + printf("Using framebuffer device: %s\n", device); lv_linux_fbdev_set_file(disp, device); -} \ No newline at end of file + lv_linux_fbdev_set_force_refresh(disp, true); + + lv_coord_t w = lv_display_get_horizontal_resolution(disp); + lv_coord_t h = lv_display_get_vertical_resolution(disp); + printf("Framebuffer resolution: %dx%d\n", w, h); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c index 11f68e07..6fcf8137 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c @@ -102,8 +102,106 @@ void kbd_dump_keymap_table(void) void kbd_dump_keymap_table(void) {} #endif +__attribute__((weak)) void ui_global_hint_on_key(const struct key_item *elm) +{ + (void)elm; +} + +static const char *getenv_default(const char *name, const char *dflt) +{ + const char *value = getenv(name); + return (value && value[0] != '\0') ? value : dflt; +} + +static int cp0_evdev_process_key(uint16_t code) +{ + switch (code) { + case KEY_UP: + return LV_KEY_UP; + case KEY_DOWN: + return LV_KEY_DOWN; + case KEY_RIGHT: + return LV_KEY_RIGHT; + case KEY_LEFT: + return LV_KEY_LEFT; + case KEY_ESC: + return LV_KEY_ESC; + case KEY_DELETE: + return LV_KEY_DEL; + case KEY_BACKSPACE: + return LV_KEY_BACKSPACE; + case KEY_ENTER: + return LV_KEY_ENTER; + case KEY_NEXT: + return LV_KEY_NEXT; + case KEY_TAB: + return KEY_TAB; + case KEY_PREVIOUS: + return LV_KEY_PREV; + case KEY_HOME: + return LV_KEY_HOME; + case KEY_END: + return LV_KEY_END; + default: + return code; + } +} + +static void cp0_keypad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) +{ + (void)indev; + + data->state = LV_INDEV_STATE_RELEASED; + data->continue_reading = false; + + pthread_mutex_lock(&keyboard_mutex); + if (!STAILQ_EMPTY(&keyboard_queue)) { + struct key_item *elm = STAILQ_FIRST(&keyboard_queue); + STAILQ_REMOVE_HEAD(&keyboard_queue, entries); + + char utf8_dbg[64] = ""; + int di = 0; + for (int i = 0; i < (int)sizeof(elm->utf8) && elm->utf8[i] && di < 60; i++) { + unsigned char c = (unsigned char)elm->utf8[i]; + if (c >= 0x20 && c < 0x7f) + utf8_dbg[di++] = (char)c; + else + di += snprintf(utf8_dbg + di, 64 - di, "\\x%02x", c); + } + utf8_dbg[di] = '\0'; + printf("[INDEV] dequeue code=%u state=%s sym=%s utf8='%s' cp=0x%x active_screen=%p\n", + elm->key_code, kbd_state_name(elm->key_state), elm->sym_name, + utf8_dbg, elm->codepoint, (void *)lv_screen_active()); + + lv_obj_t *root = lv_screen_active(); + if (root) + lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); + + ui_global_hint_on_key(elm); + + data->key = cp0_evdev_process_key(elm->key_code); + if (data->key) { + data->state = (lv_indev_state_t)elm->key_state; + data->continue_reading = !STAILQ_EMPTY(&keyboard_queue); + } + free(elm); + } + pthread_mutex_unlock(&keyboard_mutex); +} + +static void cp0_create_lvgl_input_devices(void) +{ + const char *mouse_device = getenv_default("LV_LINUX_MOUSE_DEVICE", NULL); + if (mouse_device) + lv_evdev_create(LV_INDEV_TYPE_POINTER, mouse_device); + + lv_indev_t *indev = lv_indev_create(); + if (indev != NULL) { + lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(indev, cp0_keypad_read_cb); + } +} -#if !LV_USE_SDL /* ============================================================ * Parameters * ============================================================ */ @@ -704,8 +802,23 @@ void *keyboard_read_thread(void *argv) { return NULL; } -/* APPLaunch still installs its own LVGL keypad read callback, so this - * compatibility symbol intentionally does not create another input device. */ -void init_input(void) {} +void init_input(void) +{ + static int input_initialized = 0; + if (input_initialized) + return; -#endif + if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + + pthread_t keyboard_read_thread_id; + const char *keyboard_device = getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); + if (pthread_create(&keyboard_read_thread_id, NULL, keyboard_read_thread, (void *)keyboard_device) != 0) { + perror("pthread_create keyboard_read_thread"); + return; + } + + pthread_detach(keyboard_read_thread_id); + cp0_create_lvgl_input_devices(); + input_initialized = 1; +} diff --git a/projects/APPLaunch/main/src/app_battery.c b/projects/APPLaunch/main/src/app_battery.c index 0b8b1bc2..bb0fc0fe 100644 --- a/projects/APPLaunch/main/src/app_battery.c +++ b/projects/APPLaunch/main/src/app_battery.c @@ -6,9 +6,9 @@ extern threadpool g_launch_thread_pool; static void _battery_timer_cb(int *workingp) { - lv_battery_event_data_t data; + cp0_battery_info_t data; memset(&data, 0, sizeof(data)); - data.info = cp0_battery_read(); + data = cp0_battery_read(); lv_lock(); lv_obj_t *root = lv_screen_active(); if(root) diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index 77015b02..b577348d 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -1,21 +1,16 @@ #include "lvgl/lvgl.h" #include "lvgl/demos/lv_demos.h" #include -#include -#include #include #include -#include -#include #include #include #include "ui/ui.h" -#include "ui/ui_global_hint.h" #include "keyboard_input.h" #include "battery.h" -#include "compat/input_keys.h" #include "cp0_lvgl_app.h" #include "cp0_lvgl_file.hpp" +#include "hal_lvgl_bsp.h" #include "global_config.h" #if CONFIG_BACKWARD_CPP_ENABLED #define BACKWARD_HAS_DW 1 @@ -28,243 +23,27 @@ extern "C" { threadpool g_launch_thread_pool; } static const char* lock_file = NULL; -volatile uint32_t LV_EVENT_BATTERY; -volatile uint32_t LV_EVENT_WIFI_INFO; -volatile uint32_t LV_EVENT_DELL_CPP_DATA; - - - static const char *getenv_default(const char *name, const char *dflt) { return getenv(name) ? : dflt; } -int get_st7789v_fbdev(char *dev_path, size_t buf_size) -{ - if (dev_path == NULL || buf_size == 0) { - return -1; - } - - FILE *fp = fopen("/proc/fb", "r"); - if (fp == NULL) { - perror("Failed to open /proc/fb"); - return -1; - } - - char line[256]; - int fb_num = -1; - - /* Read line by line and find the line containing fb_st7789v, e.g. 0 fb_st7789v */ - while (fgets(line, sizeof(line), fp) != NULL) { - if (strstr(line, "fb_st7789v") != NULL) { - if (sscanf(line, "%d", &fb_num) == 1) { - break; - } - } - } - - fclose(fp); - - if (fb_num < 0) { - fprintf(stderr, "fb_st7789v not found in /proc/fb\n"); - } - if (access("/dev/fb_lcd", F_OK) == 0) { - snprintf(dev_path, buf_size, "/dev/fb_lcd"); - } else { - fb_num = 0; - snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); - } - return 0; -} - - -static int _evdev_process_key(uint16_t code) -{ - switch(code) { - case KEY_UP: - return LV_KEY_UP; - case KEY_DOWN: - return LV_KEY_DOWN; - case KEY_RIGHT: - return LV_KEY_RIGHT; - case KEY_LEFT: - return LV_KEY_LEFT; - case KEY_ESC: - return LV_KEY_ESC; - case KEY_DELETE: - return LV_KEY_DEL; - case KEY_BACKSPACE: - return LV_KEY_BACKSPACE; - case KEY_ENTER: - return LV_KEY_ENTER; - case KEY_NEXT: - return LV_KEY_NEXT; - case KEY_TAB: - return KEY_TAB; - case KEY_PREVIOUS: - return LV_KEY_PREV; - case KEY_HOME: - return LV_KEY_HOME; - case KEY_END: - return LV_KEY_END; - default: - return code; - } -} - - -/* ----------------- LVGL read callback ----------------- */ -static void keypad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) -{ - (void)indev; - - /* Output one queued event for each LVGL call */ - data->state = LV_INDEV_STATE_RELEASED; - data->continue_reading = false; - // Dequeue - { - pthread_mutex_lock(&keyboard_mutex); - if (!STAILQ_EMPTY(&keyboard_queue)) - { - struct key_item *elm = NULL; - elm = STAILQ_FIRST(&keyboard_queue); - STAILQ_REMOVE_HEAD(&keyboard_queue, entries); - - { - char utf8_dbg[64] = ""; - int di = 0; - for (int i = 0; i < (int)sizeof(elm->utf8) && elm->utf8[i] && di < 60; i++) { - unsigned char c = (unsigned char)elm->utf8[i]; - if (c >= 0x20 && c < 0x7f) utf8_dbg[di++] = (char)c; - else di += snprintf(utf8_dbg+di, 64-di, "\\x%02x", c); - } - utf8_dbg[di] = '\0'; - printf("[INDEV] dequeue code=%u state=%s sym=%s utf8='%s' cp=0x%x active_screen=%p\n", - elm->key_code, kbd_state_name(elm->key_state), - elm->sym_name, utf8_dbg, elm->codepoint, - (void*)lv_screen_active()); - } - lv_obj_t *root = lv_screen_active(); - if (root) { - lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); - } - - /* Global on-screen hint overlay (ESC / Shift / SYM). - * Called after the page has had a chance to react, and only - * READS elm — never frees it. */ - ui_global_hint_on_key(elm); - - data->key = _evdev_process_key(elm->key_code); - if(data->key) - { - data->state = (lv_indev_state_t)elm->key_state; - data->continue_reading = !STAILQ_EMPTY(&keyboard_queue); - } - free(elm); - } - pthread_mutex_unlock(&keyboard_mutex); - } -} #if LV_USE_EVDEV - -static void lv_linux_indev_init(void) +static void lv_linux_input_env_init(void) { - const char *mouse_device = getenv_default("LV_LINUX_MOUSE_DEVICE", NULL); const std::string default_keyboard_device = cp0_file_path("keyboard_device"); const std::string default_keyboard_map = cp0_file_path("keyboard_map"); const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", default_keyboard_device.c_str()); const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", default_keyboard_map.c_str()); setenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE", keyboard_device, 1); setenv("APPLAUNCH_LINUX_KEYBOARD_MAP", keyboard_map, 1); - - { - pthread_t keyboard_read_thread_id; - pthread_create(&keyboard_read_thread_id, // thread ID (output) - NULL, // thread attributes (NULL=default) - keyboard_read_thread,// thread function - NULL); // argument passed to the thread function - pthread_detach(keyboard_read_thread_id); - } - - - - if (mouse_device) - lv_evdev_create(LV_INDEV_TYPE_POINTER, mouse_device); - - lv_indev_t *indev = lv_indev_create(); - lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD); - lv_indev_set_read_cb(indev, keypad_read_cb); } -#endif - - -#if LV_USE_LINUX_FBDEV -static void lv_linux_disp_init(void) -{ - const char *device = NULL; - char fbdev[64] = {0}; - device = getenv_default("LV_LINUX_FBDEV_DEVICE", NULL); - if ((device == NULL) && (get_st7789v_fbdev(fbdev, sizeof(fbdev)) == 0)) { - device = fbdev; - } - setenv("APPLAUNCH_LINUX_FBDEV_DEVICE", device, 1); - printf("Using framebuffer device: %s\n", device); - lv_display_t * disp = lv_linux_fbdev_create(); - if(disp == NULL) { - printf("Failed to create fbdev display!\n"); - return; - } - - lv_linux_fbdev_set_file(disp, device); - - // Force the fbdev driver to ioctl(FBIOPUT_VSCREENINFO) after every flush, - // which makes fbtft push the buffer to the SPI LCD immediately. Without - // this, fbtft may batch/delay updates causing tearing. - lv_linux_fbdev_set_force_refresh(disp, true); - - // Print the detected resolution - lv_coord_t w = lv_display_get_horizontal_resolution(disp); - lv_coord_t h = lv_display_get_vertical_resolution(disp); - printf("Framebuffer resolution: %dx%d\n", w, h); -} -#if ! LV_USE_EVDEV && ! LV_USE_LIBINPUT -static void lv_linux_indev_init(void) -{ -} -#endif - -#elif LV_USE_LINUX_DRM -static void lv_linux_disp_init(void) -{ - const char *device = getenv_default("LV_LINUX_DRM_CARD", "/dev/dri/card0"); - lv_display_t * disp = lv_linux_drm_create(); - - lv_linux_drm_set_file(disp, device, -1); -} -#elif LV_USE_SDL -static void lv_linux_disp_init(void) -{ - const int width = atoi(getenv("LV_SDL_VIDEO_WIDTH") ? : "320"); - const int height = atoi(getenv("LV_SDL_VIDEO_HEIGHT") ? : "170"); - - lv_sdl_window_create(width, height); -} - -static void lv_linux_indev_init(void) +#else +static void lv_linux_input_env_init(void) { - lv_sdl_mouse_create(); - lv_sdl_keyboard_create(); } - -#else -#error Unsupported configuration #endif -#include -#include -#include -#include -#include void APPLaunch_lock() { @@ -301,8 +80,6 @@ void APPLaunch_lock() } } -extern "C" void init_audio(void); -extern "C" void init_camera(void); int main(void) { setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1); @@ -315,18 +92,14 @@ int main(void) lv_init(); printf("[BOOT] lv_init() done\n"); - /*Linux display device init*/ - printf("[BOOT] lv_linux_disp_init() starting...\n"); - lv_linux_disp_init(); - printf("[BOOT] lv_linux_disp_init() done\n"); + lv_linux_input_env_init(); - printf("[BOOT] lv_linux_indev_init() starting...\n"); - lv_linux_indev_init(); - printf("[BOOT] lv_linux_indev_init() done\n"); + printf("[BOOT] cp0_lvgl_init() starting...\n"); + cp0_lvgl_init(); + printf("[BOOT] cp0_lvgl_init() done\n"); - LV_EVENT_KEYBOARD = lv_event_register_id(); - LV_EVENT_BATTERY = lv_event_register_id(); - LV_EVENT_DELL_CPP_DATA = lv_event_register_id(); + if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); lv_timer_create(battery_timer_cb, 3000, NULL); // Restore saved brightness @@ -342,8 +115,6 @@ int main(void) if (saved_vol >= 0) cp0_volume_write(saved_vol); } - init_audio(); - init_camera(); ui_init(); // Force full-screen refresh immediately after init diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index af23e1a6..e61c41f7 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -6,6 +6,8 @@ #ifndef _ZERO_UI_H #define _ZERO_UI_H +#include "hal_lvgl_bsp.h" + #ifdef __cplusplus extern "C" { #endif @@ -63,14 +65,10 @@ void ui_info_bind(); #define IS_KEY_PRESSED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD)&&(LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) #define IS_KEY_RELEASED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD)&&(LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) -extern volatile uint32_t LV_EVENT_BATTERY; -extern volatile uint32_t LV_EVENT_DELL_CPP_DATA; - -typedef struct { - cp0_battery_info_t info; -} lv_battery_event_data_t; +#define LV_EVENT_BATTERY lv_c_event[CP0_C_EVENT_BATTERY] +#define LV_EVENT_DELL_CPP_DATA lv_c_event[CP0_C_EVENT_DELL_CPP_DATA] -#define LV_EVENT_BATTERY_GET_INFO(e) (&((lv_battery_event_data_t *)lv_event_get_param(e))->info) +#define LV_EVENT_BATTERY_GET_INFO(e) ((cp0_battery_info_t *)lv_event_get_param(e)) #undef UI_DEFINE_OBJECT From 93685bf08aec220c5a86776afd88952f70da5ae8 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 15:26:56 +0800 Subject: [PATCH 14/70] Move APPLaunch battery polling into cp0_lvgl Let cp0_lvgl own periodic battery event publication instead of keeping an APPLaunch-local battery facade. init_battery() now creates a guarded BatterySystem singleton, registers the existing cp0_signal_battery_pub hook, and installs a 3 second LVGL timer that publishes lv_c_event[CP0_C_EVENT_BATTERY]. Remove projects/APPLaunch/main/src/app_battery.c and projects/APPLaunch/main/include/battery.h. main.cpp no longer includes battery.h or creates battery_timer_cb directly, so APPLaunch startup relies on cp0_lvgl_init() for battery polling just like the other hardware-facing initialization paths. Keep the event payload shape aligned with the lv_c_event migration: battery events carry cp0_battery_info_t pointers. Also update the global hint comment to point at the cp0_lvgl keyboard dispatch path after moving keypad dispatch out of main.cpp. Validation: reviewed README_ZH.md build guidance, then ran export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed successfully. --- .../cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp | 76 +++++++------------ projects/APPLaunch/main/include/battery.h | 13 ---- projects/APPLaunch/main/src/app_battery.c | 30 -------- projects/APPLaunch/main/src/main.cpp | 2 - projects/APPLaunch/main/ui/ui_global_hint.h | 4 +- 5 files changed, 29 insertions(+), 96 deletions(-) delete mode 100644 projects/APPLaunch/main/include/battery.h delete mode 100644 projects/APPLaunch/main/src/app_battery.c diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp index 85d908eb..9c0301d2 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_battery.cpp @@ -2,65 +2,43 @@ #include "lvgl/lvgl.h" #include "cp0_lvgl.h" #include "cp0_lvgl_app.h" -#include +#include #include -#include -#include -#include -#include class BatterySystem { -private: - /* data */ public: - BatterySystem() = default; void pub() { - if (lv_c_event[CP0_C_EVENT_BATTERY] != 0) - { - cp0_battery_info_t info = cp0_battery_read(); - if (info.valid) - { - // lv_lock(); - lv_obj_t *root = lv_display_get_screen_active(NULL); - if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_BATTERY], (void *)&info); - // lv_unlock(); - } - } - } - ~BatterySystem() = default; + if (lv_c_event[CP0_C_EVENT_BATTERY] == 0) + return; -private: - std::string scanf_battery_path() - { - return "/sys/class/power_supply/bq27220-0/capacity"; - } - int get_battery_value() - { - int capacity; - std::string capacity_path = scanf_battery_path(); - std::ifstream file(capacity_path); - if (!file.is_open()) - { - std::cerr << "Failed to open file: " << capacity_path << std::endl; - return -1; - } - file >> capacity; - if (file.fail()) - { - std::cerr << "Failed to read capacity value" << std::endl; - return -1; - } - file.close(); - return capacity; + cp0_battery_info_t info = cp0_battery_read(); + if (!info.valid) + return; + + lv_obj_t *root = lv_display_get_screen_active(NULL); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_BATTERY], (void *)&info); } }; +static void battery_timer_cb(lv_timer_t *timer) +{ + auto *battery = static_cast(lv_timer_get_user_data(timer)); + if (battery != nullptr) + battery->pub(); +} + extern "C" void init_battery() { - std::shared_ptr battery = std::make_shared(); - cp0_signal_battery_pub.append([battery](std::function fun) - { battery->pub(); }); -} \ No newline at end of file + static std::shared_ptr battery; + if (battery) + return; + + battery = std::make_shared(); + BatterySystem *battery_ptr = battery.get(); + cp0_signal_battery_pub.append([battery_ptr](std::function fun) + { battery_ptr->pub(); }); + lv_timer_create(battery_timer_cb, 3000, battery_ptr); +} diff --git a/projects/APPLaunch/main/include/battery.h b/projects/APPLaunch/main/include/battery.h deleted file mode 100644 index ba421008..00000000 --- a/projects/APPLaunch/main/include/battery.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef __BATTERY__H__ -#define __BATTERY__H__ - -#include "lvgl/lvgl.h" -#include "ui/ui.h" -#ifdef __cplusplus -extern "C" { -#endif -void battery_timer_cb(lv_timer_t *timer); -#ifdef __cplusplus -} -#endif -#endif \ No newline at end of file diff --git a/projects/APPLaunch/main/src/app_battery.c b/projects/APPLaunch/main/src/app_battery.c deleted file mode 100644 index bb0fc0fe..00000000 --- a/projects/APPLaunch/main/src/app_battery.c +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include "battery.h" -#include "thpool.h" - -extern threadpool g_launch_thread_pool; - -static void _battery_timer_cb(int *workingp) -{ - cp0_battery_info_t data; - memset(&data, 0, sizeof(data)); - data = cp0_battery_read(); - lv_lock(); - lv_obj_t *root = lv_screen_active(); - if(root) - { - lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_BATTERY, &data); - } - lv_unlock(); - *workingp = 0; -} - - -void battery_timer_cb(lv_timer_t *timer) -{ - static int working = 0; - if(working) - return; - working = 1; - thpool_add_work(g_launch_thread_pool, (void (*)(void *))_battery_timer_cb, &working); -} \ No newline at end of file diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index b577348d..1ee9fb70 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -7,7 +7,6 @@ #include #include "ui/ui.h" #include "keyboard_input.h" -#include "battery.h" #include "cp0_lvgl_app.h" #include "cp0_lvgl_file.hpp" #include "hal_lvgl_bsp.h" @@ -100,7 +99,6 @@ int main(void) if (LV_EVENT_KEYBOARD == 0) LV_EVENT_KEYBOARD = lv_event_register_id(); - lv_timer_create(battery_timer_cb, 3000, NULL); // Restore saved brightness { diff --git a/projects/APPLaunch/main/ui/ui_global_hint.h b/projects/APPLaunch/main/ui/ui_global_hint.h index d18a8dd6..1ab8e3a6 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.h +++ b/projects/APPLaunch/main/ui/ui_global_hint.h @@ -5,8 +5,8 @@ * page. Shows a short, transient banner near the top of the active * screen when specific keys are pressed. * - * Hooked from the global key dispatch in main.cpp (keypad_read_cb), - * right after LV_EVENT_KEYBOARD has been sent to the active screen. + * Hooked from the cp0_lvgl keyboard dispatch after LV_EVENT_KEYBOARD + * has been sent to the active screen. * The helper only READS elm — it never frees it. */ #ifndef UI_GLOBAL_HINT_H From 20616d1be2ba9ed066e8616618b08c536e064d97 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 15:36:40 +0800 Subject: [PATCH 15/70] Remove unused APPLaunch thpool dependency APPLaunch no longer schedules work through C-Thread-Pool after battery polling and audio playback were moved away from the old thpool paths. Remove the remaining g_launch_thread_pool global, thpool_init() startup call, and thpool.h includes. Drop the C-Thread-Pool fetch/source/include block from the APPLaunch SConstruct so the project no longer downloads or builds thpool.c unnecessarily. Also clean the stale comment in ui_events.c that referenced the old thpool_add_work audio path. Validation: reviewed README_ZH.md build guidance, then ran export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed successfully. --- projects/APPLaunch/main/SConstruct | 7 ------- projects/APPLaunch/main/src/main.cpp | 5 ----- projects/APPLaunch/main/ui/ui_events.c | 10 +--------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 90445ee5..a1b1177a 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -168,13 +168,6 @@ SRCS += [ ] INCLUDE += [os.path.join(RadioLib, 'src')] -# add https://github.com/Pithikos/C-Thread-Pool.git -CThreadPool = wget_github('https://github.com/Pithikos/C-Thread-Pool.git') -SRCS += [ - os.path.join(CThreadPool, 'thpool.c') -] -INCLUDE += [CThreadPool] - # add static files STATIC_FILES += [ADir('../APPLaunch')] diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index 1ee9fb70..a5efa672 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -17,10 +17,6 @@ #include "backward.h" #endif -#include "thpool.h" -extern "C" { - threadpool g_launch_thread_pool; -} static const char* lock_file = NULL; static const char *getenv_default(const char *name, const char *dflt) { @@ -87,7 +83,6 @@ int main(void) static const std::string default_lock_file = cp0_file_path("lock_file"); lock_file = default_lock_file.c_str(); - g_launch_thread_pool = thpool_init(3); lv_init(); printf("[BOOT] lv_init() done\n"); diff --git a/projects/APPLaunch/main/ui/ui_events.c b/projects/APPLaunch/main/ui/ui_events.c index 7cb44f1d..83a4e995 100644 --- a/projects/APPLaunch/main/ui/ui_events.c +++ b/projects/APPLaunch/main/ui/ui_events.c @@ -13,9 +13,6 @@ // #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" -#include "thpool.h" - -extern threadpool g_launch_thread_pool; typedef void (*switch_cb_t)(lv_event_t *); @@ -750,12 +747,7 @@ void main_key_switch(lv_event_t *e) case KEY_LEFT: { - /* - * This used to be: - * thpool_add_work(g_launch_thread_pool, play_audio, caudio_path("switch.wav")); - * - * Now it directly plays the preloaded sound effect. - */ + /* Play the preloaded sound effect directly before switching pages. */ if (!lvping_lock) { audio_play_switch(); From 66802ed0924f4561e1a96ec87667d2a708797006 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 15:45:07 +0800 Subject: [PATCH 16/70] Move APPLaunch env and volume handling into cp0_lvgl Centralize APPLaunch runtime environment setup in cp0_lvgl by moving XDG/PipeWire/Pulse and Linux keyboard environment initialization out of main.cpp and into init_lvgl_env(). cp0_lvgl_init() now prepares these runtime values before events, framebuffer, input, audio, battery, and camera initialization. Route cp0_volume_read() and cp0_volume_write() through cp0_signal_audio_api so APPLaunch continues to use the same public cp0 app API while the implementation is owned by AudioSystem. AudioSystem now handles VolumeRead and VolumeWrite commands and cp0_app_settings no longer directly shells out for volume control. Validation: read README_ZH.md cross-compile instructions, then ran cd projects/APPLaunch && export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed. --- ext_components/cp0_lvgl/include/commount.h | 1 + ext_components/cp0_lvgl/src/commount.c | 25 ++++++++ .../cp0_lvgl/src/cp0/cp0_app_settings.cpp | 28 --------- ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_app.cpp | 24 ++++++++ .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 57 ++++++++++++++++++- projects/APPLaunch/main/src/main.cpp | 28 --------- 7 files changed, 107 insertions(+), 57 deletions(-) diff --git a/ext_components/cp0_lvgl/include/commount.h b/ext_components/cp0_lvgl/include/commount.h index 30494196..66237b5c 100644 --- a/ext_components/cp0_lvgl/include/commount.h +++ b/ext_components/cp0_lvgl/include/commount.h @@ -6,6 +6,7 @@ extern "C" { void init_lvgl_event(); void init_lvgl_event_cpp(); +void init_lvgl_env(); #ifdef __cplusplus } diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index bda226b9..05046367 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -1,9 +1,34 @@ #include "hal_lvgl_bsp.h" #include "commount.h" +#include "cp0_lvgl_app.h" #include "lvgl/lvgl.h" +#include uint32_t lv_c_event[(2*CP0_C_EVENT_END)] = {0}; +static const char *getenv_default(const char *name, const char *dflt) +{ + const char *value = getenv(name); + return value ? value : dflt; +} + +void init_lvgl_env() +{ + setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1); + setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", 1); + setenv("PULSE_SERVER", "unix:/run/user/1000/pulse/native", 1); + +#if LV_USE_EVDEV + const char *default_keyboard_device = cp0_file_path("keyboard_device"); + const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", default_keyboard_device); + setenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE", keyboard_device, 1); + + const char *default_keyboard_map = cp0_file_path("keyboard_map"); + const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", default_keyboard_map); + setenv("APPLAUNCH_LINUX_KEYBOARD_MAP", keyboard_map, 1); +#endif +} + void init_lvgl_event() { for (int i = 0; i < CP0_C_EVENT_END; i++) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp index 895b6b55..7b429b23 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp @@ -204,34 +204,6 @@ int cp0_backlight_write(int val) return val; } -int cp0_volume_read(void) -{ - FILE *p = popen("amixer -c1 sget 'Headphone Playback Volume' 2>/dev/null", "r"); - if (!p) return -1; - char buf[256]; - int val = -1; - while (fgets(buf, sizeof(buf), p)) { - char *s = strstr(buf, ": values="); - if (s) { val = atoi(s + 9); break; } - } - pclose(p); - return val; -} - -int cp0_volume_write(int val) -{ - if (val < 0) val = 0; - if (val > 63) val = 63; - char cmd[128]; - snprintf(cmd, sizeof(cmd), "amixer -c1 sset 'Headphone Playback Volume' %d 2>/dev/null", val); - FILE *p = popen(cmd, "r"); - if (!p) return -1; - char buf[128]; - while (fgets(buf, sizeof(buf), p)) {} - pclose(p); - return val; -} - // ── Async WiFi status: background thread polls nmcli, main thread reads cache ── #include diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index d5441d2f..645d4967 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -4,6 +4,7 @@ #include "cp0_lvgl.h" void cp0_lvgl_init(void) { + init_lvgl_env(); init_lvgl_event(); init_freambuffer_disp(); init_input(); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp index 85f3b39d..e896339a 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp @@ -1,6 +1,8 @@ #include "cp0_lvgl_app.h" #include "hal_lvgl_bsp.h" +#include +#include #include #include @@ -13,4 +15,26 @@ void cp0_signal_audio_api_play_file(const char *path) } } +int cp0_volume_read(void) +{ + int volume = -1; + cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) { + if (code == 0) { + volume = std::atoi(data.c_str()); + } + }); + return volume; +} + +int cp0_volume_write(int val) +{ + int volume = -1; + cp0_signal_audio_api({"VolumeWrite", std::to_string(val)}, [&](int code, std::string data) { + if (code == 0) { + volume = std::atoi(data.c_str()); + } + }); + return volume; +} + } diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index 5fdbcbbf..bc6cd319 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -512,6 +512,20 @@ class AudioSystem _cap_status_callback = callback; } + void VolumeRead(arg_t arg, callback_t callback) + { + (void)arg; + int val = read_system_volume(); + report(callback, val >= 0 ? 0 : -1, std::to_string(val)); + } + + void VolumeWrite(arg_t arg, callback_t callback) + { + int val = parse_volume_arg(arg); + int ret = write_system_volume(val); + report(callback, ret >= 0 ? 0 : -1, std::to_string(ret)); + } + void api_call(arg_t arg, callback_t callback) { if(arg.empty()) @@ -532,7 +546,9 @@ class AudioSystem map_fun(CapContinue), map_fun(CapEnd), map_fun(CapFileSave), - map_fun(SetCallback) + map_fun(SetCallback), + map_fun(VolumeRead), + map_fun(VolumeWrite) }; #undef map_fun @@ -547,6 +563,45 @@ class AudioSystem } report(callback, -1, "unknown audio api\n"); } + + static int read_system_volume() + { + FILE *p = popen("amixer -c1 sget 'Headphone Playback Volume' 2>/dev/null", "r"); + if (!p) return -1; + char buf[256]; + int val = -1; + while (fgets(buf, sizeof(buf), p)) { + char *s = strstr(buf, ": values="); + if (s) { + val = atoi(s + 9); + break; + } + } + pclose(p); + return val; + } + + static int write_system_volume(int val) + { + if (val < 0) val = 0; + if (val > 63) val = 63; + + char cmd[128]; + snprintf(cmd, sizeof(cmd), "amixer -c1 sset 'Headphone Playback Volume' %d 2>/dev/null", val); + FILE *p = popen(cmd, "r"); + if (!p) return -1; + char buf[128]; + while (fgets(buf, sizeof(buf), p)) {} + pclose(p); + return val; + } + + static int parse_volume_arg(const arg_t& arg) + { + std::string value = first_arg_after_command(arg); + if (value.empty()) return 0; + return std::atoi(value.c_str()); + } }; extern "C" void init_audio(void) diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index a5efa672..9af84024 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -2,7 +2,6 @@ #include "lvgl/demos/lv_demos.h" #include #include -#include #include #include #include "ui/ui.h" @@ -18,27 +17,6 @@ #endif static const char* lock_file = NULL; -static const char *getenv_default(const char *name, const char *dflt) -{ - return getenv(name) ? : dflt; -} - - -#if LV_USE_EVDEV -static void lv_linux_input_env_init(void) -{ - const std::string default_keyboard_device = cp0_file_path("keyboard_device"); - const std::string default_keyboard_map = cp0_file_path("keyboard_map"); - const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", default_keyboard_device.c_str()); - const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", default_keyboard_map.c_str()); - setenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE", keyboard_device, 1); - setenv("APPLAUNCH_LINUX_KEYBOARD_MAP", keyboard_map, 1); -} -#else -static void lv_linux_input_env_init(void) -{ -} -#endif void APPLaunch_lock() { @@ -77,17 +55,11 @@ void APPLaunch_lock() int main(void) { - setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1); - setenv("PIPEWIRE_RUNTIME_DIR", "/run/user/1000", 1); - setenv("PULSE_SERVER", "unix:/run/user/1000/pulse/native", 1); - static const std::string default_lock_file = cp0_file_path("lock_file"); lock_file = default_lock_file.c_str(); lv_init(); printf("[BOOT] lv_init() done\n"); - lv_linux_input_env_init(); - printf("[BOOT] cp0_lvgl_init() starting...\n"); cp0_lvgl_init(); printf("[BOOT] cp0_lvgl_init() done\n"); From 2a7686d8af598c27654cea959cad81bdd6da7891 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 15:46:58 +0800 Subject: [PATCH 17/70] Remove unused APPLaunch TinyGPS dependency TinyGPS++ was still compiled through main/src/*.c* but APPLaunch no longer uses the parser. The only remaining project reference was an unused include in the HikePod page, so remove that include and delete the TinyGPS++ source and compatibility headers. This reduces APPLaunch build inputs and avoids keeping an unused GPS parsing dependency in the launcher while preserving the current HikePod UI behavior. Validation: read README_ZH.md cross-compile instructions, then ran cd projects/APPLaunch && export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed. --- projects/APPLaunch/main/include/TinyGPS++.h | 342 ------------ projects/APPLaunch/main/include/TinyGPSPlus.h | 26 - projects/APPLaunch/main/src/TinyGPS++.cpp | 522 ------------------ .../ui/components/page_app/ui_app_hikepod.hpp | 1 - 4 files changed, 891 deletions(-) delete mode 100644 projects/APPLaunch/main/include/TinyGPS++.h delete mode 100644 projects/APPLaunch/main/include/TinyGPSPlus.h delete mode 100644 projects/APPLaunch/main/src/TinyGPS++.cpp diff --git a/projects/APPLaunch/main/include/TinyGPS++.h b/projects/APPLaunch/main/include/TinyGPS++.h deleted file mode 100644 index e833f93e..00000000 --- a/projects/APPLaunch/main/include/TinyGPS++.h +++ /dev/null @@ -1,342 +0,0 @@ -/* -TinyGPS++ - a small GPS library for Arduino providing universal NMEA parsing -Based on work by and "distanceBetween" and "courseTo" courtesy of Maarten Lamers. -Suggestion to add satellites, courseTo(), and cardinal() by Matt Monson. -Location precision improvements suggested by Wayne Holder. -Copyright (C) 2008-2024 Mikal Hart -All rights reserved. - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - - - -#ifndef __TinyGPSPlus_h -#define __TinyGPSPlus_h - -#include -#ifdef ARDUINO -#include "Arduino.h" -#else -#include -#include -#ifndef PI -#define PI 3.1415926535897932384626433832795 -#endif -#ifndef HALF_PI -#define HALF_PI 1.5707963267948966192313216916398 -#endif -#ifndef TWO_PI -#define TWO_PI 6.283185307179586476925286766559 -#endif -#ifndef DEG_TO_RAD -#define DEG_TO_RAD 0.017453292519943295769236907684886 -#endif -#ifndef RAD_TO_DEG -#define RAD_TO_DEG 57.295779513082320876798154814105 -#endif - -typedef uint8_t byte; -inline double radians(double deg) { return deg * DEG_TO_RAD; } -inline double degrees(double rad) { return rad * RAD_TO_DEG; } -inline double sq(double x) { return x * x; } - -unsigned long millis(); -#endif -#include - -#define _GPS_VERSION "1.1.0" // software version of this library -#define _GPS_MPH_PER_KNOT 1.15077945 -#define _GPS_MPS_PER_KNOT 0.51444444 -#define _GPS_KMPH_PER_KNOT 1.852 -#define _GPS_MILES_PER_METER 0.00062137112 -#define _GPS_KM_PER_METER 0.001 -#define _GPS_FEET_PER_METER 3.2808399 -#define _GPS_MAX_FIELD_SIZE 15 -#define _GPS_EARTH_MEAN_RADIUS 6371009 // old: 6372795 - -struct RawDegrees -{ - uint16_t deg; - uint32_t billionths; - bool negative; -public: - RawDegrees() : deg(0), billionths(0), negative(false) - {} -}; - -struct TinyGPSLocation -{ - friend class TinyGPSPlus; -public: - enum Quality { Invalid = '0', GPS = '1', DGPS = '2', PPS = '3', RTK = '4', FloatRTK = '5', Estimated = '6', Manual = '7', Simulated = '8' }; - enum Mode { N = 'N', A = 'A', D = 'D', E = 'E'}; - - bool isValid() const { return valid; } - bool isUpdated() const { return updated; } - uint32_t age() const { return valid ? millis() - lastCommitTime : (uint32_t)ULONG_MAX; } - const RawDegrees &rawLat() { updated = false; return rawLatData; } - const RawDegrees &rawLng() { updated = false; return rawLngData; } - double lat(); - double lng(); - Quality FixQuality() { updated = false; return fixQuality; } - Mode FixMode() { updated = false; return fixMode; } - - TinyGPSLocation() : valid(false), updated(false), fixQuality(Invalid), fixMode(N) - {} - -private: - bool valid, updated; - RawDegrees rawLatData, rawLngData, rawNewLatData, rawNewLngData; - Quality fixQuality, newFixQuality; - Mode fixMode, newFixMode; - uint32_t lastCommitTime; - void commit(); - void setLatitude(const char *term); - void setLongitude(const char *term); -}; - -struct TinyGPSDate -{ - friend class TinyGPSPlus; -public: - bool isValid() const { return valid; } - bool isUpdated() const { return updated; } - uint32_t age() const { return valid ? millis() - lastCommitTime : (uint32_t)ULONG_MAX; } - - uint32_t value() { updated = false; return date; } - uint16_t year(); - uint8_t month(); - uint8_t day(); - - TinyGPSDate() : valid(false), updated(false), date(0) - {} - -private: - bool valid, updated; - uint32_t date, newDate; - uint32_t lastCommitTime; - void commit(); - void setDate(const char *term); -}; - -struct TinyGPSTime -{ - friend class TinyGPSPlus; -public: - bool isValid() const { return valid; } - bool isUpdated() const { return updated; } - uint32_t age() const { return valid ? millis() - lastCommitTime : (uint32_t)ULONG_MAX; } - - uint32_t value() { updated = false; return time; } - uint8_t hour(); - uint8_t minute(); - uint8_t second(); - uint8_t centisecond(); - - TinyGPSTime() : valid(false), updated(false), time(0) - {} - -private: - bool valid, updated; - uint32_t time, newTime; - uint32_t lastCommitTime; - void commit(); - void setTime(const char *term); -}; - -struct TinyGPSDecimal -{ - friend class TinyGPSPlus; -public: - bool isValid() const { return valid; } - bool isUpdated() const { return updated; } - uint32_t age() const { return valid ? millis() - lastCommitTime : (uint32_t)ULONG_MAX; } - int32_t value() { updated = false; return val; } - - TinyGPSDecimal() : valid(false), updated(false), val(0) - {} - -private: - bool valid, updated; - uint32_t lastCommitTime; - int32_t val, newval; - void commit(); - void set(const char *term); -}; - -struct TinyGPSInteger -{ - friend class TinyGPSPlus; -public: - bool isValid() const { return valid; } - bool isUpdated() const { return updated; } - uint32_t age() const { return valid ? millis() - lastCommitTime : (uint32_t)ULONG_MAX; } - uint32_t value() { updated = false; return val; } - - TinyGPSInteger() : valid(false), updated(false), val(0) - {} - -private: - bool valid, updated; - uint32_t lastCommitTime; - uint32_t val, newval; - void commit(); - void set(const char *term); -}; - -struct TinyGPSSpeed : TinyGPSDecimal -{ - double knots() { return value() / 100.0; } - double mph() { return _GPS_MPH_PER_KNOT * value() / 100.0; } - double mps() { return _GPS_MPS_PER_KNOT * value() / 100.0; } - double kmph() { return _GPS_KMPH_PER_KNOT * value() / 100.0; } -}; - -struct TinyGPSCourse : public TinyGPSDecimal -{ - double deg() { return value() / 100.0; } -}; - -struct TinyGPSAltitude : TinyGPSDecimal -{ - double meters() { return value() / 100.0; } - double miles() { return _GPS_MILES_PER_METER * value() / 100.0; } - double kilometers() { return _GPS_KM_PER_METER * value() / 100.0; } - double feet() { return _GPS_FEET_PER_METER * value() / 100.0; } -}; - -struct TinyGPSHDOP : TinyGPSDecimal -{ - double hdop() { return value() / 100.0; } -}; - -class TinyGPSPlus; -class TinyGPSCustom -{ -public: - TinyGPSCustom() {}; - TinyGPSCustom(TinyGPSPlus &gps, const char *sentenceName, int termNumber); - void begin(TinyGPSPlus &gps, const char *_sentenceName, int _termNumber); - - bool isUpdated() const { return updated; } - bool isValid() const { return valid; } - uint32_t age() const { return valid ? millis() - lastCommitTime : (uint32_t)ULONG_MAX; } - const char *value() { updated = false; return buffer; } - -private: - void commit(); - void set(const char *term); - - char stagingBuffer[_GPS_MAX_FIELD_SIZE + 1]; - char buffer[_GPS_MAX_FIELD_SIZE + 1]; - unsigned long lastCommitTime; - bool valid, updated; - const char *sentenceName; - int termNumber; - friend class TinyGPSPlus; - TinyGPSCustom *next; -}; - -class TinyGPSPlus -{ -public: - TinyGPSPlus(); - bool encode(char c); // process one character received from GPS - TinyGPSPlus &operator << (char c) {encode(c); return *this;} - - TinyGPSLocation location; - TinyGPSDate date; - TinyGPSTime time; - TinyGPSSpeed speed; - TinyGPSCourse course; - TinyGPSAltitude altitude; - TinyGPSInteger satellites; - TinyGPSHDOP hdop; - - static const char *libraryVersion() { return _GPS_VERSION; } - - static double distanceBetween(double lat1, double long1, double lat2, double long2); - static double courseTo(double lat1, double long1, double lat2, double long2); - static const char *cardinal(double course); - - static int32_t parseDecimal(const char *term); - static void parseDegrees(const char *term, RawDegrees °); - - uint32_t charsProcessed() const { return encodedCharCount; } - uint32_t sentencesWithFix() const { return sentencesWithFixCount; } - uint32_t failedChecksum() const { return failedChecksumCount; } - uint32_t passedChecksum() const { return passedChecksumCount; } - void reset() { - // Reset all parsing state variables - parity = 0; - isChecksumTerm = false; - curSentenceType = 0; - curTermNumber = 0; - curTermOffset = 0; - sentenceHasFix = false; - - // Reset statistics - encodedCharCount = 0; - sentencesWithFixCount = 0; - failedChecksumCount = 0; - passedChecksumCount = 0; - - // Clear location, date, and time - location = TinyGPSLocation(); // Assuming default constructor resets it - date = TinyGPSDate(); // Assuming default constructor resets it - time = TinyGPSTime(); // Assuming default constructor resets it - - // Clear other attributes if necessary - speed = TinyGPSSpeed(); // Assuming default constructor resets it - course = TinyGPSCourse(); // Assuming default constructor resets it - altitude = TinyGPSAltitude(); // Assuming default constructor resets it - satellites = TinyGPSInteger(); // Assuming default constructor resets it - hdop = TinyGPSHDOP(); // Assuming default constructor resets it - - // Clear custom elements if applicable - customElts = nullptr; - customCandidates = nullptr; - } -private: - enum {GPS_SENTENCE_GGA, GPS_SENTENCE_RMC, GPS_SENTENCE_OTHER}; - - // parsing state variables - uint8_t parity; - bool isChecksumTerm; - char term[_GPS_MAX_FIELD_SIZE]; - uint8_t curSentenceType; - uint8_t curTermNumber; - uint8_t curTermOffset; - bool sentenceHasFix; - - // custom element support - friend class TinyGPSCustom; - TinyGPSCustom *customElts; - TinyGPSCustom *customCandidates; - void insertCustom(TinyGPSCustom *pElt, const char *sentenceName, int index); - - // statistics - uint32_t encodedCharCount; - uint32_t sentencesWithFixCount; - uint32_t failedChecksumCount; - uint32_t passedChecksumCount; - - // internal utilities - int fromHex(char a); - bool endOfTermHandler(); -}; - -#endif // def(__TinyGPSPlus_h) diff --git a/projects/APPLaunch/main/include/TinyGPSPlus.h b/projects/APPLaunch/main/include/TinyGPSPlus.h deleted file mode 100644 index 23d9fefb..00000000 --- a/projects/APPLaunch/main/include/TinyGPSPlus.h +++ /dev/null @@ -1,26 +0,0 @@ -/* -TinyGPSPlus - a small GPS library for Arduino providing universal NMEA parsing -Based on work by and "distanceBetween" and "courseTo" courtesy of Maarten Lamers. -Suggestion to add satellites, courseTo(), and cardinal() by Matt Monson. -Location precision improvements suggested by Wayne Holder. -Copyright (C) 2008-2024 Mikal Hart -All rights reserved. - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef __TinyGPSPlus_h -#include "TinyGPS++.h" -#endif \ No newline at end of file diff --git a/projects/APPLaunch/main/src/TinyGPS++.cpp b/projects/APPLaunch/main/src/TinyGPS++.cpp deleted file mode 100644 index 2d4de7d1..00000000 --- a/projects/APPLaunch/main/src/TinyGPS++.cpp +++ /dev/null @@ -1,522 +0,0 @@ -/* -TinyGPS++ - a small GPS library for Arduino providing universal NMEA parsing -Based on work by and "distanceBetween" and "courseTo" courtesy of Maarten Lamers. -Suggestion to add satellites, courseTo(), and cardinal() by Matt Monson. -Location precision improvements suggested by Wayne Holder. -Copyright (C) 2008-2024 Mikal Hart -All rights reserved. - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#include "TinyGPS++.h" - -#include -#include -#include -#include - -#define _RMCterm "RMC" -#define _GGAterm "GGA" - -#if !defined(ARDUINO) && !defined(__AVR__) -#include -// Alternate implementation of millis() that relies on std -unsigned long millis() -{ - static auto start_time = std::chrono::high_resolution_clock::now(); - - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - - return static_cast(duration.count()); -} -#endif - -TinyGPSPlus::TinyGPSPlus() - : parity(0) - , isChecksumTerm(false) - , curSentenceType(GPS_SENTENCE_OTHER) - , curTermNumber(0) - , curTermOffset(0) - , sentenceHasFix(false) - , customElts(0) - , customCandidates(0) - , encodedCharCount(0) - , sentencesWithFixCount(0) - , failedChecksumCount(0) - , passedChecksumCount(0) -{ - term[0] = '\0'; -} - -// -// public methods -// - -bool TinyGPSPlus::encode(char c) -{ - ++encodedCharCount; - - switch(c) - { - case ',': // term terminators - parity ^= (uint8_t)c; - case '\r': - case '\n': - case '*': - { - bool isValidSentence = false; - if (curTermOffset < sizeof(term)) - { - term[curTermOffset] = 0; - isValidSentence = endOfTermHandler(); - } - ++curTermNumber; - curTermOffset = 0; - isChecksumTerm = c == '*'; - return isValidSentence; - } - break; - - case '$': // sentence begin - curTermNumber = curTermOffset = 0; - parity = 0; - curSentenceType = GPS_SENTENCE_OTHER; - isChecksumTerm = false; - sentenceHasFix = false; - return false; - - default: // ordinary characters - if (curTermOffset < sizeof(term) - 1) - term[curTermOffset++] = c; - if (!isChecksumTerm) - parity ^= c; - return false; - } - - return false; -} - -// -// internal utilities -// -int TinyGPSPlus::fromHex(char a) -{ - if (a >= 'A' && a <= 'F') - return a - 'A' + 10; - else if (a >= 'a' && a <= 'f') - return a - 'a' + 10; - else - return a - '0'; -} - -// static -// Parse a (potentially negative) number with up to 2 decimal digits -xxxx.yy -int32_t TinyGPSPlus::parseDecimal(const char *term) -{ - bool negative = *term == '-'; - if (negative) ++term; - int32_t ret = 100 * (int32_t)atol(term); - while (isdigit(*term)) ++term; - if (*term == '.' && isdigit(term[1])) - { - ret += 10 * (term[1] - '0'); - if (isdigit(term[2])) - ret += term[2] - '0'; - } - return negative ? -ret : ret; -} - -// static -// Parse degrees in that funny NMEA format DDMM.MMMM -void TinyGPSPlus::parseDegrees(const char *term, RawDegrees °) -{ - uint32_t leftOfDecimal = (uint32_t)atol(term); - uint16_t minutes = (uint16_t)(leftOfDecimal % 100); - uint32_t multiplier = 10000000UL; - uint32_t tenMillionthsOfMinutes = minutes * multiplier; - - deg.deg = (int16_t)(leftOfDecimal / 100); - - while (isdigit(*term)) - ++term; - - if (*term == '.') - while (isdigit(*++term)) - { - multiplier /= 10; - tenMillionthsOfMinutes += (*term - '0') * multiplier; - } - - deg.billionths = (5 * tenMillionthsOfMinutes + 1) / 3; - deg.negative = false; -} - -#define COMBINE(sentence_type, term_number) (((unsigned)(sentence_type) << 5) | term_number) - -// Processes a just-completed term -// Returns true if new sentence has just passed checksum test and is validated -bool TinyGPSPlus::endOfTermHandler() -{ - // If it's the checksum term, and the checksum checks out, commit - if (isChecksumTerm) - { - byte checksum = 16 * fromHex(term[0]) + fromHex(term[1]); - if (checksum == parity) - { - passedChecksumCount++; - if (sentenceHasFix) - ++sentencesWithFixCount; - - switch(curSentenceType) - { - case GPS_SENTENCE_RMC: - date.commit(); - time.commit(); - if (sentenceHasFix) - { - location.commit(); - speed.commit(); - course.commit(); - } - break; - case GPS_SENTENCE_GGA: - time.commit(); - if (sentenceHasFix) - { - location.commit(); - altitude.commit(); - } - satellites.commit(); - hdop.commit(); - break; - } - - // Commit all custom listeners of this sentence type - for (TinyGPSCustom *p = customCandidates; p != NULL && strcmp(p->sentenceName, customCandidates->sentenceName) == 0; p = p->next) - p->commit(); - return true; - } - - else - { - ++failedChecksumCount; - } - - return false; - } - - // the first term determines the sentence type - if (curTermNumber == 0) - { - if (strchr("GB", term[0]) && strchr("PNABLD", term[1]) != NULL && !strcmp(term + 2, _RMCterm)) - curSentenceType = GPS_SENTENCE_RMC; - else if (strchr("GB", term[0]) && strchr("PNABLD", term[1]) != NULL && !strcmp(term + 2, _GGAterm)) - curSentenceType = GPS_SENTENCE_GGA; - else - curSentenceType = GPS_SENTENCE_OTHER; - - // Any custom candidates of this sentence type? - for (customCandidates = customElts; customCandidates != NULL && strcmp(customCandidates->sentenceName, term) < 0; customCandidates = customCandidates->next); - if (customCandidates != NULL && strcmp(customCandidates->sentenceName, term) > 0) - customCandidates = NULL; - - return false; - } - - if (curSentenceType != GPS_SENTENCE_OTHER && term[0]) - switch(COMBINE(curSentenceType, curTermNumber)) - { - case COMBINE(GPS_SENTENCE_RMC, 1): // Time in both sentences - case COMBINE(GPS_SENTENCE_GGA, 1): - time.setTime(term); - break; - case COMBINE(GPS_SENTENCE_RMC, 2): // RMC validity - sentenceHasFix = term[0] == 'A'; - break; - case COMBINE(GPS_SENTENCE_RMC, 3): // Latitude - case COMBINE(GPS_SENTENCE_GGA, 2): - location.setLatitude(term); - break; - case COMBINE(GPS_SENTENCE_RMC, 4): // N/S - case COMBINE(GPS_SENTENCE_GGA, 3): - location.rawNewLatData.negative = term[0] == 'S'; - break; - case COMBINE(GPS_SENTENCE_RMC, 5): // Longitude - case COMBINE(GPS_SENTENCE_GGA, 4): - location.setLongitude(term); - break; - case COMBINE(GPS_SENTENCE_RMC, 6): // E/W - case COMBINE(GPS_SENTENCE_GGA, 5): - location.rawNewLngData.negative = term[0] == 'W'; - break; - case COMBINE(GPS_SENTENCE_RMC, 7): // Speed (RMC) - speed.set(term); - break; - case COMBINE(GPS_SENTENCE_RMC, 8): // Course (RMC) - course.set(term); - break; - case COMBINE(GPS_SENTENCE_RMC, 9): // Date (RMC) - date.setDate(term); - break; - case COMBINE(GPS_SENTENCE_GGA, 6): // Fix data (GGA) - sentenceHasFix = term[0] > '0'; - location.newFixQuality = (TinyGPSLocation::Quality)term[0]; - break; - case COMBINE(GPS_SENTENCE_GGA, 7): // Satellites used (GGA) - satellites.set(term); - break; - case COMBINE(GPS_SENTENCE_GGA, 8): // HDOP - hdop.set(term); - break; - case COMBINE(GPS_SENTENCE_GGA, 9): // Altitude (GGA) - altitude.set(term); - break; - case COMBINE(GPS_SENTENCE_RMC, 12): - location.newFixMode = (TinyGPSLocation::Mode)term[0]; - break; - } - - // Set custom values as needed - for (TinyGPSCustom *p = customCandidates; p != NULL && strcmp(p->sentenceName, customCandidates->sentenceName) == 0 && p->termNumber <= curTermNumber; p = p->next) - if (p->termNumber == curTermNumber) - p->set(term); - - return false; -} - -/* static */ -double TinyGPSPlus::distanceBetween(double lat1, double long1, double lat2, double long2) -{ - // returns distance in meters between two positions, both specified - // as signed decimal-degrees latitude and longitude. Uses great-circle - // distance computation for hypothetical sphere of radius 6371009 meters. - // Because Earth is no exact sphere, rounding errors may be up to 0.5%. - // Courtesy of Maarten Lamers - double delta = radians(long1-long2); - double sdlong = sin(delta); - double cdlong = cos(delta); - lat1 = radians(lat1); - lat2 = radians(lat2); - double slat1 = sin(lat1); - double clat1 = cos(lat1); - double slat2 = sin(lat2); - double clat2 = cos(lat2); - delta = (clat1 * slat2) - (slat1 * clat2 * cdlong); - delta = sq(delta); - delta += sq(clat2 * sdlong); - delta = sqrt(delta); - double denom = (slat1 * slat2) + (clat1 * clat2 * cdlong); - delta = atan2(delta, denom); - return delta * _GPS_EARTH_MEAN_RADIUS; -} - -double TinyGPSPlus::courseTo(double lat1, double long1, double lat2, double long2) -{ - // returns course in degrees (North=0, West=270) from position 1 to position 2, - // both specified as signed decimal-degrees latitude and longitude. - // Because Earth is no exact sphere, calculated course may be off by a tiny fraction. - // Courtesy of Maarten Lamers - double dlon = radians(long2-long1); - lat1 = radians(lat1); - lat2 = radians(lat2); - double a1 = sin(dlon) * cos(lat2); - double a2 = sin(lat1) * cos(lat2) * cos(dlon); - a2 = cos(lat1) * sin(lat2) - a2; - a2 = atan2(a1, a2); - if (a2 < 0.0) - { - a2 += TWO_PI; - } - return degrees(a2); -} - -const char *TinyGPSPlus::cardinal(double course) -{ - static const char* directions[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; - int direction = (int)((course + 11.25f) / 22.5f); - return directions[direction % 16]; -} - -void TinyGPSLocation::commit() -{ - rawLatData = rawNewLatData; - rawLngData = rawNewLngData; - fixQuality = newFixQuality; - fixMode = newFixMode; - lastCommitTime = millis(); - valid = updated = true; -} - -void TinyGPSLocation::setLatitude(const char *term) -{ - TinyGPSPlus::parseDegrees(term, rawNewLatData); -} - -void TinyGPSLocation::setLongitude(const char *term) -{ - TinyGPSPlus::parseDegrees(term, rawNewLngData); -} - -double TinyGPSLocation::lat() -{ - updated = false; - double ret = rawLatData.deg + rawLatData.billionths / 1000000000.0; - return rawLatData.negative ? -ret : ret; -} - -double TinyGPSLocation::lng() -{ - updated = false; - double ret = rawLngData.deg + rawLngData.billionths / 1000000000.0; - return rawLngData.negative ? -ret : ret; -} - -void TinyGPSDate::commit() -{ - date = newDate; - lastCommitTime = millis(); - valid = updated = true; -} - -void TinyGPSTime::commit() -{ - time = newTime; - lastCommitTime = millis(); - valid = updated = true; -} - -void TinyGPSTime::setTime(const char *term) -{ - newTime = (uint32_t)TinyGPSPlus::parseDecimal(term); -} - -void TinyGPSDate::setDate(const char *term) -{ - newDate = atol(term); -} - -uint16_t TinyGPSDate::year() -{ - updated = false; - uint16_t year = date % 100; - return year + 2000; -} - -uint8_t TinyGPSDate::month() -{ - updated = false; - return (date / 100) % 100; -} - -uint8_t TinyGPSDate::day() -{ - updated = false; - return date / 10000; -} - -uint8_t TinyGPSTime::hour() -{ - updated = false; - return time / 1000000; -} - -uint8_t TinyGPSTime::minute() -{ - updated = false; - return (time / 10000) % 100; -} - -uint8_t TinyGPSTime::second() -{ - updated = false; - return (time / 100) % 100; -} - -uint8_t TinyGPSTime::centisecond() -{ - updated = false; - return time % 100; -} - -void TinyGPSDecimal::commit() -{ - val = newval; - lastCommitTime = millis(); - valid = updated = true; -} - -void TinyGPSDecimal::set(const char *term) -{ - newval = TinyGPSPlus::parseDecimal(term); -} - -void TinyGPSInteger::commit() -{ - val = newval; - lastCommitTime = millis(); - valid = updated = true; -} - -void TinyGPSInteger::set(const char *term) -{ - newval = atol(term); -} - -TinyGPSCustom::TinyGPSCustom(TinyGPSPlus &gps, const char *_sentenceName, int _termNumber) -{ - begin(gps, _sentenceName, _termNumber); -} - -void TinyGPSCustom::begin(TinyGPSPlus &gps, const char *_sentenceName, int _termNumber) -{ - lastCommitTime = 0; - updated = valid = false; - sentenceName = _sentenceName; - termNumber = _termNumber; - memset(stagingBuffer, '\0', sizeof(stagingBuffer)); - memset(buffer, '\0', sizeof(buffer)); - - // Insert this item into the GPS tree - gps.insertCustom(this, _sentenceName, _termNumber); -} - -void TinyGPSCustom::commit() -{ - strcpy(this->buffer, this->stagingBuffer); - lastCommitTime = millis(); - valid = updated = true; -} - -void TinyGPSCustom::set(const char *term) -{ - strncpy(this->stagingBuffer, term, sizeof(this->stagingBuffer) - 1); -} - -void TinyGPSPlus::insertCustom(TinyGPSCustom *pElt, const char *sentenceName, int termNumber) -{ - TinyGPSCustom **ppelt; - - for (ppelt = &this->customElts; *ppelt != NULL; ppelt = &(*ppelt)->next) - { - int cmp = strcmp(sentenceName, (*ppelt)->sentenceName); - if (cmp < 0 || (cmp == 0 && termNumber < (*ppelt)->termNumber)) - break; - } - - pElt->next = *ppelt; - *ppelt = pElt; -} diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_hikepod.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_hikepod.hpp index 3f5c0566..c642da6d 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_hikepod.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_hikepod.hpp @@ -1,7 +1,6 @@ #pragma once #if !defined(HAL_PLATFORM_SDL) #include "../ui_app_page.hpp" -#include "TinyGPS++.h" #include #include #include From 6ff4465783409aa5ed3ab341fa0efe0e2992731b Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 15:55:55 +0800 Subject: [PATCH 18/70] Use SDK sample logging in APPLaunch Replace APPLaunch direct printf diagnostics with the SDK sample_log.h SLOGI logging macros and add the SDK utilities include path to the APPLaunch build. This keeps startup, launcher, audio, console, file, SSH, LoRa, music, and recorder logs on the common SDK logging path instead of raw stdout formatting. Also move the remaining startup restore of saved brightness and volume out of main.cpp into cp0_lvgl init via init_lvgl_saved_settings(). cp0_lvgl_init() now restores these persisted hardware settings after audio initialization so volume writes continue to route through AudioSystem/cp0_signal_audio_api. Validation: read README_ZH.md cross-compile instructions, then ran cd projects/APPLaunch && export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed. --- ext_components/cp0_lvgl/include/commount.h | 1 + ext_components/cp0_lvgl/src/commount.c | 11 +++ ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 1 + projects/APPLaunch/main/SConstruct | 1 + projects/APPLaunch/main/src/main.cpp | 26 ++----- .../ui/components/page_app/ui_app_console.hpp | 7 +- .../ui/components/page_app/ui_app_file.hpp | 3 +- .../ui/components/page_app/ui_app_lora.hpp | 73 ++++++++++--------- .../ui/components/page_app/ui_app_music.hpp | 31 ++++---- .../ui/components/page_app/ui_app_rec.hpp | 3 +- .../ui/components/page_app/ui_app_ssh.hpp | 3 +- .../main/ui/components/ui_app_launch.cpp | 15 ++-- .../main/ui/components/ui_launch_page.hpp | 7 +- projects/APPLaunch/main/ui/ui.c | 5 +- projects/APPLaunch/main/ui/ui_events.c | 13 ++-- 15 files changed, 106 insertions(+), 94 deletions(-) diff --git a/ext_components/cp0_lvgl/include/commount.h b/ext_components/cp0_lvgl/include/commount.h index 66237b5c..23633d6a 100644 --- a/ext_components/cp0_lvgl/include/commount.h +++ b/ext_components/cp0_lvgl/include/commount.h @@ -7,6 +7,7 @@ extern "C" { void init_lvgl_event(); void init_lvgl_event_cpp(); void init_lvgl_env(); +void init_lvgl_saved_settings(); #ifdef __cplusplus } diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index 05046367..66fe2511 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -29,6 +29,17 @@ void init_lvgl_env() #endif } +void init_lvgl_saved_settings() +{ + int saved_bright = cp0_config_get_int("brightness", -1); + if (saved_bright > 0) + cp0_backlight_write(saved_bright); + + int saved_vol = cp0_config_get_int("volume", -1); + if (saved_vol >= 0) + cp0_volume_write(saved_vol); +} + void init_lvgl_event() { for (int i = 0; i < CP0_C_EVENT_END; i++) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index 645d4967..3d7ab8e0 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -9,6 +9,7 @@ void cp0_lvgl_init(void) init_freambuffer_disp(); init_input(); init_audio(); + init_lvgl_saved_settings(); init_battery(); init_camera(); } diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index a1b1177a..d1c6768a 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -57,6 +57,7 @@ SRCS += append_srcs_dir(ADir('ui')) # add includes INCLUDE += [ADir('.'), ADir('include')] INCLUDE += [os.path.join(os.environ['EXT_COMPONENTS_PATH'], 'cp0_lvgl', 'include')] +INCLUDE += [os.path.join(os.environ['SDK_PATH'], 'components', 'utilities', 'include')] # add requirements REQUIREMENTS += ['lvgl_component', 'pthread'] diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index 9af84024..c6f46838 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -10,6 +10,7 @@ #include "cp0_lvgl_file.hpp" #include "hal_lvgl_bsp.h" #include "global_config.h" +#include "sample_log.h" #if CONFIG_BACKWARD_CPP_ENABLED #define BACKWARD_HAS_DW 1 #include "backward.hpp" @@ -58,38 +59,25 @@ int main(void) static const std::string default_lock_file = cp0_file_path("lock_file"); lock_file = default_lock_file.c_str(); lv_init(); - printf("[BOOT] lv_init() done\n"); + SLOGI("[BOOT] lv_init() done"); - printf("[BOOT] cp0_lvgl_init() starting...\n"); + SLOGI("[BOOT] cp0_lvgl_init() starting..."); cp0_lvgl_init(); - printf("[BOOT] cp0_lvgl_init() done\n"); + SLOGI("[BOOT] cp0_lvgl_init() done"); if (LV_EVENT_KEYBOARD == 0) LV_EVENT_KEYBOARD = lv_event_register_id(); - // Restore saved brightness - { - int saved_bright = cp0_config_get_int("brightness", -1); - if (saved_bright > 0) - cp0_backlight_write(saved_bright); - } - - // Restore saved volume - { - int saved_vol = cp0_config_get_int("volume", -1); - if (saved_vol >= 0) - cp0_volume_write(saved_vol); - } ui_init(); // Force full-screen refresh immediately after init - printf("[BOOT] ui_init done, forcing full refresh...\n"); + SLOGI("[BOOT] ui_init done, forcing full refresh..."); lv_obj_invalidate(lv_scr_act()); lv_refr_now(NULL); - printf("[BOOT] First frame flushed to fb0.\n"); + SLOGI("[BOOT] First frame flushed to fb0."); /*Handle LVGL tasks*/ - printf("Entering main loop (FULL render mode)...\n"); + SLOGI("Entering main loop (FULL render mode)..."); while(1) { APPLaunch_lock(); lv_timer_handler(); diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp index c970307a..98e475bd 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #include "../ui_app_page.hpp" #include #include @@ -314,7 +315,7 @@ class UIConsolePage : public app_base if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) { struct key_item *elm = (struct key_item *)lv_event_get_param(e); - printf("[CONSOLE] code=%u state=%s sym=%s utf8_len=%zu pty_active=%d waiting_exit=%d\n", + SLOGI("[CONSOLE] code=%u state=%s sym=%s utf8_len=%zu pty_active=%d waiting_exit=%d", elm->key_code, kbd_state_name(elm->key_state), elm->sym_name, strlen(elm->utf8), (int)terminal_active, (int)waiting_key_to_exit); if (waiting_key_to_exit && (elm->key_state == 0)) @@ -335,7 +336,7 @@ class UIConsolePage : public app_base if (pty_handle != NULL && terminal_active) { if (elm->key_state) { - printf("[CONSOLE] -> PTY write (state=%s)\n", kbd_state_name(elm->key_state)); + SLOGI("[CONSOLE] -> PTY write (state=%s)", kbd_state_name(elm->key_state)); write_key_to_pty(elm->key_code, elm->utf8); } } @@ -379,7 +380,7 @@ class UIConsolePage : public app_base if (std::chrono::duration_cast(end_time - start_time).count() >= 5) { end_status = 0; - printf("[CONSOLE] ESC held 5s -> kill PTY and go back home\n"); + SLOGI("[CONSOLE] ESC held 5s -> kill PTY and go back home"); self->stop_pty(); self->terminal_active = false; if (self->go_back_home) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp index 9f41bcc7..1b39c139 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #include "../ui_app_page.hpp" #include "compat/input_keys.h" #include @@ -90,7 +91,7 @@ class UIFilePage : public app_base DIR *dp = opendir(current_path_.c_str()); if (!dp) { - printf("[File] opendir failed: %s\n", current_path_.c_str()); + SLOGI("[File] opendir failed: %s", current_path_.c_str()); return; } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp index 6b536572..51b82c80 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #if !defined(HAL_PLATFORM_SDL) #include "ui_app_lora.hpp" #include "lvgl/lvgl.h" @@ -488,12 +489,12 @@ static int gpio_init_output_any(const char *chip_env_name, const char *offset_en if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); if (offset_env && offset_env[0]) offset = atoi(offset_env); if (line_fd && gpio_open_output_line(chip_path, offset, value, line_fd)) { - printf("LoRa GPIO %s via cdev: %s[%d]=%d\n", line_name ? line_name : "out", chip_path, offset, value); + SLOGI("LoRa GPIO %s via cdev: %s[%d]=%d", line_name ? line_name : "out", chip_path, offset, value); return 0; } #endif if (gpio_init_output(gpio, value) == 0) return 0; - printf("LoRa GPIO %s init failed: gpio=%d errno=%d\n", line_name ? line_name : "out", gpio, errno); + SLOGI("LoRa GPIO %s init failed: gpio=%d errno=%d", line_name ? line_name : "out", gpio, errno); return -1; } @@ -508,12 +509,12 @@ static int gpio_init_input_any(const char *chip_env_name, const char *offset_env if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); if (offset_env && offset_env[0]) offset = atoi(offset_env); if (line_fd && gpio_open_input_line(chip_path, offset, line_fd)) { - printf("LoRa GPIO %s via cdev: %s[%d]\n", line_name ? line_name : "in", chip_path, offset); + SLOGI("LoRa GPIO %s via cdev: %s[%d]", line_name ? line_name : "in", chip_path, offset); return 0; } #endif if (gpio_init_input(gpio) == 0) return 0; - printf("LoRa GPIO %s input init failed: gpio=%d errno=%d\n", line_name ? line_name : "in", gpio, errno); + SLOGI("LoRa GPIO %s input init failed: gpio=%d errno=%d", line_name ? line_name : "in", gpio, errno); return -1; } @@ -528,12 +529,12 @@ static int gpio_init_input_irq_any(const char *chip_env_name, const char *offset if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); if (offset_env && offset_env[0]) offset = atoi(offset_env); if (line_fd && gpio_open_input_event_line(chip_path, offset, line_fd)) { - printf("LoRa GPIO %s irq-event via cdev: %s[%d]\n", line_name ? line_name : "irq", chip_path, offset); + SLOGI("LoRa GPIO %s irq-event via cdev: %s[%d]", line_name ? line_name : "irq", chip_path, offset); return 0; } #endif if (line_fd && gpio_init_input_irq_sysfs(gpio, line_fd) == 0) { - printf("LoRa GPIO %s irq-event via sysfs: gpio%d rising\n", line_name ? line_name : "irq", gpio); + SLOGI("LoRa GPIO %s irq-event via sysfs: gpio%d rising", line_name ? line_name : "irq", gpio); return 0; } return -1; @@ -595,7 +596,7 @@ static void lora_update_power_debug(const char *stage, int sysfs_ret, int gpio_v const char *value_text = gpio_value < 0 ? "read_fail" : (gpio_value ? "HIGH" : "LOW"); snprintf(text, sizeof(text), "5VDBG %s cdev=%s chip=%s[%d] sysfs_ret=%d gpio5=%s", stage ? stage : "?", cdev_ok ? "ok" : "fail", chip_text, g_hat_5vout_offset, sysfs_ret, value_text); - printf("%s\n", text); + SLOGI("%s", text); } static bool hat_5vout_prepare_line(void) @@ -823,7 +824,7 @@ static bool probe_lora_spi_device(void) snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "SPI: no spidev found"); return false; } - printf("LoRa SPI probe policy: prefer SPI0 only, CE1 then CE0\n"); + SLOGI("LoRa SPI probe policy: prefer SPI0 only, CE1 then CE0"); summary[0] = '\0'; for (size_t i = 0; i < candidate_count; ++i) { const char *dev = candidates[i]; @@ -844,7 +845,7 @@ static bool probe_lora_spi_device(void) snprintf(g_spi_device, sizeof(g_spi_device), "%s", dev); g_lora_nss_manual = false; const char *cs_name = strstr(g_spi_device, "spidev0.1") ? "SPI0-CE1" : (strstr(g_spi_device, "spidev0.0") ? "SPI0-CE0" : "non-SPI0"); - printf("LoRa probe: trying %s [%s] (cs=hw-auto)\n", g_spi_device, cs_name); + SLOGI("LoRa probe: trying %s [%s] (cs=hw-auto)", g_spi_device, cs_name); g_lora_initialized = false; if (g_spi_fd >= 0) { close(g_spi_fd); g_spi_fd = -1; } if (gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", g_lora_rst_gpio, 1, &g_lora_rst_fd, "RST") < 0) { @@ -876,7 +877,7 @@ static bool probe_lora_spi_device(void) snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "status read failed on %s", g_spi_device); return false; } - printf("LoRa probe: %s [%s] (cs=hw-auto) status=0x%02X\n", g_spi_device, cs_name, status); + SLOGI("LoRa probe: %s [%s] (cs=hw-auto) status=0x%02X", g_spi_device, cs_name, status); snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "probe ok on %s[%s] cs=hw-auto status=0x%02X", g_spi_device, cs_name, status); snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "FOUND: %s (%s)", g_spi_device, cs_name); return true; @@ -993,7 +994,7 @@ static void lora_set_diag_step(const char *step, int code, const char *detail) { snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "%s%s%s | rc=%d", step ? step : "diag", (detail && detail[0]) ? " | " : "", (detail && detail[0]) ? detail : "", code); - printf("LoRa diag: %s\n", g_lora_last_diag); + SLOGI("LoRa diag: %s", g_lora_last_diag); } static const char *lora_radiolib_status_text(int16_t state) @@ -1015,14 +1016,14 @@ static const char *lora_radiolib_status_text(int16_t state) static void lora_capture_device_errors(const char *stage, uint16_t irq_status) { if (!g_lora_initialized || g_lora_radio == NULL) return; - printf("LoRa error: %s irq=0x%04X\n", stage ? stage : "radio_err", irq_status); + SLOGI("LoRa error: %s irq=0x%04X", stage ? stage : "radio_err", irq_status); snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "%s irq=0x%04X", stage ? stage : "radio_err", irq_status); } static bool lora_send_text_packet(const char *payload) { if (!g_lora_initialized || g_lora_radio == NULL) { - printf("LoRa TX: not initialized\n"); + SLOGI("LoRa TX: not initialized"); return false; } if (payload == NULL || payload[0] == '\0') return false; @@ -1039,12 +1040,12 @@ static bool lora_send_text_packet(const char *payload) if (state != RADIOLIB_ERR_NONE) { g_lora_tx_in_progress = false; g_lora_pending_rx_after_tx = false; - printf("LoRa TX: startTransmit failed rc=%d(%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa TX: startTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); return false; } g_lora_tx_in_progress = true; g_lora_tx_start_ms = g_lora_last_auto_tx_ms = get_monotonic_ms(); - printf("LoRa TX: sending '%s'\n", g_lora_last_tx); + SLOGI("LoRa TX: sending '%s'", g_lora_last_tx); return true; } @@ -1060,32 +1061,32 @@ static void lora_send_demo_packet(void) int16_t state = g_lora_radio->startTransmit((uint8_t *)g_lora_last_tx, strlen(g_lora_last_tx)); if (state != RADIOLIB_ERR_NONE) { g_lora_tx_in_progress = false; - printf("LoRa TX: demo startTransmit failed rc=%d(%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa TX: demo startTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); return; } g_lora_tx_in_progress = true; g_lora_tx_start_ms = g_lora_last_auto_tx_ms = get_monotonic_ms(); - printf("LoRa TX: demo sending '%s'\n", g_lora_last_tx); + SLOGI("LoRa TX: demo sending '%s'", g_lora_last_tx); ++g_lora_tx_counter; } static void lora_start_receive_mode(void) { if (!g_lora_initialized || g_lora_radio == NULL) { - printf("LoRa RX: startReceive skipped, not initialized\n"); + SLOGI("LoRa RX: startReceive skipped, not initialized"); return; } if (g_lora_tx_in_progress) { - printf("LoRa RX: startReceive skipped, TX in progress\n"); + SLOGI("LoRa RX: startReceive skipped, TX in progress"); g_lora_pending_rx_after_tx = true; return; } g_lora_tx_mode = false; g_lora_selected_tx_mode = false; g_lora_pending_rx_after_tx = false; - printf("LoRa RX: startReceive()\n"); + SLOGI("LoRa RX: startReceive()"); int16_t state = g_lora_radio->startReceive(); - printf("LoRa RX: startReceive rc=%d(%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa RX: startReceive rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); if (state != RADIOLIB_ERR_NONE) { snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "startReceive rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); } @@ -1095,7 +1096,7 @@ static void lora_apply_mode(bool tx_mode) { g_lora_selected_tx_mode = tx_mode; if (!g_lora_initialized || g_lora_radio == NULL) { - printf("LoRa mode: not initialized\n"); + SLOGI("LoRa mode: not initialized"); return; } if (tx_mode) { @@ -1103,19 +1104,19 @@ static void lora_apply_mode(bool tx_mode) g_lora_tx_mode = true; g_lora_last_auto_tx_ms = get_monotonic_ms(); if (g_lora_tx_in_progress) { - printf("LoRa mode: TX already in progress\n"); + SLOGI("LoRa mode: TX already in progress"); return; } int16_t state = g_lora_radio->standby(); if (state == RADIOLIB_ERR_NONE) { - printf("LoRa mode: TX ready\n"); + SLOGI("LoRa mode: TX ready"); } else { - printf("LoRa mode: set TX failed rc=%d(%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa mode: set TX failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); } } else { if (g_lora_tx_in_progress) { g_lora_pending_rx_after_tx = true; - printf("LoRa mode: TX in progress, will RX after done\n"); + SLOGI("LoRa mode: TX in progress, will RX after done"); return; } g_lora_pending_rx_after_tx = false; @@ -1150,7 +1151,7 @@ static void lora_service_irq_once(void) uint32_t irq_flags = g_lora_radio->getIrqFlags(); if (irq_flags != RADIOLIB_SX126X_IRQ_NONE || irq_event) { - printf("LoRa IRQ: event=%d flags=0x%08lX tx_in_progress=%d tx_mode=%d\n", + SLOGI("LoRa IRQ: event=%d flags=0x%08lX tx_in_progress=%d tx_mode=%d", irq_event ? 1 : 0, (unsigned long)irq_flags, g_lora_tx_in_progress ? 1 : 0, g_lora_tx_mode ? 1 : 0); } if (!irq_event && irq_flags == RADIOLIB_SX126X_IRQ_NONE) return; @@ -1162,7 +1163,7 @@ static void lora_service_irq_once(void) g_lora_tx_done = true; } else { g_lora_tx_in_progress = false; - printf("LoRa TX: finishTransmit failed rc=%d(%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa TX: finishTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); } } else if (irq_flags & RADIOLIB_SX126X_IRQ_TIMEOUT) { g_lora_tx_in_progress = false; @@ -1176,25 +1177,25 @@ static void lora_service_irq_once(void) if (irq_flags & RADIOLIB_SX126X_IRQ_RX_DONE) { uint8_t rx_buf[sizeof(g_lora_last_rx)] = {0}; int16_t state = g_lora_radio->readData(rx_buf, sizeof(g_lora_last_rx) - 1); - printf("LoRa RX: readData rc=%d(%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa RX: readData rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); if (state == RADIOLIB_ERR_NONE) { memcpy(g_lora_last_rx, rx_buf, sizeof(g_lora_last_rx)); g_lora_last_rx[sizeof(g_lora_last_rx) - 1] = '\0'; g_lora_last_rssi = g_lora_radio->getRSSI(); g_lora_last_snr = g_lora_radio->getSNR(); g_lora_rx_done = true; - printf("LoRa RX OK: '%s' RSSI=%.1f SNR=%.1f\n", g_lora_last_rx, g_lora_last_rssi, g_lora_last_snr); + SLOGI("LoRa RX OK: '%s' RSSI=%.1f SNR=%.1f", g_lora_last_rx, g_lora_last_rssi, g_lora_last_snr); } else if (state != RADIOLIB_ERR_CRC_MISMATCH) { snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "readData rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); } if (!g_lora_tx_mode) lora_start_receive_mode(); } else if (irq_flags & (RADIOLIB_SX126X_IRQ_CRC_ERR | RADIOLIB_SX126X_IRQ_HEADER_ERR)) { snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RX crc/header error irq=0x%04lX", (unsigned long)irq_flags); - printf("LoRa RX error: %s\n", g_lora_last_diag); + SLOGI("LoRa RX error: %s", g_lora_last_diag); if (!g_lora_tx_mode) lora_start_receive_mode(); } else if (irq_flags & RADIOLIB_SX126X_IRQ_TIMEOUT) { snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RX timeout irq=0x%04lX", (unsigned long)irq_flags); - printf("LoRa RX timeout: %s\n", g_lora_last_diag); + SLOGI("LoRa RX timeout: %s", g_lora_last_diag); } } @@ -1269,7 +1270,7 @@ static void lora_init_hardware(void) lora_set_diag_step("power_enable", 0, "start"); if (!hat_5vout_enable()) { - printf("Status: GPIO5 low set failed\n"); + SLOGI("Status: GPIO5 low set failed"); lora_set_diag_step("power_enable", 1, "GPIO5 low set failed"); } usleep(100000); @@ -1354,7 +1355,7 @@ static void lora_init_hardware(void) if (state != RADIOLIB_ERR_NONE) { g_lora_initialized = false; g_lora_hw_ready = false; snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RadioLib begin rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - printf("LoRa init failed: rc=%d (%s)\n", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa init failed: rc=%d (%s)", (int)state, lora_radiolib_status_text(state)); lora_set_diag_step("radiolib_begin", state, g_lora_last_diag); return; } @@ -1372,7 +1373,7 @@ static void lora_init_hardware(void) g_lora_last_auto_tx_ms = get_monotonic_ms(); lora_set_diag_step("ready", 0, "LoRa init finished"); - printf("LoRa: init done, auto enter RX\n"); + SLOGI("LoRa: init done, auto enter RX"); lora_start_receive_mode(); } @@ -1781,7 +1782,7 @@ static void lora_key_event_cb(lv_event_t *e) else if (key == KEY_BACKSPACE) key = LV_KEY_BACKSPACE; else if (key == KEY_DELETE) key = LV_KEY_DEL; - printf("[LoRa] raw=%u cp=%u key=0x%X view=%d\n", elm->key_code, cp, key, (int)g_lora_view); + SLOGI("[LoRa] raw=%u cp=%u key=0x%X view=%d", elm->key_code, cp, key, (int)g_lora_view); (void)handle_app_key(key); } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp index 3ac40986..239c96a2 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #include "../ui_app_page.hpp" #include "compat/input_keys.h" @@ -137,7 +138,7 @@ class UIMusicPage : public app_base if (!audio_ready_) { - printf("[Music] Cannot play: PulseAudio/miniaudio not ready\n"); + SLOGI("[Music] Cannot play: PulseAudio/miniaudio not ready"); play_state_ = PlayState::STOPPED; update_main_ui(); return; @@ -156,7 +157,7 @@ class UIMusicPage : public app_base } else { - printf("[Music] resume failed, result=%d\n", static_cast(r)); + SLOGI("[Music] resume failed, result=%d", static_cast(r)); stop_playback(); play_state_ = PlayState::STOPPED; } @@ -423,7 +424,7 @@ class UIMusicPage : public app_base { struct key_item *elm = static_cast(lv_event_get_param(e)); - printf("[MUSIC][KEYBOARD] code=%u state=%s sym=%s view=%d\n", + SLOGI("[MUSIC][KEYBOARD] code=%u state=%s sym=%s view=%d", elm->key_code, kbd_state_name(elm->key_state), elm->sym_name, @@ -437,7 +438,7 @@ class UIMusicPage : public app_base uint32_t raw = lv_event_get_key(e); uint32_t key = fzxc_to_lv_arrow(raw); - printf("[MUSIC][LV_KEY] raw=%u mapped=%u view=%d\n", + SLOGI("[MUSIC][LV_KEY] raw=%u mapped=%u view=%d", raw, key, static_cast(view_state_)); @@ -492,7 +493,7 @@ class UIMusicPage : public app_base break; case LV_KEY_ESC: - printf("[MUSIC] ESC -> go_back_home()\n"); + SLOGI("[MUSIC] ESC -> go_back_home()"); go_back_home(); break; @@ -746,7 +747,7 @@ class UIMusicPage : public app_base } else { - printf("[Music] opendir failed: %s\n", browse_dir_.c_str()); + SLOGI("[Music] opendir failed: %s", browse_dir_.c_str()); } if (!browse_entries_.empty() && browse_entries_[0] == "..") @@ -907,7 +908,7 @@ class UIMusicPage : public app_base if (!dp) { - printf("[Music] Cannot open dir: %s\n", dir.c_str()); + SLOGI("[Music] Cannot open dir: %s", dir.c_str()); update_main_ui(); return; } @@ -943,7 +944,7 @@ class UIMusicPage : public app_base std::sort(playlist_.begin(), playlist_.end()); - printf("[Music] Loaded %d audio files from %s\n", + SLOGI("[Music] Loaded %d audio files from %s", static_cast(playlist_.size()), dir.c_str()); @@ -965,7 +966,7 @@ class UIMusicPage : public app_base if (r != MA_SUCCESS) { - printf("[Music] ma_context_init PulseAudio failed, result=%d\n", static_cast(r)); + SLOGI("[Music] ma_context_init PulseAudio failed, result=%d", static_cast(r)); audio_ready_ = false; return; } @@ -977,7 +978,7 @@ class UIMusicPage : public app_base if (r != MA_SUCCESS) { - printf("[Music] ma_engine_init failed, result=%d\n", static_cast(r)); + SLOGI("[Music] ma_engine_init failed, result=%d", static_cast(r)); ma_context_uninit(&audio_ctx_); @@ -989,7 +990,7 @@ class UIMusicPage : public app_base audio_timer_ = lv_timer_create(UIMusicPage::static_audio_timer_cb, 200, this); - printf("[Music] miniaudio initialized with PulseAudio backend\n"); + SLOGI("[Music] miniaudio initialized with PulseAudio backend"); } void uninit_audio() @@ -1065,7 +1066,7 @@ class UIMusicPage : public app_base if (!audio_ready_) { - printf("[Music] Audio not ready. PulseAudio backend unavailable.\n"); + SLOGI("[Music] Audio not ready. PulseAudio backend unavailable."); return false; } @@ -1077,7 +1078,7 @@ class UIMusicPage : public app_base const std::string &file = playlist_[current_track_]; - printf("[Music] Playing by miniaudio: %s\n", file.c_str()); + SLOGI("[Music] Playing by miniaudio: %s", file.c_str()); track_finished_.store(false); @@ -1093,7 +1094,7 @@ class UIMusicPage : public app_base if (r != MA_SUCCESS) { - printf("[Music] ma_sound_init_from_file failed, result=%d, file=%s\n", + SLOGI("[Music] ma_sound_init_from_file failed, result=%d, file=%s", static_cast(r), file.c_str()); @@ -1114,7 +1115,7 @@ class UIMusicPage : public app_base if (r != MA_SUCCESS) { - printf("[Music] ma_sound_start failed, result=%d\n", static_cast(r)); + SLOGI("[Music] ma_sound_start failed, result=%d", static_cast(r)); stop_playback(); return false; } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp index 55bc41e8..5c9e1ba5 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #include "../ui_app_page.hpp" @@ -293,7 +294,7 @@ class rec_page : public app_base // lvgl_add_call(but[0], [](lv_event_code_t c, void *d){ // if(c == LV_EVENT_CLICKED) // { - // printf("butt will be clicked\n"); + // SLOGI("butt will be clicked"); // } // }, NULL); } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp index 925e9faa..27b63243 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #include "../ui_app_page.hpp" #include "ui_app_console.hpp" #include "compat/input_keys.h" @@ -220,7 +221,7 @@ class UISSHPage : public app_base if (!port.empty() && port != "22") cmd += " -p " + port; - printf("[SSH] Launching: %s\n", cmd.c_str()); + SLOGI("[SSH] Launching: %s", cmd.c_str()); // Create console page console_page_ = std::make_shared(); diff --git a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp index 08a452b6..c1c674ca 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp +++ b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp @@ -17,6 +17,7 @@ #include "ui_launch_page.hpp" #include "../ui_loading.h" #include "page_app.h" +#include "sample_log.h" /* img_path() now defined in ui_app_page.hpp */ @@ -272,26 +273,26 @@ class app_launch_S static void lv_go_back_home(void *arg) { auto self = (app_launch_S *)arg; - printf("[HOME] lv_go_back_home executing (page=%p)\n", self->app_Page.get()); + SLOGI("[HOME] lv_go_back_home executing (page=%p)", self->app_Page.get()); lv_timer_enable(true); lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); lv_disp_load_scr(ui_Screen1); lv_refr_now(NULL); if (self->app_Page) self->app_Page.reset(); - printf("[HOME] lv_go_back_home done, on launcher home\n"); + SLOGI("[HOME] lv_go_back_home done, on launcher home"); } void go_back_home() { - printf("[HOME] go_back_home() requested, scheduling async call (page=%p)\n", app_Page.get()); + SLOGI("[HOME] go_back_home() requested, scheduling async call (page=%p)", app_Page.get()); lv_async_call(lv_go_back_home, this); } // Changed to accept std::string and no longer depend on app::Exec void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) { - printf("Launching terminal app: %s\n", exec.c_str()); + SLOGI("Launching terminal app: %s", exec.c_str()); /* Instant visual feedback; paint before the (potentially slow) * Console page construction so the user sees it right away. */ ui_loading_show("Loading..."); @@ -311,7 +312,7 @@ class app_launch_S void launch_Exec(const std::string &exec, bool keep_root = false) { - printf("Launching external app: %s (keep_root=%d)\n", exec.c_str(), keep_root); + SLOGI("Launching external app: %s (keep_root=%d)", exec.c_str(), keep_root); /* Show overlay BEFORE we tear down LVGL input/timers so the user * gets immediate feedback when ENTER was pressed. The overlay * stays drawn on the framebuffer right up until the child takes @@ -326,7 +327,7 @@ class app_launch_S lv_refr_now(disp); int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); - printf("App %s exited with code %d\n", exec.c_str(), ret); + SLOGI("App %s exited with code %d", exec.c_str(), ret); lv_timer_enable(true); if (indev) lv_indev_set_group(indev, Screen1group); @@ -623,7 +624,7 @@ class app_launch_S if (cp0_dir_watch_poll(self->dir_watcher) > 0) { - printf("app_dir_watch_cb: applications dir changed, reloading...\n"); + SLOGI("app_dir_watch_cb: applications dir changed, reloading..."); self->applications_reload(); } } diff --git a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp index 55c49786..457d33c5 100644 --- a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp @@ -1,4 +1,5 @@ #pragma once +#include "sample_log.h" #include "ui_app_page.hpp" #include @@ -561,14 +562,14 @@ class UILaunchPage : public home_base // ==================== App launch helper ==================== void launch_exec_in_terminal(lp_app_item *it) { - printf("Launching terminal app: %s\n", it->Exec.c_str()); + SLOGI("Launching terminal app: %s", it->Exec.c_str()); // Simple implementation: fork+exec directly without terminal UI launch_exec(it); } void launch_exec(lp_app_item *it) { - printf("Launching external app: %s\n", it->Exec.c_str()); + SLOGI("Launching external app: %s", it->Exec.c_str()); lv_disp_t *disp = lv_disp_get_default(); lv_indev_t *indev = lv_indev_get_next(NULL); if (indev) lv_indev_set_group(indev, NULL); @@ -576,7 +577,7 @@ class UILaunchPage : public home_base lv_refr_now(disp); int ret = cp0_process_exec_blocking(it->Exec.c_str(), &LVGL_HOME_KEY_FLAG, 0); - printf("App %s exited with code %d\n", it->Exec.c_str(), ret); + SLOGI("App %s exited with code %d", it->Exec.c_str(), ret); lv_timer_enable(true); if (indev) lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); lv_disp_load_scr(ui_Screen1); diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index ef9ae0cf..2578e825 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -9,6 +9,7 @@ #include #include "lvgl/src/widgets/gif/lv_gif.h" #include "cp0_lvgl_app.h" +#include "sample_log.h" ///////////////////// VARIABLES //////////////////// @@ -178,7 +179,7 @@ void font_manager_init(void) void home_screen_load() { - printf("[HOME] home_screen_load() - loading launcher home screen\n"); + SLOGI("[HOME] home_screen_load() - loading launcher home screen"); ui____initial_actions0 = lv_obj_create(NULL); lv_disp_load_scr(ui_Screen1); lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); @@ -196,7 +197,7 @@ void ui_event_logo_over(lv_event_t * e) { lv_event_code_t event_code = lv_event_get_code(e); if(event_code == LV_EVENT_READY && !done) { done = 1; - printf("[GIF] first LV_EVENT_READY -> pause + home_screen_load()\n"); + SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); if (startup_gif) lv_gif_pause(startup_gif); diff --git a/projects/APPLaunch/main/ui/ui_events.c b/projects/APPLaunch/main/ui/ui_events.c index 83a4e995..93b7187c 100644 --- a/projects/APPLaunch/main/ui/ui_events.c +++ b/projects/APPLaunch/main/ui/ui_events.c @@ -1,4 +1,5 @@ #include "ui.h" +#include "sample_log.h" #include #include @@ -114,10 +115,10 @@ static ma_result audio_find_es8388_device(ma_context *context, ma_device_id *out return result; } - printf("[AUDIO] PulseAudio playback devices:\n"); + SLOGI("[AUDIO] PulseAudio playback devices:"); for (ma_uint32 i = 0; i < playbackCount; i++) { - printf("[AUDIO] [%u] %s%s\n", + SLOGI("[AUDIO] [%u] %s%s", i, pPlaybackInfos[i].name, pPlaybackInfos[i].isDefault ? " [default]" : ""); @@ -127,7 +128,7 @@ static ma_result audio_find_es8388_device(ma_context *context, ma_device_id *out *outDeviceID = pPlaybackInfos[i].id; - printf("[AUDIO] selected PulseAudio playback device: %s\n", + SLOGI("[AUDIO] selected PulseAudio playback device: %s", pPlaybackInfos[i].name); return MA_SUCCESS; @@ -221,7 +222,7 @@ int audio_system_init(void) g_audio_inited = 1; - printf("[AUDIO] audio system initialized with PulseAudio backend\n"); + SLOGI("[AUDIO] audio system initialized with PulseAudio backend"); pthread_mutex_unlock(&g_audio_mutex); return 0; @@ -314,7 +315,7 @@ int audio_load_sounds(void) g_audio_sounds_loaded = 1; - printf("[AUDIO] sounds loaded\n"); + SLOGI("[AUDIO] sounds loaded"); pthread_mutex_unlock(&g_audio_mutex); return 0; @@ -729,7 +730,7 @@ void main_key_switch(lv_event_t *e) struct key_item *elm = (struct key_item *)lv_event_get_param(e); uint32_t code = fzxc_to_arrow(elm->key_code); - printf("[LAUNCHER] main_key_switch raw=%u->code=%u state=%s sym=%s\n", + SLOGI("[LAUNCHER] main_key_switch raw=%u->code=%u state=%s sym=%s", elm->key_code, code, kbd_state_name(elm->key_state), From a338ae69a61e75f7a403e545933a1016512a236e Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 16:11:04 +0800 Subject: [PATCH 19/70] Add cached cp0 system audio signal Add cp0_signal_system_play as a dedicated cp0_lvgl signal for launcher system sound effects. Implement switch.wav and enter.wav playback inside AudioSystem using a cached miniaudio context, engine, and decoded ma_sound instances so repeated navigation sounds reuse the loaded assets instead of reopening files. Expose cp0_signal_system_play_asset() for C callers and switch APPLaunch navigation/confirm effects to that API. Keep startup.mp3 on the generic cp0_signal_audio_api asset path because it is not one of the cached system effects. Remove the remaining APPLaunch-local miniaudio audio system from ui_events.c, including ES8388 lookup, audio init/load helpers, and direct playback state. Validation: cd projects/APPLaunch && export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed --- .../cp0_lvgl/include/cp0_lvgl_app.h | 2 + .../cp0_lvgl/include/signal_register_plan.h | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_app.cpp | 14 + .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 130 +++++++ projects/APPLaunch/main/ui/ui.c | 21 +- projects/APPLaunch/main/ui/ui_events.c | 361 +----------------- 6 files changed, 154 insertions(+), 375 deletions(-) diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index 2e6f7aa3..67ecc3c4 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -67,6 +67,8 @@ typedef void *cp0_pty_t; typedef int cp0_pid_t; void cp0_signal_audio_api_play_file(const char *path); +void cp0_signal_audio_api_play_asset(const char *name); +void cp0_signal_system_play_asset(const char *name); void cp0_config_init(void); int cp0_config_get_int(const char *key, int default_val); diff --git a/ext_components/cp0_lvgl/include/signal_register_plan.h b/ext_components/cp0_lvgl/include/signal_register_plan.h index d9e62b9a..1915a5cf 100644 --- a/ext_components/cp0_lvgl/include/signal_register_plan.h +++ b/ext_components/cp0_lvgl/include/signal_register_plan.h @@ -7,3 +7,4 @@ def_hal_fun(void(), cp0_signal_forkexec) def_hal_fun(void(), cp0_signal_screenshot) def_hal_fun(void(std::function), cp0_signal_battery_pub) def_hal_fun(void(std::list, std::function), cp0_signal_camera_api) +def_hal_fun(void(std::string), cp0_signal_system_play) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp index e896339a..5642fb6c 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp @@ -15,6 +15,20 @@ void cp0_signal_audio_api_play_file(const char *path) } } +void cp0_signal_audio_api_play_asset(const char *name) +{ + if (name && name[0]) { + cp0_signal_audio_api({"Play", std::string(name)}, nullptr); + } +} + +void cp0_signal_system_play_asset(const char *name) +{ + if (name && name[0]) { + cp0_signal_system_play(std::string(name)); + } +} + int cp0_volume_read(void) { int volume = -1; diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index bc6cd319..4a8bef45 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -33,6 +33,11 @@ class AudioSystem // initialize(); } + ~AudioSystem() + { + uninit_system_play(); + } + public: std::function _cap_status_callback; std::unique_ptr ma_cp0_cap_device; @@ -42,6 +47,14 @@ class AudioSystem std::unique_ptr ma_cp0_play_decoder; std::atomic play_finished_reported_{false}; + ma_context system_play_context_{}; + ma_engine system_play_engine_{}; + ma_sound system_sound_switch_{}; + ma_sound system_sound_enter_{}; + bool system_play_inited_ = false; + bool system_sounds_loaded_ = false; + std::mutex system_play_mutex_; + static constexpr int kRecWaveformSize = 128; std::array rec_waveform_{}; size_t rec_waveform_index_ = 0; @@ -155,6 +168,106 @@ class AudioSystem return path.empty() ? file : path; } + int init_system_play_locked() + { + if(system_play_inited_) return 0; + + ma_backend backends[] = { + ma_backend_pulseaudio + }; + + ma_result result = ma_context_init( + backends, + sizeof(backends) / sizeof(backends[0]), + NULL, + &system_play_context_ + ); + if(result != MA_SUCCESS) return -1; + + ma_engine_config engineConfig = ma_engine_config_init(); + engineConfig.pContext = &system_play_context_; + engineConfig.pPlaybackDeviceID = NULL; + engineConfig.channels = 2; + engineConfig.sampleRate = 48000; + + result = ma_engine_init(&engineConfig, &system_play_engine_); + if(result != MA_SUCCESS) + { + ma_context_uninit(&system_play_context_); + return -2; + } + + system_play_inited_ = true; + return 0; + } + + int load_system_sounds_locked() + { + if(system_sounds_loaded_) return 0; + + int ret = init_system_play_locked(); + if(ret != 0) return ret; + + std::string switch_path = resolve_play_file("switch.wav", true); + std::string enter_path = resolve_play_file("enter.wav", true); + + ma_result result = ma_sound_init_from_file( + &system_play_engine_, + switch_path.c_str(), + MA_SOUND_FLAG_DECODE, + NULL, + NULL, + &system_sound_switch_ + ); + if(result != MA_SUCCESS) return -3; + + result = ma_sound_init_from_file( + &system_play_engine_, + enter_path.c_str(), + MA_SOUND_FLAG_DECODE, + NULL, + NULL, + &system_sound_enter_ + ); + if(result != MA_SUCCESS) + { + ma_sound_uninit(&system_sound_switch_); + return -4; + } + + system_sounds_loaded_ = true; + return 0; + } + + ma_sound* system_sound_for_name(const std::string& name) + { + size_t pos = name.find_last_of("/\\"); + std::string base = (pos == std::string::npos) ? name : name.substr(pos + 1); + + if(base == "switch.wav") return &system_sound_switch_; + if(base == "enter.wav") return &system_sound_enter_; + return nullptr; + } + + void uninit_system_play() + { + std::lock_guard lock(system_play_mutex_); + + if(system_sounds_loaded_) + { + ma_sound_uninit(&system_sound_switch_); + ma_sound_uninit(&system_sound_enter_); + system_sounds_loaded_ = false; + } + + if(system_play_inited_) + { + ma_engine_uninit(&system_play_engine_); + ma_context_uninit(&system_play_context_); + system_play_inited_ = false; + } + } + void stop_play_device(bool report_state) { play_finished_reported_.store(false); @@ -354,6 +467,20 @@ class AudioSystem } } public: + void system_play(std::string name) + { + std::lock_guard lock(system_play_mutex_); + + if(load_system_sounds_locked() != 0) return; + + ma_sound* sound = system_sound_for_name(name); + if(sound == nullptr) return; + + ma_sound_stop(sound); + ma_sound_seek_to_pcm_frame(sound, 0); + ma_sound_start(sound); + } + void cap(bool enable) { if(enable) @@ -619,4 +746,7 @@ extern "C" void init_audio(void) cp0_signal_audio_api.append([audio](std::list arg, std::function callback) { audio->api_call(arg, callback); }); + cp0_signal_system_play.append([audio](std::string name) + { audio->system_play(name); }); + } diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index 2578e825..f7f5ffda 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -184,14 +184,9 @@ void home_screen_load() lv_disp_load_scr(ui_Screen1); lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); - static char _startup_snd[256]; - snprintf(_startup_snd, sizeof(_startup_snd), "%s", cp0_file_path("startup.mp3")); - cp0_signal_audio_api_play_file(_startup_snd); + cp0_signal_audio_api_play_asset("startup.mp3"); } -void audio_system_init(); -void audio_load_sounds(); - void ui_event_logo_over(lv_event_t * e) { static int done = 0; lv_event_code_t event_code = lv_event_get_code(e); @@ -200,20 +195,6 @@ void ui_event_logo_over(lv_event_t * e) { SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); if (startup_gif) lv_gif_pause(startup_gif); - - /* - * This runs once while the program is running. - * This is therefore the best place to initialize audio. - * - * audio_system_init(): - * Open the ALSA device and keep it open. - * - * audio_load_sounds(): - * Preload switch.wav / enter.wav. - */ - audio_system_init(); - audio_load_sounds(); - home_screen_load(); } } diff --git a/projects/APPLaunch/main/ui/ui_events.c b/projects/APPLaunch/main/ui/ui_events.c index 93b7187c..056e4a82 100644 --- a/projects/APPLaunch/main/ui/ui_events.c +++ b/projects/APPLaunch/main/ui/ui_events.c @@ -1,19 +1,12 @@ #include "ui.h" #include "sample_log.h" -#include #include -#include -#include #include #include -#include #include "compat/input_keys.h" -// #define MINIAUDIO_IMPLEMENTATION -#include "miniaudio.h" - typedef void (*switch_cb_t)(lv_event_t *); @@ -56,361 +49,19 @@ static int switch_current_pos = 11; // audio // ============================================================ -static ma_context g_audio_context; -static ma_engine g_audio_engine; -static ma_device_id g_audio_device_id; -static ma_sound g_sound_switch; -static ma_sound g_sound_enter; - -static int g_audio_inited = 0; -static int g_audio_sounds_loaded = 0; - -static pthread_mutex_t g_audio_mutex = PTHREAD_MUTEX_INITIALIZER; - -/** - * Copy the string so it is not overwritten if caudio_path() returns a static buffer. - */ -static char *audio_strdup_safe(const char *s) -{ - if (s == NULL) { - return NULL; - } - - size_t len = strlen(s); - char *p = (char *)malloc(len + 1); - if (p == NULL) { - return NULL; - } - - memcpy(p, s, len + 1); - return p; -} - -/** - * Find the ES8388 / ES8389 playback device. - * - * Note: - * This searches the playback device names exposed by PulseAudio / PipeWire-Pulse. - * If the PulseAudio device name does not contain ES8388 / ES8389, - * switch to using the default device directly. - */ -static ma_result audio_find_es8388_device(ma_context *context, ma_device_id *outDeviceID) +static void audio_play_ui_asset(const char *name) { - ma_device_info *pPlaybackInfos = NULL; - ma_uint32 playbackCount = 0; - - ma_device_info *pCaptureInfos = NULL; - ma_uint32 captureCount = 0; - - ma_result result = ma_context_get_devices( - context, - &pPlaybackInfos, - &playbackCount, - &pCaptureInfos, - &captureCount - ); - - if (result != MA_SUCCESS) { - fprintf(stderr, "[AUDIO] ma_context_get_devices failed: %d\n", result); - return result; - } - - SLOGI("[AUDIO] PulseAudio playback devices:"); - - for (ma_uint32 i = 0; i < playbackCount; i++) { - SLOGI("[AUDIO] [%u] %s%s", - i, - pPlaybackInfos[i].name, - pPlaybackInfos[i].isDefault ? " [default]" : ""); - - if (strstr(pPlaybackInfos[i].name, "ES8388") != NULL || - strstr(pPlaybackInfos[i].name, "ES8389") != NULL) { - - *outDeviceID = pPlaybackInfos[i].id; - - SLOGI("[AUDIO] selected PulseAudio playback device: %s", - pPlaybackInfos[i].name); - - return MA_SUCCESS; - } - } - - fprintf(stderr, "[AUDIO] ES8388/ES8389 PulseAudio playback device not found\n"); - return MA_DOES_NOT_EXIST; + cp0_signal_system_play_asset(name); } -/** - * Initialize the audio system. - * - * Only the PulseAudio backend is used here. - * - * This means miniaudio connects through: - * - * ma_backend_pulseaudio - * - * to PulseAudio or pipewire-pulse. - * - * It does not open ALSA devices directly. - */ -int audio_system_init(void) +static void audio_play_switch(void) { - pthread_mutex_lock(&g_audio_mutex); - - if (g_audio_inited) { - pthread_mutex_unlock(&g_audio_mutex); - return 0; - } - - ma_result result; - - /* - * Enable only the PulseAudio backend. - * - * Do not use: - * - * ma_backend_alsa - * - * If the system uses PipeWire, as long as pipewire-pulse is running, - * the PulseAudio backend also works correctly. - */ - ma_backend backends[] = { - ma_backend_pulseaudio - }; - - result = ma_context_init( - backends, - sizeof(backends) / sizeof(backends[0]), - NULL, - &g_audio_context - ); - - if (result != MA_SUCCESS) { - fprintf(stderr, "[AUDIO] ma_context_init PulseAudio failed: %d\n", result); - pthread_mutex_unlock(&g_audio_mutex); - return -1; - } - - ma_engine_config engineConfig = ma_engine_config_init(); - - /* - * Use the PulseAudio context. - */ - engineConfig.pContext = &g_audio_context; - - /* - * Use the default PulseAudio playback device. - */ - engineConfig.pPlaybackDeviceID = NULL; - - /* - * Use a fixed output format. - * - * If the default PulseAudio / PipeWire sample rate is not 48000, - * it can also be changed to 44100. - */ - engineConfig.channels = 2; - engineConfig.sampleRate = 48000; - - result = ma_engine_init(&engineConfig, &g_audio_engine); - - if (result != MA_SUCCESS) { - fprintf(stderr, "[AUDIO] ma_engine_init PulseAudio failed: %d\n", result); - ma_context_uninit(&g_audio_context); - pthread_mutex_unlock(&g_audio_mutex); - return -1; - } - - g_audio_inited = 1; - - SLOGI("[AUDIO] audio system initialized with PulseAudio backend"); - - pthread_mutex_unlock(&g_audio_mutex); - return 0; + audio_play_ui_asset("switch.wav"); } -/** - * Preload switch.wav and enter.wav. - * - * Use MA_SOUND_FLAG_DECODE to decode into memory during initialization. - * Playback will not reopen the wav file repeatedly. - */ -int audio_load_sounds(void) +static void audio_play_enter(void) { - if (!g_audio_inited) { - if (audio_system_init() != 0) { - return -1; - } - } - - pthread_mutex_lock(&g_audio_mutex); - - if (g_audio_sounds_loaded) { - pthread_mutex_unlock(&g_audio_mutex); - return 0; - } - - ma_result result; - - char *switch_path = audio_strdup_safe(caudio_path("switch.wav")); - if (switch_path == NULL) { - fprintf(stderr, "[AUDIO] malloc switch_path failed\n"); - pthread_mutex_unlock(&g_audio_mutex); - return -1; - } - - char *enter_path = audio_strdup_safe(caudio_path("enter.wav")); - if (enter_path == NULL) { - fprintf(stderr, "[AUDIO] malloc enter_path failed\n"); - free(switch_path); - pthread_mutex_unlock(&g_audio_mutex); - return -1; - } - - result = ma_sound_init_from_file( - &g_audio_engine, - switch_path, - MA_SOUND_FLAG_DECODE, - NULL, - NULL, - &g_sound_switch - ); - - if (result != MA_SUCCESS) { - fprintf(stderr, - "[AUDIO] load switch.wav failed: %d, path=%s\n", - result, - switch_path); - - free(switch_path); - free(enter_path); - pthread_mutex_unlock(&g_audio_mutex); - return -1; - } - - result = ma_sound_init_from_file( - &g_audio_engine, - enter_path, - MA_SOUND_FLAG_DECODE, - NULL, - NULL, - &g_sound_enter - ); - - if (result != MA_SUCCESS) { - fprintf(stderr, - "[AUDIO] load enter.wav failed: %d, path=%s\n", - result, - enter_path); - - ma_sound_uninit(&g_sound_switch); - - free(switch_path); - free(enter_path); - pthread_mutex_unlock(&g_audio_mutex); - return -1; - } - - free(switch_path); - free(enter_path); - - g_audio_sounds_loaded = 1; - - SLOGI("[AUDIO] sounds loaded"); - - pthread_mutex_unlock(&g_audio_mutex); - return 0; -} - -/** - * Play an already loaded sound effect. - * - * If the previous playback is still running, stop it and restart from the beginning. - */ -static void audio_play_loaded_sound(ma_sound *sound) -{ - if (!g_audio_sounds_loaded) { - if (audio_load_sounds() != 0) { - return; - } - } - - pthread_mutex_lock(&g_audio_mutex); - - ma_sound_stop(sound); - ma_sound_seek_to_pcm_frame(sound, 0); - ma_sound_start(sound); - - pthread_mutex_unlock(&g_audio_mutex); -} - -/** - * Play the switch sound effect. - */ -void audio_play_switch(void) -{ - audio_play_loaded_sound(&g_sound_switch); -} - -/** - * Play the confirm sound effect. - */ -void audio_play_enter(void) -{ - audio_play_loaded_sound(&g_sound_enter); -} - -/** - * Keep compatibility with the old play_audio(path) API. - * - * Note: - * This API does not preload files. - * However, it does not reopen the PulseAudio device repeatedly. - */ -void play_audio(char *path) -{ - if (path == NULL) { - return; - } - - if (!g_audio_inited) { - if (audio_system_init() != 0) { - return; - } - } - - pthread_mutex_lock(&g_audio_mutex); - - ma_result result = ma_engine_play_sound(&g_audio_engine, path, NULL); - - if (result != MA_SUCCESS) { - fprintf(stderr, "[AUDIO] play_audio failed: %d, path=%s\n", result, path); - } - - pthread_mutex_unlock(&g_audio_mutex); -} - -/** - * Release the audio system. - * - * Call this function during shutdown if the program has an exit flow. - */ -void audio_system_uninit(void) -{ - pthread_mutex_lock(&g_audio_mutex); - - if (g_audio_sounds_loaded) { - ma_sound_uninit(&g_sound_switch); - ma_sound_uninit(&g_sound_enter); - g_audio_sounds_loaded = 0; - } - - if (g_audio_inited) { - ma_engine_uninit(&g_audio_engine); - ma_context_uninit(&g_audio_context); - g_audio_inited = 0; - } - - pthread_mutex_unlock(&g_audio_mutex); + audio_play_ui_asset("enter.wav"); } // ============================================================ From 8479ad51afb60f86833399c313a2de60632d7f78 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 16:14:44 +0800 Subject: [PATCH 20/70] Remove unused APPLaunch input groups Drop AppStoregroup, APPNotegroup, and AppPythongroup from ui_input_group because no APPLaunch callers reference them or switch input devices to those groups. Keep Screen1group and input_group_init() because the launcher still uses Screen1group when loading home and restoring focus after app/page transitions. Also remove unused stdio/string includes from ui_input_group.c. Validation: cd projects/APPLaunch && export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk && scons -j8 --implicit-deps-changed --- projects/APPLaunch/main/ui/ui_input_group.c | 14 ++------------ projects/APPLaunch/main/ui/ui_input_group.h | 3 --- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/projects/APPLaunch/main/ui/ui_input_group.c b/projects/APPLaunch/main/ui/ui_input_group.c index bc4848cb..b07ff9cd 100644 --- a/projects/APPLaunch/main/ui/ui_input_group.c +++ b/projects/APPLaunch/main/ui/ui_input_group.c @@ -1,20 +1,10 @@ #include "ui.h" -#include -#include -lv_group_t *Screen1group = NULL; -lv_group_t *AppStoregroup = NULL; -lv_group_t *APPNotegroup = NULL; -lv_group_t *AppPythongroup = NULL; - - +lv_group_t *Screen1group = NULL; void input_group_init(void) { Screen1group = lv_group_create(); - AppStoregroup = lv_group_create(); - APPNotegroup = lv_group_create(); - AppPythongroup = lv_group_create(); lv_group_add_obj(Screen1group, ui_Screen1); lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); -} \ No newline at end of file +} diff --git a/projects/APPLaunch/main/ui/ui_input_group.h b/projects/APPLaunch/main/ui/ui_input_group.h index dc804fbc..7d1e4373 100644 --- a/projects/APPLaunch/main/ui/ui_input_group.h +++ b/projects/APPLaunch/main/ui/ui_input_group.h @@ -11,9 +11,6 @@ extern "C" { #endif extern lv_group_t *Screen1group; -extern lv_group_t *AppStoregroup; -extern lv_group_t *APPNotegroup; -extern lv_group_t *AppPythongroup; void input_group_init(void); From ffbd68d81eaad18f52e0fc171b0c0c813742868b Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Thu, 11 Jun 2026 18:20:37 +0800 Subject: [PATCH 21/70] Migrate launcher animations and fix SDL asset paths --- .../cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp | 321 ++++++++++++++++++ .../cp0_lvgl/src/sdl/sdl_lvgl_file.cpp | 181 +++++++++- .../APPLaunch/main/ui/Animation/Animation.hpp | 181 ++++++++++ .../ui/Animation/ui_launcher_animation.cpp | 148 ++++++++ .../main/ui/Animation/ui_launcher_animation.h | 16 + .../main/ui/components/ui_app_launch.cpp | 11 +- projects/APPLaunch/main/ui/ui_events.c | 21 +- 7 files changed, 851 insertions(+), 28 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp create mode 100644 projects/APPLaunch/main/ui/Animation/Animation.hpp create mode 100644 projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp create mode 100644 projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp new file mode 100644 index 00000000..44b8d23a --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp @@ -0,0 +1,321 @@ +#include "cp0_lvgl_app.h" + +#include "hal/hal_audio.h" +#include "hal/hal_config.h" +#include "hal/hal_filesystem.h" +#include "hal/hal_network.h" +#include "hal/hal_process.h" +#include "hal/hal_pty.h" +#include "hal/hal_screenshot.h" +#include "hal/hal_settings.h" + +#include + +extern "C" { + +void cp0_signal_audio_api_play_file(const char *path) +{ + if (path && path[0]) { + hal_audio_play(path); + } +} + +void cp0_signal_audio_api_play_asset(const char *name) +{ + const char *path = cp0_file_path_c(name); + cp0_signal_audio_api_play_file(path && path[0] ? path : name); +} + +void cp0_signal_system_play_asset(const char *name) +{ + cp0_signal_audio_api_play_asset(name); +} + +void cp0_config_init(void) +{ + hal_config_init(); +} + +int cp0_config_get_int(const char *key, int default_val) +{ + return hal_config_get_int(key, default_val); +} + +void cp0_config_set_int(const char *key, int val) +{ + hal_config_set_int(key, val); +} + +const char *cp0_config_get_str(const char *key, const char *default_val) +{ + return hal_config_get_str(key, default_val); +} + +void cp0_config_set_str(const char *key, const char *val) +{ + hal_config_set_str(key, val); +} + +void cp0_config_save(void) +{ + hal_config_save(); +} + +int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) +{ + if (!entries || max_entries <= 0) { + return hal_dir_list(path, nullptr, 0, out_count); + } + + hal_dirent_t hal_entries[max_entries]; + int ret = hal_dir_list(path, hal_entries, max_entries, out_count); + int count = out_count ? *out_count : 0; + if (count > max_entries) { + count = max_entries; + } + + for (int i = 0; i < count; ++i) { + std::strncpy(entries[i].name, hal_entries[i].name, sizeof(entries[i].name) - 1); + entries[i].name[sizeof(entries[i].name) - 1] = '\0'; + entries[i].is_dir = hal_entries[i].is_dir; + } + + return ret; +} + +cp0_watcher_t cp0_dir_watch_start(const char *path) +{ + return reinterpret_cast(hal_dir_watch_start(path)); +} + +int cp0_dir_watch_poll(cp0_watcher_t watcher) +{ + return hal_dir_watch_poll(reinterpret_cast(watcher)); +} + +void cp0_dir_watch_stop(cp0_watcher_t watcher) +{ + hal_dir_watch_stop(reinterpret_cast(watcher)); +} + +int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) +{ + if (!entries || max_entries <= 0) { + return hal_network_list(nullptr, 0, out_count); + } + + hal_netif_info_t hal_entries[max_entries]; + int ret = hal_network_list(hal_entries, max_entries, out_count); + int count = out_count ? *out_count : 0; + if (count > max_entries) { + count = max_entries; + } + + for (int i = 0; i < count; ++i) { + std::strncpy(entries[i].iface, hal_entries[i].iface, sizeof(entries[i].iface) - 1); + entries[i].iface[sizeof(entries[i].iface) - 1] = '\0'; + std::strncpy(entries[i].ipv4, hal_entries[i].ipv4, sizeof(entries[i].ipv4) - 1); + entries[i].ipv4[sizeof(entries[i].ipv4) - 1] = '\0'; + std::strncpy(entries[i].netmask, hal_entries[i].netmask, sizeof(entries[i].netmask) - 1); + entries[i].netmask[sizeof(entries[i].netmask) - 1] = '\0'; + entries[i].is_up = hal_entries[i].is_up; + } + + return ret; +} + +int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ + return hal_process_exec_blocking(exec_path, home_key_flag, keep_root); +} + +cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) +{ + return hal_process_spawn(exec_path, keep_root); +} + +void cp0_process_stop(cp0_pid_t pid) +{ + hal_process_stop(pid); +} + +int cp0_process_check_lock(const char *lock_path, int *holder_pid) +{ + return hal_process_check_lock(lock_path, holder_pid); +} + +void cp0_process_kill(int pid, int grace_ms) +{ + hal_process_kill(pid, grace_ms); +} + +void cp0_system_shutdown(void) +{ + hal_system_shutdown(); +} + +void cp0_system_reboot(void) +{ + hal_system_reboot(); +} + +cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows) +{ + return reinterpret_cast(hal_pty_open(cmd, args, cols, rows)); +} + +int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size) +{ + return hal_pty_read(reinterpret_cast(pty), buf, buf_size); +} + +int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len) +{ + return hal_pty_write(reinterpret_cast(pty), buf, len); +} + +int cp0_pty_check_child(cp0_pty_t pty, int *exit_status) +{ + return hal_pty_check_child(reinterpret_cast(pty), exit_status); +} + +void cp0_pty_close(cp0_pty_t pty) +{ + hal_pty_close(reinterpret_cast(pty)); +} + +int cp0_screenshot_save(const char *dir) +{ + return hal_screenshot_save(dir); +} + +cp0_battery_info_t cp0_battery_read(void) +{ + hal_battery_info_t hal = hal_battery_read(); + cp0_battery_info_t info{}; + info.voltage_mv = hal.voltage_mv; + info.current_ma = hal.current_ma; + info.temperature_c10 = hal.temperature_c10; + info.soc = hal.soc; + info.remain_mah = hal.remain_mah; + info.full_mah = hal.full_mah; + info.flags = hal.flags; + info.avg_current_ma = hal.avg_current_ma; + info.valid = hal.valid; + return info; +} + +int cp0_backlight_read(void) +{ + return hal_backlight_read(); +} + +int cp0_backlight_max(void) +{ + return hal_backlight_max(); +} + +int cp0_backlight_write(int val) +{ + return hal_backlight_write(val); +} + +int cp0_volume_read(void) +{ + return hal_volume_read(); +} + +int cp0_volume_write(int val) +{ + return hal_volume_write(val); +} + +cp0_wifi_status_t cp0_wifi_get_status(void) +{ + hal_wifi_status_t hal = hal_wifi_get_status(); + cp0_wifi_status_t st{}; + st.connected = hal.connected; + std::strncpy(st.ssid, hal.ssid, sizeof(st.ssid) - 1); + std::strncpy(st.ip, hal.ip, sizeof(st.ip) - 1); + st.signal = hal.signal; + return st; +} + +int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps) +{ + if (!out || max_aps <= 0) { + return hal_wifi_scan(nullptr, 0); + } + + hal_wifi_ap_t hal_aps[max_aps]; + int count = hal_wifi_scan(hal_aps, max_aps); + if (count > max_aps) { + count = max_aps; + } + + for (int i = 0; i < count; ++i) { + std::strncpy(out[i].ssid, hal_aps[i].ssid, sizeof(out[i].ssid) - 1); + out[i].ssid[sizeof(out[i].ssid) - 1] = '\0'; + out[i].signal = hal_aps[i].signal; + std::strncpy(out[i].security, hal_aps[i].security, sizeof(out[i].security) - 1); + out[i].security[sizeof(out[i].security) - 1] = '\0'; + out[i].in_use = hal_aps[i].in_use; + } + + return count; +} + +int cp0_wifi_connect(const char *ssid, const char *password) +{ + return hal_wifi_connect(ssid, password); +} + +int cp0_wifi_disconnect(void) +{ + return hal_wifi_disconnect(); +} + +cp0_bt_status_t cp0_bt_get_status(void) +{ + hal_bt_status_t hal = hal_bt_get_status(); + cp0_bt_status_t st{}; + st.powered = hal.powered; + std::strncpy(st.address, hal.address, sizeof(st.address) - 1); + return st; +} + +int cp0_bt_set_power(int on) +{ + return hal_bt_set_power(on); +} + +int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) +{ + if (!out || max_devices <= 0) { + return hal_bt_scan(nullptr, 0); + } + + hal_bt_device_t hal_devices[max_devices]; + int count = hal_bt_scan(hal_devices, max_devices); + if (count > max_devices) { + count = max_devices; + } + + for (int i = 0; i < count; ++i) { + std::strncpy(out[i].name, hal_devices[i].name, sizeof(out[i].name) - 1); + out[i].name[sizeof(out[i].name) - 1] = '\0'; + std::strncpy(out[i].address, hal_devices[i].address, sizeof(out[i].address) - 1); + out[i].address[sizeof(out[i].address) - 1] = '\0'; + out[i].rssi = hal_devices[i].rssi; + out[i].connected = hal_devices[i].connected; + } + + return count; +} + +void cp0_time_str(char *buf, int buf_size) +{ + hal_time_str(buf, buf_size); +} + +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp index f518bda6..1d849d92 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp @@ -6,22 +6,158 @@ #include #include #include +#include namespace { -std::string get_app_root_path() +std::string dirname_of(const std::string &path) +{ + size_t slash = path.find_last_of('/'); + return (slash == std::string::npos) ? "." : path.substr(0, slash); +} + +bool path_exists(const std::string &path) +{ + return access(path.c_str(), F_OK) == 0; +} + +std::string get_exe_dir() { char exe_path[PATH_MAX] = {0}; ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); if (len <= 0) { - return "APPLaunch"; + return "."; } exe_path[len] = '\0'; - std::string path(exe_path); - size_t slash = path.find_last_of('/'); - std::string exe_dir = (slash == std::string::npos) ? "." : path.substr(0, slash); + return dirname_of(exe_path); +} + +std::string get_cwd() +{ + char cwd[PATH_MAX] = {0}; + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + return "."; + } + return cwd; +} + +std::string normalize_path(const std::string &path) +{ + if (path.empty()) { + return "."; + } + + const bool absolute = path[0] == '/'; + std::vector parts; + size_t start = 0; + while (start < path.size()) { + size_t end = path.find('/', start); + std::string part = path.substr(start, end == std::string::npos ? std::string::npos : end - start); + if (part.empty() || part == ".") { + // Skip. + } else if (part == "..") { + if (!parts.empty() && parts.back() != "..") { + parts.pop_back(); + } else if (!absolute) { + parts.push_back(part); + } + } else { + parts.push_back(part); + } + if (end == std::string::npos) { + break; + } + start = end + 1; + } + + std::string out = absolute ? "/" : ""; + for (size_t i = 0; i < parts.size(); ++i) { + if (i > 0) { + out += "/"; + } + out += parts[i]; + } + return out.empty() ? (absolute ? "/" : ".") : out; +} + +std::string make_absolute_path(const std::string &path) +{ + if (!path.empty() && path[0] == '/') { + return normalize_path(path); + } + return normalize_path(get_cwd() + "/" + path); +} + +std::vector split_path(const std::string &path) +{ + std::vector parts; + size_t start = (path.size() > 0 && path[0] == '/') ? 1 : 0; + while (start < path.size()) { + size_t end = path.find('/', start); + std::string part = path.substr(start, end == std::string::npos ? std::string::npos : end - start); + if (!part.empty()) { + parts.push_back(part); + } + if (end == std::string::npos) { + break; + } + start = end + 1; + } + return parts; +} + +std::string make_relative_to_cwd(const std::string &path) +{ + const std::string abs_path = make_absolute_path(path); + const std::string abs_cwd = make_absolute_path(get_cwd()); + if (abs_path == abs_cwd) { + return "."; + } - return exe_dir + "/APPLaunch"; + const auto path_parts = split_path(abs_path); + const auto cwd_parts = split_path(abs_cwd); + + size_t common = 0; + while (common < path_parts.size() && common < cwd_parts.size() && path_parts[common] == cwd_parts[common]) { + ++common; + } + + std::string rel; + for (size_t i = common; i < cwd_parts.size(); ++i) { + if (!rel.empty()) { + rel += "/"; + } + rel += ".."; + } + for (size_t i = common; i < path_parts.size(); ++i) { + if (!rel.empty()) { + rel += "/"; + } + rel += path_parts[i]; + } + + return rel.empty() ? "." : rel; +} + +std::string get_app_root_path() +{ + const std::string exe_dir = get_exe_dir(); + const std::string cwd = get_cwd(); + const std::string exe_parent = dirname_of(exe_dir); + + std::vector candidates = { + exe_dir + "/APPLaunch", + cwd + "/APPLaunch", + exe_parent + "/APPLaunch", + }; + + for (const auto &candidate : candidates) { + if (path_exists(candidate + "/share")) { + return make_absolute_path(candidate); + } + } + + return make_absolute_path(exe_dir + "/APPLaunch"); } std::string lower_ext(const std::string &file) @@ -52,6 +188,33 @@ bool is_font_ext(const std::string &ext) { return ext == "ttf" || ext == "otf"; } + +bool is_absolute_path(const std::string &file) +{ + return !file.empty() && file[0] == '/'; +} + +bool starts_with(const std::string &value, const char *prefix) +{ + const std::string p(prefix); + return value.compare(0, p.size(), p) == 0; +} + +std::string app_relative_path(const std::string &root_path, const std::string &file, const char *dir) +{ + std::string absolute_path; + if (starts_with(file, "APPLaunch/")) { + absolute_path = dirname_of(root_path) + "/" + file; + } else if (is_absolute_path(file)) { + absolute_path = file; + } else if (starts_with(file, dir)) { + absolute_path = root_path + "/" + file; + } else { + absolute_path = root_path + "/" + dir + file; + } + + return make_relative_to_cwd(absolute_path); +} } // namespace std::string cp0_file_path(std::string file) @@ -79,13 +242,13 @@ std::string cp0_file_path(std::string file) const std::string ext = lower_ext(file); if (is_image_ext(ext)) { - return "share/images/" + file; + return app_relative_path(root_path, file, "share/images/"); } if (is_audio_ext(ext)) { - return root_path + "/share/audio/" + file; + return app_relative_path(root_path, file, "share/audio/"); } if (is_font_ext(ext)) { - return root_path + "/share/font/" + file; + return app_relative_path(root_path, file, "share/font/"); } return file; diff --git a/projects/APPLaunch/main/ui/Animation/Animation.hpp b/projects/APPLaunch/main/ui/Animation/Animation.hpp new file mode 100644 index 00000000..7a6ebeaa --- /dev/null +++ b/projects/APPLaunch/main/ui/Animation/Animation.hpp @@ -0,0 +1,181 @@ +#pragma once + +#include "lvgl.h" + +#include + +class LvglAnimation { +public: + typedef std::function callback_t; + typedef std::function raw_callback_t; + typedef std::function finished_callback_t; + + enum Type { + linear, + ease_in, + ease_out, + ease_in_out, + overshoot, + bounce, + }; + + static void start(void *obj, + int time, + int start_val, + int end_val, + callback_t callback, + finished_callback_t finished = nullptr, + Type type = overshoot) + { + LvglAnimation *self = new LvglAnimation(time, type); + self->callback_ = std::move(callback); + self->finished_callback_ = std::move(finished); + self->launch(obj, obj, start_val, end_val, false); + } + + static void start_raw(int time, + raw_callback_t callback, + finished_callback_t finished = nullptr, + Type type = overshoot) + { + LvglAnimation *self = new LvglAnimation(time, type); + self->raw_callback_ = std::move(callback); + self->finished_callback_ = std::move(finished); + self->launch(nullptr, self, 0, LV_BEZIER_VAL_MAX, true); + } + + int32_t Animation_map(int32_t start, int32_t end) + { + lv_anim_t tmp; + lv_memzero(&tmp, sizeof(tmp)); + tmp.act_time = current_progress_; + tmp.duration = LV_BEZIER_VAL_MAX; + tmp.start_value = 0; + tmp.end_value = LV_BEZIER_VAL_MAX; + + int32_t curved = path_cb_(&tmp); + return start + (int32_t)(((int64_t)(end - start) * curved) >> LV_BEZIER_VAL_SHIFT); + } + + int32_t progress() const + { + return current_progress_; + } + +private: + callback_t callback_; + raw_callback_t raw_callback_; + finished_callback_t finished_callback_; + int time_; + Type type_; + lv_anim_path_cb_t path_cb_; + int32_t current_progress_ = 0; + bool raw_ = false; + + LvglAnimation(int time, Type type) + : time_(time), + type_(type), + path_cb_(get_path_cb(type)) + { + } + + void launch(void *obj, void *var, int32_t start_val, int32_t end_val, bool raw) + { + (void)obj; + raw_ = raw; + + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_var(&anim, var); + lv_anim_set_duration(&anim, time_); + lv_anim_set_values(&anim, start_val, end_val); + lv_anim_set_user_data(&anim, this); + lv_anim_set_deleted_cb(&anim, LvglAnimation::deleted_cb); + + if (raw_) { + lv_anim_set_custom_exec_cb(&anim, LvglAnimation::exec_cb_raw); + lv_anim_set_path_cb(&anim, lv_anim_path_linear); + } else { + lv_anim_set_custom_exec_cb(&anim, LvglAnimation::exec_cb); + lv_anim_set_path_cb(&anim, path_cb_); + } + + lv_anim_start(&anim); + } + + static LvglAnimation *from_anim(lv_anim_t *anim) + { + return static_cast(lv_anim_get_user_data(anim)); + } + + static int32_t path_overshoot(const lv_anim_t *anim) + { + int32_t t; + if (anim->act_time >= anim->duration) { + t = LV_BEZIER_VAL_MAX; + } else { + t = lv_map(anim->act_time, 0, anim->duration, 0, LV_BEZIER_VAL_MAX); + } + + int32_t step = lv_bezier3(t, 0, 600, 1300, LV_BEZIER_VAL_MAX); + int64_t value = (int64_t)step * (anim->end_value - anim->start_value); + value >>= LV_BEZIER_VAL_SHIFT; + value += anim->start_value; + return (int32_t)value; + } + + static lv_anim_path_cb_t get_path_cb(Type type) + { + switch (type) { + case linear: + return lv_anim_path_linear; + case ease_in: + return lv_anim_path_ease_in; + case ease_out: + return lv_anim_path_ease_out; + case ease_in_out: + return lv_anim_path_ease_in_out; + case overshoot: + return LvglAnimation::path_overshoot; + case bounce: + return lv_anim_path_bounce; + default: + return LvglAnimation::path_overshoot; + } + } + + static void exec_cb(lv_anim_t *anim, int32_t value) + { + LvglAnimation *self = from_anim(anim); + if (self && self->callback_) { + self->callback_(anim->var, value); + } + } + + static void exec_cb_raw(lv_anim_t *anim, int32_t value) + { + LvglAnimation *self = from_anim(anim); + if (!self) { + return; + } + + self->current_progress_ = value; + if (self->raw_callback_) { + self->raw_callback_(self); + } + } + + static void deleted_cb(lv_anim_t *anim) + { + LvglAnimation *self = from_anim(anim); + if (!self) { + return; + } + + if (self->finished_callback_) { + self->finished_callback_(self); + } + + delete self; + } +}; diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp new file mode 100644 index 00000000..f83f4bfa --- /dev/null +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp @@ -0,0 +1,148 @@ +#include "ui_launcher_animation.h" + +#include "Animation.hpp" + +extern "C" int Animation_time; + +namespace { + +struct LauncherSlot { + lv_coord_t x; + lv_coord_t y; + lv_coord_t w; + lv_coord_t h; +}; + +struct LauncherHomeAnimContext { + lv_obj_t *items[10]; + bool to_right; + launcher_anim_ready_cb_t ready_cb; +}; + +constexpr LauncherSlot kPanelSlots[] = { + {-177, 4, 61, 61}, + {-99, -6, 80, 80}, + {0, -16, 100, 100}, + {99, -6, 80, 80}, + {177, 4, 61, 61}, +}; + +constexpr LauncherSlot kLabelSlots[] = { + {-177, 50, 0, 0}, + {-99, 50, 0, 0}, + {0, 50, 0, 0}, + {99, 50, 0, 0}, + {177, 50, 0, 0}, +}; + +void apply_panel_slot(lv_obj_t *obj, const LauncherSlot &slot) +{ + if (!obj) { + return; + } + + lv_obj_set_x(obj, slot.x); + lv_obj_set_y(obj, slot.y); + lv_obj_set_width(obj, slot.w); + lv_obj_set_height(obj, slot.h); +} + +void apply_label_slot(lv_obj_t *obj, const LauncherSlot &slot) +{ + if (!obj) { + return; + } + + lv_obj_set_x(obj, slot.x); + lv_obj_set_y(obj, slot.y); +} + +void animate_panel(lv_obj_t *obj, const LauncherSlot &from, const LauncherSlot &to, LvglAnimation *anim) +{ + if (!obj) { + return; + } + + lv_obj_set_x(obj, anim->Animation_map(from.x, to.x)); + lv_obj_set_y(obj, anim->Animation_map(from.y, to.y)); + lv_obj_set_width(obj, anim->Animation_map(from.w, to.w)); + lv_obj_set_height(obj, anim->Animation_map(from.h, to.h)); +} + +void animate_label(lv_obj_t *obj, const LauncherSlot &from, const LauncherSlot &to, LvglAnimation *anim) +{ + if (!obj) { + return; + } + + lv_obj_set_x(obj, anim->Animation_map(from.x, to.x)); + lv_obj_set_y(obj, anim->Animation_map(from.y, to.y)); +} + +void animate_home(LauncherHomeAnimContext *ctx, LvglAnimation *anim) +{ + if (ctx->to_right) { + for (int i = 0; i < 4; ++i) { + animate_panel(ctx->items[i], kPanelSlots[i], kPanelSlots[i + 1], anim); + animate_label(ctx->items[i + 5], kLabelSlots[i], kLabelSlots[i + 1], anim); + } + } else { + for (int i = 1; i < 5; ++i) { + animate_panel(ctx->items[i], kPanelSlots[i], kPanelSlots[i - 1], anim); + animate_label(ctx->items[i + 5], kLabelSlots[i], kLabelSlots[i - 1], anim); + } + } +} + +void finish_home(LauncherHomeAnimContext *ctx) +{ + if (ctx->to_right) { + for (int i = 0; i < 4; ++i) { + apply_panel_slot(ctx->items[i], kPanelSlots[i + 1]); + apply_label_slot(ctx->items[i + 5], kLabelSlots[i + 1]); + } + } else { + for (int i = 1; i < 5; ++i) { + apply_panel_slot(ctx->items[i], kPanelSlots[i - 1]); + apply_label_slot(ctx->items[i + 5], kLabelSlots[i - 1]); + } + } + + if (ctx->ready_cb) { + ctx->ready_cb(nullptr); + } + + delete ctx; +} + +void launcher_home_animate(lv_obj_t **items, bool to_right, launcher_anim_ready_cb_t ready_cb) +{ + auto *ctx = new LauncherHomeAnimContext{}; + ctx->to_right = to_right; + ctx->ready_cb = ready_cb; + + for (int i = 0; i < 10; ++i) { + ctx->items[i] = items[i]; + } + + LvglAnimation::start_raw( + Animation_time, + [ctx](LvglAnimation *anim) { + animate_home(ctx, anim); + }, + [ctx](LvglAnimation *) { + finish_home(ctx); + }); +} + +} // namespace + +extern "C" void launcher_home_animate_right(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb) +{ + launcher_home_animate(items, true, ready_cb); +} + +extern "C" void launcher_home_animate_left(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb) +{ + launcher_home_animate(items, false, ready_cb); +} diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h new file mode 100644 index 00000000..03674580 --- /dev/null +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h @@ -0,0 +1,16 @@ +#pragma once + +#include "lvgl/lvgl.h" + +typedef void (*launcher_anim_ready_cb_t)(lv_anim_t *); + +#ifdef __cplusplus +extern "C" { +#endif + +void launcher_home_animate_right(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb); +void launcher_home_animate_left(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb); + +#ifdef __cplusplus +} +#endif diff --git a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp index c1c674ca..56dea9b5 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp +++ b/projects/APPLaunch/main/ui/components/ui_app_launch.cpp @@ -29,6 +29,15 @@ static void panel_set_icon(lv_obj_t *panel, const char *src) { + const char *icon_src = src ? src : ""; + if (icon_src[0] == '\0') { + SLOGW("[LAUNCHER] set panel icon with empty path"); + } else if (access(icon_src, R_OK) == 0) { + SLOGI("[LAUNCHER] set panel icon: %s", icon_src); + } else { + SLOGW("[LAUNCHER] set panel icon missing/unreadable: %s", icon_src); + } + lv_obj_set_style_pad_all(panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_t *img = lv_obj_get_child(panel, 0); @@ -38,7 +47,7 @@ static void panel_set_icon(lv_obj_t *panel, const char *src) lv_obj_set_align(img, LV_ALIGN_CENTER); lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); } - lv_image_set_src(img, src); + lv_image_set_src(img, icon_src); } // ============================================================ diff --git a/projects/APPLaunch/main/ui/ui_events.c b/projects/APPLaunch/main/ui/ui_events.c index 056e4a82..14d76611 100644 --- a/projects/APPLaunch/main/ui/ui_events.c +++ b/projects/APPLaunch/main/ui/ui_events.c @@ -6,6 +6,7 @@ #include #include "compat/input_keys.h" +#include "Animation/ui_launcher_animation.h" typedef void (*switch_cb_t)(lv_event_t *); @@ -242,20 +243,12 @@ void switch_right(lv_event_t *e) lv_obj_clear_flag(launch_circle[0], LV_OBJ_FLAG_HIDDEN); - leftOuterPanelToLeft_Animation(launch_circle[0], 0, NULL); - leftPanelToCenter_Animation(launch_circle[1], 0, NULL); - centerPanelToRight_Animation(launch_circle[2], 0, NULL); - rightPanelToRightOuter_Animation(launch_circle[3], 0, snap_all_panels); + launcher_home_animate_right(launch_circle, snap_all_panels); snap_panel_to_slot(launch_circle[4], 0); lv_obj_clear_flag(launch_circle[5], LV_OBJ_FLAG_HIDDEN); - leftOuterLabelToLeft_Animation(launch_circle[5], 0, NULL); - leftLabelToCenter_Animation(launch_circle[6], 0, NULL); - centerLabelToRight_Animation(launch_circle[7], 0, NULL); - rightLabelToRightOuter_Animation(launch_circle[8], 0, NULL); - snap_label_to_slot(launch_circle[9], 5); cpp_app_right(launch_circle[4], launch_circle[9]); @@ -290,20 +283,12 @@ void switch_left(lv_event_t *e) lv_obj_clear_flag(launch_circle[4], LV_OBJ_FLAG_HIDDEN); - rightOuterPanelToRight_Animation(launch_circle[4], 0, NULL); - rightPanelToCenter_Animation(launch_circle[3], 0, NULL); - centerPanelToLeft_Animation(launch_circle[2], 0, NULL); - leftPanelToLeftOuter_Animation(launch_circle[1], 0, snap_all_panels); + launcher_home_animate_left(launch_circle, snap_all_panels); snap_panel_to_slot(launch_circle[0], 4); lv_obj_clear_flag(launch_circle[9], LV_OBJ_FLAG_HIDDEN); - rightOuterLabelToRight_Animation(launch_circle[9], 0, NULL); - rightLabelToCenter_Animation(launch_circle[8], 0, NULL); - centerLabelToLeft_Animation(launch_circle[7], 0, NULL); - leftLabelToLeftOuter_Animation(launch_circle[6], 0, NULL); - snap_label_to_slot(launch_circle[5], 9); cpp_app_left(launch_circle[0], launch_circle[5]); From 668ed08d2b9337566d06f9418784197641718f39 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 13:42:07 +0800 Subject: [PATCH 22/70] refactor(APPLaunch): consolidate launch page and home UI --- .../main/ui/Animation/Animation_lable.h | 353 --------- .../main/ui/Animation/Animation_panel.h | 649 ---------------- .../ui/Animation/ui_launcher_animation.cpp | 20 +- .../main/ui/Animation/ui_launcher_animation.h | 16 +- .../ui_app_launch.cpp => Launch.cpp} | 108 ++- projects/APPLaunch/main/ui/Launch.h | 24 + .../ui_Screen1.c => UILaunchPage.cpp} | 725 ++++++++++++++++-- projects/APPLaunch/main/ui/UILaunchPage.h | 26 + .../APPLaunch/main/ui/components/ui_comp.c | 1 - projects/APPLaunch/main/ui/ui.c | 184 ----- projects/APPLaunch/main/ui/ui.cpp | 22 + projects/APPLaunch/main/ui/ui.h | 158 ++-- projects/APPLaunch/main/ui/ui_event_fun.h | 25 - projects/APPLaunch/main/ui/ui_events.c | 439 ----------- projects/APPLaunch/main/ui/ui_events.h | 8 +- projects/APPLaunch/main/ui/ui_events_weak.c | 66 -- projects/APPLaunch/main/ui/ui_helpers.c | 277 ------- projects/APPLaunch/main/ui/ui_helpers.h | 141 ---- projects/APPLaunch/main/ui/ui_obj.h | 26 +- projects/APPLaunch/main/ui/zero_lvgl_os.cpp | 23 + projects/APPLaunch/main/ui/zero_lvgl_os.h | 22 + 21 files changed, 956 insertions(+), 2357 deletions(-) delete mode 100644 projects/APPLaunch/main/ui/Animation/Animation_lable.h delete mode 100644 projects/APPLaunch/main/ui/Animation/Animation_panel.h rename projects/APPLaunch/main/ui/{components/ui_app_launch.cpp => Launch.cpp} (93%) create mode 100644 projects/APPLaunch/main/ui/Launch.h rename projects/APPLaunch/main/ui/{screens/ui_Screen1.c => UILaunchPage.cpp} (51%) create mode 100644 projects/APPLaunch/main/ui/UILaunchPage.h create mode 100644 projects/APPLaunch/main/ui/ui.cpp delete mode 100644 projects/APPLaunch/main/ui/ui_event_fun.h delete mode 100644 projects/APPLaunch/main/ui/ui_events.c delete mode 100644 projects/APPLaunch/main/ui/ui_events_weak.c delete mode 100644 projects/APPLaunch/main/ui/ui_helpers.c delete mode 100644 projects/APPLaunch/main/ui/ui_helpers.h create mode 100644 projects/APPLaunch/main/ui/zero_lvgl_os.cpp create mode 100644 projects/APPLaunch/main/ui/zero_lvgl_os.h diff --git a/projects/APPLaunch/main/ui/Animation/Animation_lable.h b/projects/APPLaunch/main/ui/Animation/Animation_lable.h deleted file mode 100644 index ae727897..00000000 --- a/projects/APPLaunch/main/ui/Animation/Animation_lable.h +++ /dev/null @@ -1,353 +0,0 @@ -// leftOuterLabelToLeft: pos5(-177,57) → pos6(-99,57) delta x=+78, y=0 -void leftOuterLabelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// leftLabelToCenter: pos6(-99,57) → pos7(0,50) delta x=+99, y=-7 -void leftLabelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// centerLabelToRight: pos7(0,50) → pos8(99,57) delta x=+99, y=+7 -void centerLabelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// rightLabelToRightOuter: pos8(99,57) → pos9(177,57) delta x=+78, y=0 -void rightLabelToRightOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// leftward direction: labels slide left (pos9→8→7→6→5) - -// rightOuterLabelToRight: pos9(177,57) → pos8(99,57) delta x=-78, y=0 -void rightOuterLabelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// rightLabelToCenter: pos8(99,57) → pos7(0,50) delta x=-99, y=-7 -void rightLabelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// centerLabelToLeft: pos7(0,50) → pos6(-99,57) delta x=-99, y=+7 -void centerLabelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} - -// leftLabelToLeftOuter: pos6(-99,57) → pos5(-177,57) delta x=-78, y=0 -void leftLabelToLeftOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - PropertyAnimation_1_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 0); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); -} diff --git a/projects/APPLaunch/main/ui/Animation/Animation_panel.h b/projects/APPLaunch/main/ui/Animation/Animation_panel.h deleted file mode 100644 index 5f0579b9..00000000 --- a/projects/APPLaunch/main/ui/Animation/Animation_panel.h +++ /dev/null @@ -1,649 +0,0 @@ -void leftPanelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, -10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void centerPanelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void rightPanelToRightOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void leftOuterPanelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, 78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, -10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void leftPanelToLeftOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void centerPanelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, 10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, -20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void rightPanelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -99); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, -10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} -void rightOuterPanelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb) -{ - ui_anim_user_data_t * PropertyAnimation_0_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_0_user_data->target = TargetObject; - PropertyAnimation_0_user_data->val = -1; - lv_anim_t PropertyAnimation_0; - lv_anim_init(&PropertyAnimation_0); - lv_anim_set_time(&PropertyAnimation_0, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_0, PropertyAnimation_0_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_0, _ui_anim_callback_set_x); - lv_anim_set_values(&PropertyAnimation_0, 0, -78); - lv_anim_set_path_cb(&PropertyAnimation_0, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_0, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_0, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_0, 0); - lv_anim_set_playback_delay(&PropertyAnimation_0, 0); - lv_anim_set_repeat_count(&PropertyAnimation_0, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_0, 0); - lv_anim_set_early_apply(&PropertyAnimation_0, false); - lv_anim_set_get_value_cb(&PropertyAnimation_0, &_ui_anim_callback_get_x); - lv_anim_start(&PropertyAnimation_0); - ui_anim_user_data_t * PropertyAnimation_1_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_1_user_data->target = TargetObject; - PropertyAnimation_1_user_data->val = -1; - lv_anim_t PropertyAnimation_1; - lv_anim_init(&PropertyAnimation_1); - lv_anim_set_time(&PropertyAnimation_1, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_1, PropertyAnimation_1_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_1, _ui_anim_callback_set_y); - lv_anim_set_values(&PropertyAnimation_1, 0, -10); - lv_anim_set_path_cb(&PropertyAnimation_1, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_1, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_1, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_1, 0); - lv_anim_set_playback_delay(&PropertyAnimation_1, 0); - lv_anim_set_repeat_count(&PropertyAnimation_1, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_1, 0); - lv_anim_set_early_apply(&PropertyAnimation_1, false); - lv_anim_set_get_value_cb(&PropertyAnimation_1, &_ui_anim_callback_get_y); - lv_anim_start(&PropertyAnimation_1); - ui_anim_user_data_t * PropertyAnimation_2_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_2_user_data->target = TargetObject; - PropertyAnimation_2_user_data->val = -1; - lv_anim_t PropertyAnimation_2; - lv_anim_init(&PropertyAnimation_2); - lv_anim_set_time(&PropertyAnimation_2, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_2, PropertyAnimation_2_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_2, _ui_anim_callback_set_width); - lv_anim_set_values(&PropertyAnimation_2, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_2, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_2, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_2, _ui_anim_callback_free_user_data); - lv_anim_set_playback_time(&PropertyAnimation_2, 0); - lv_anim_set_playback_delay(&PropertyAnimation_2, 0); - lv_anim_set_repeat_count(&PropertyAnimation_2, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_2, 0); - lv_anim_set_early_apply(&PropertyAnimation_2, false); - lv_anim_set_get_value_cb(&PropertyAnimation_2, &_ui_anim_callback_get_width); - lv_anim_start(&PropertyAnimation_2); - ui_anim_user_data_t * PropertyAnimation_3_user_data = lv_mem_alloc(sizeof(ui_anim_user_data_t)); - PropertyAnimation_3_user_data->target = TargetObject; - PropertyAnimation_3_user_data->val = -1; - PropertyAnimation_3_user_data->ready_cb = ready_cb; - lv_anim_t PropertyAnimation_3; - lv_anim_init(&PropertyAnimation_3); - lv_anim_set_time(&PropertyAnimation_3, Animation_time); - lv_anim_set_user_data(&PropertyAnimation_3, PropertyAnimation_3_user_data); - lv_anim_set_custom_exec_cb(&PropertyAnimation_3, _ui_anim_callback_set_height); - lv_anim_set_values(&PropertyAnimation_3, 0, 20); - lv_anim_set_path_cb(&PropertyAnimation_3, lv_anim_path_overshoot); - lv_anim_set_delay(&PropertyAnimation_3, delay + 0); - lv_anim_set_deleted_cb(&PropertyAnimation_3, _ui_anim_callback_free_user_data_and_ready_cb); - lv_anim_set_playback_time(&PropertyAnimation_3, 0); - lv_anim_set_playback_delay(&PropertyAnimation_3, 0); - lv_anim_set_repeat_count(&PropertyAnimation_3, 0); - lv_anim_set_repeat_delay(&PropertyAnimation_3, 0); - lv_anim_set_early_apply(&PropertyAnimation_3, false); - lv_anim_set_get_value_cb(&PropertyAnimation_3, &_ui_anim_callback_get_height); - lv_anim_start(&PropertyAnimation_3); - -} - diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp index f83f4bfa..d7d64f76 100644 --- a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp @@ -2,6 +2,8 @@ #include "Animation.hpp" +#include + extern "C" int Animation_time; namespace { @@ -16,7 +18,7 @@ struct LauncherSlot { struct LauncherHomeAnimContext { lv_obj_t *items[10]; bool to_right; - launcher_anim_ready_cb_t ready_cb; + launcher_home_animation::ReadyCallback ready_cb; }; constexpr LauncherSlot kPanelSlots[] = { @@ -109,13 +111,13 @@ void finish_home(LauncherHomeAnimContext *ctx) } if (ctx->ready_cb) { - ctx->ready_cb(nullptr); + ctx->ready_cb(); } delete ctx; } -void launcher_home_animate(lv_obj_t **items, bool to_right, launcher_anim_ready_cb_t ready_cb) +void launcher_home_animate(lv_obj_t **items, bool to_right, launcher_home_animation::ReadyCallback ready_cb) { auto *ctx = new LauncherHomeAnimContext{}; ctx->to_right = to_right; @@ -137,12 +139,16 @@ void launcher_home_animate(lv_obj_t **items, bool to_right, launcher_anim_ready_ } // namespace -extern "C" void launcher_home_animate_right(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb) +namespace launcher_home_animation { + +void animate_right(lv_obj_t **items, ReadyCallback ready_cb) { - launcher_home_animate(items, true, ready_cb); + launcher_home_animate(items, true, std::move(ready_cb)); } -extern "C" void launcher_home_animate_left(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb) +void animate_left(lv_obj_t **items, ReadyCallback ready_cb) { - launcher_home_animate(items, false, ready_cb); + launcher_home_animate(items, false, std::move(ready_cb)); } + +} // namespace launcher_home_animation diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h index 03674580..7c69d827 100644 --- a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h @@ -2,15 +2,13 @@ #include "lvgl/lvgl.h" -typedef void (*launcher_anim_ready_cb_t)(lv_anim_t *); +#include -#ifdef __cplusplus -extern "C" { -#endif +namespace launcher_home_animation { -void launcher_home_animate_right(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb); -void launcher_home_animate_left(lv_obj_t **items, launcher_anim_ready_cb_t ready_cb); +using ReadyCallback = std::function; -#ifdef __cplusplus -} -#endif +void animate_right(lv_obj_t **items, ReadyCallback ready_cb); +void animate_left(lv_obj_t **items, ReadyCallback ready_cb); + +} // namespace launcher_home_animation diff --git a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp similarity index 93% rename from projects/APPLaunch/main/ui/components/ui_app_launch.cpp rename to projects/APPLaunch/main/ui/Launch.cpp index c4f46961..9376cab2 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -1,22 +1,27 @@ -#include "../ui.h" -#include -#include -#include -#include -#include +#include "Launch.h" + +#include "ui.h" +#include "UILaunchPage.h" +#include "ui_loading.h" +#include "components/page_app.h" #include "cp0_lvgl_app.h" #include "cp0_lvgl_file.hpp" -#include -#include -#include -#include -#include +#include "sample_log.h" + #include +#include +#include #include +#include +#include +#include #include -#include "../ui_loading.h" -#include "page_app.h" -#include "sample_log.h" +#include +#include +#include +#include +#include +#include /* img_path() now defined in ui_app_page.hpp */ @@ -63,7 +68,7 @@ Icon=share/images/e-Mail_80.png */ // Forward declarations -class app_launch_S; +class LaunchImpl; // ============================================================ // Type tag @@ -86,7 +91,7 @@ struct app std::string Icon; std::string Exec; - std::function launch; + std::function launch; // ① External command app(std::string name, @@ -117,9 +122,9 @@ struct app }; // ============================================================ -// app_launch_S +// LaunchImpl // ============================================================ -class app_launch_S +class LaunchImpl { private: int current_app = 2; @@ -131,9 +136,9 @@ class app_launch_S public: std::list app_list; std::shared_ptr app_Page; - + std::shared_ptr home_Page; public: - app_launch_S() + LaunchImpl() { // Fixed icon; users cannot modify it app_list.emplace_back("Python", @@ -244,7 +249,7 @@ class app_launch_S static void lv_go_back_home(void *arg) { - auto self = (app_launch_S *)arg; + auto self = (LaunchImpl *)arg; SLOGI("[HOME] lv_go_back_home executing (page=%p)", self->app_Page.get()); lv_timer_enable(true); lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); @@ -273,7 +278,7 @@ class app_launch_S app_Page = p; lv_disp_load_scr(p->get_ui()); lv_indev_set_group(lv_indev_get_next(NULL), p->get_key_group()); - p->go_back_home = std::bind(&app_launch_S::go_back_home, this); + p->go_back_home = std::bind(&LaunchImpl::go_back_home, this); p->terminal_sysplause = sysplause; /* Console page fully covers APP_Container; safe to hide now. * The heavy exec() call below will still run while the terminal @@ -535,7 +540,7 @@ class app_launch_S // ============================================================ static void home_status_timer_cb(lv_timer_t *timer) { - auto *self = static_cast(lv_timer_get_user_data(timer)); + auto *self = static_cast(lv_timer_get_user_data(timer)); if (self) self->update_home_status_bar(); } @@ -590,7 +595,7 @@ class app_launch_S // ============================================================ static void app_dir_watch_cb(lv_timer_t *timer) { - auto *self = static_cast(lv_timer_get_user_data(timer)); + auto *self = static_cast(lv_timer_get_user_data(timer)); if (!self || !self->dir_watcher) return; @@ -601,18 +606,18 @@ class app_launch_S } } - ~app_launch_S(); + ~LaunchImpl(); }; // ============================================================ -// app constructor implementation (placed after app_launch_S definition) +// app constructor implementation (placed after LaunchImpl definition) // ============================================================ inline app::app(std::string name, std::string icon, std::string exec, bool terminal) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [exec = std::move(exec), terminal](app_launch_S *ctx) + launch = [exec = std::move(exec), terminal](LaunchImpl *ctx) { if (terminal) ctx->launch_Exec_in_terminal(exec); @@ -627,7 +632,7 @@ inline app::app(std::string name, bool terminal, bool sysplause) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [exec = std::move(exec), terminal, sysplause](app_launch_S *ctx) + launch = [exec = std::move(exec), terminal, sysplause](LaunchImpl *ctx) { if (terminal) ctx->launch_Exec_in_terminal(exec, sysplause); @@ -643,7 +648,7 @@ inline app::app(std::string name, bool sysplause, bool run_as_root) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [exec = std::move(exec), terminal, sysplause, run_as_root](app_launch_S *ctx) + launch = [exec = std::move(exec), terminal, sysplause, run_as_root](LaunchImpl *ctx) { if (terminal) ctx->launch_Exec_in_terminal(exec, sysplause); @@ -657,7 +662,7 @@ app::app(std::string name, std::string icon, page_t /*tag*/) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [](app_launch_S *self) + launch = [](LaunchImpl *self) { /* Instant feedback: show the overlay, then force an immediate * redraw so it actually paints BEFORE the (sometimes slow) page @@ -672,7 +677,7 @@ app::app(std::string name, lv_indev_set_group(lv_indev_get_next(NULL), p->get_key_group()); p->go_back_home = - std::bind(&app_launch_S::go_back_home, self); + std::bind(&LaunchImpl::go_back_home, self); /* Page is now attached and drawable; hide the overlay. The * next LVGL frame will paint the new page without it. */ ui_loading_hide(); @@ -680,9 +685,9 @@ app::app(std::string name, } // ============================================================ -// app_launch_S destructor implementation +// LaunchImpl destructor implementation // ============================================================ -app_launch_S::~app_launch_S() +LaunchImpl::~LaunchImpl() { if (status_timer) { @@ -701,15 +706,48 @@ app_launch_S::~app_launch_S() } } +Launch::Launch() = default; + +Launch::~Launch() = default; + +void Launch::set_launch_page(std::shared_ptr launch_page) +{ + launch_page_ = std::move(launch_page); +} + +void Launch::bind_ui() +{ + impl_ = std::make_unique(); +} + +void Launch::update_left_slot(lv_obj_t *panel, lv_obj_t *label) +{ + if (impl_) + impl_->update_left_slot(panel, label); +} + +void Launch::update_right_slot(lv_obj_t *panel, lv_obj_t *label) +{ + if (impl_) + impl_->update_right_slot(panel, label); +} + +void Launch::launch_app() +{ + if (impl_) + impl_->launch_app(); +} + // ============================================================ -std::unique_ptr app_launch_Ser; +std::unique_ptr app_launch_Ser; extern "C" { void ui_info_bind() { - app_launch_Ser = std::make_unique(); + app_launch_Ser = std::make_unique(); + app_launch_Ser->bind_ui(); } void cpp_app_left(lv_obj_t *panel, lv_obj_t *label) { diff --git a/projects/APPLaunch/main/ui/Launch.h b/projects/APPLaunch/main/ui/Launch.h new file mode 100644 index 00000000..f3429bc0 --- /dev/null +++ b/projects/APPLaunch/main/ui/Launch.h @@ -0,0 +1,24 @@ +#pragma once + +#include "lvgl/lvgl.h" +#include + +class LaunchImpl; +class UILaunchPage; + +class Launch +{ +public: + Launch(); + ~Launch(); + + void bind_ui(); + void set_launch_page(std::shared_ptr launch_page); + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_app(); + +private: + std::unique_ptr impl_; + std::shared_ptr launch_page_; +}; diff --git a/projects/APPLaunch/main/ui/screens/ui_Screen1.c b/projects/APPLaunch/main/ui/UILaunchPage.cpp similarity index 51% rename from projects/APPLaunch/main/ui/screens/ui_Screen1.c rename to projects/APPLaunch/main/ui/UILaunchPage.cpp index f86dd913..b6bca039 100644 --- a/projects/APPLaunch/main/ui/screens/ui_Screen1.c +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -1,19 +1,639 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero +#include "UILaunchPage.h" -#include "../ui.h" +#include "Launch.h" +#include "lvgl/src/widgets/gif/lv_gif.h" +#include "sample_log.h" +#include "compat/input_keys.h" +#include -void ui_Screen1_screen_init(void) +#include "Animation/ui_launcher_animation.h" + +#include + +extern "C" { + +typedef void (*switch_cb_t)(lv_event_t *); + + +#define ROTATE_LEFT(arr, start, end) \ + do \ + { \ + __typeof__((arr)[0]) _tmp = (arr)[(start)]; \ + memmove(&(arr)[(start)], &(arr)[(start) + 1], \ + ((end) - (start)) * sizeof((arr)[0])); \ + (arr)[(end)] = _tmp; \ + } while (0) + +#define ROTATE_RIGHT(arr, start, end) \ + do \ + { \ + __typeof__((arr)[0]) _tmp = (arr)[(end)]; \ + memmove(&(arr)[(start) + 1], &(arr)[(start)], \ + ((end) - (start)) * sizeof((arr)[0])); \ + (arr)[(start)] = _tmp; \ + } while (0) + +lv_obj_t *launch_circle[100]; + +// ==================== standard coordinates for 5 slots ==================== + +static const lv_coord_t SLOT_X[] = {-177, -99, 0, 99, 177, -177, -99, 0, 99, 177}; +static const lv_coord_t SLOT_Y[] = {4, -6, -16, -6, 4, LABEL_Y_SIDE, LABEL_Y_SIDE, LABEL_Y_CENTER, LABEL_Y_SIDE, LABEL_Y_SIDE}; +static const lv_coord_t SLOT_W[] = {61, 80, 100, 80, 61}; +static const lv_coord_t SLOT_H[] = {61, 80, 100, 80, 61}; + +static bool is_animating = false; +static switch_cb_t pending_switch = NULL; + +static int Panel_current_pos = 2; +static int switch_current_pos = 11; + + +// ============================================================ +// audio +// ============================================================ + +static void audio_play_ui_asset(const char *name) +{ + cp0_signal_system_play_asset(name); +} + +static void audio_play_switch(void) +{ + audio_play_ui_asset("switch.wav"); +} + +static void audio_play_enter(void) +{ + audio_play_ui_asset("enter.wav"); +} + +// ============================================================ +// Initialize +// ============================================================ + +void launch_circle_init() +{ + launch_circle[0] = ui_leftOuterPanel; + launch_circle[1] = ui_leftPanel; + launch_circle[2] = ui_switchPanel; + launch_circle[3] = ui_rightPanel; + launch_circle[4] = ui_rightOuterPanel; + + launch_circle[5] = ui_leftOuterLabel; + launch_circle[6] = ui_leftLabel; + launch_circle[7] = ui_switchLabel; + launch_circle[8] = ui_rightLabel; + launch_circle[9] = ui_rightOuterLabel; + + launch_circle[10] = ui_Panel4; + launch_circle[11] = ui_Panel3; + launch_circle[12] = ui_Panel5; + launch_circle[13] = ui_Panel6; + launch_circle[14] = ui_Panel7; + launch_circle[15] = ui_Panel8; + launch_circle[16] = ui_Panel9; + launch_circle[17] = ui_Panel10; + + +} + + +// ============================================================ +// switch panel style +// ============================================================ + +static void switchpanleEnable(int obj_index, int enable) +{ + lv_obj_t *obj = launch_circle[obj_index]; + + if (enable) + { + lv_obj_set_width(obj, 10); + lv_obj_set_height(obj, 10); + lv_obj_set_align(obj, LV_ALIGN_CENTER); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_set_style_bg_color(obj, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(obj, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } + else + { + lv_obj_set_width(obj, 5); + lv_obj_set_height(obj, 5); + lv_obj_set_align(obj, LV_ALIGN_CENTER); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_set_style_bg_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } +} + + +static void switchpanleEnableClick(int obj_index, int enable) +{ + lv_obj_t *obj = launch_circle[obj_index]; + + if (enable) + { + lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE); + } + else + { + lv_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE); + } +} + + +// ============================================================ +// Force the panel to the specified slot +// ============================================================ + +static void snap_panel_to_slot(lv_obj_t *panel, int slot) +{ + lv_obj_set_x(panel, SLOT_X[slot]); + lv_obj_set_y(panel, SLOT_Y[slot]); + lv_obj_set_width(panel, SLOT_W[slot]); + lv_obj_set_height(panel, SLOT_H[slot]); + + if (slot == 0 || slot == 4) + { + lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); + } + else + { + lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); + } +} + + +// ============================================================ +// Force the label to the specified slot +// ============================================================ + +static void snap_label_to_slot(lv_obj_t *label, int slot) +{ + lv_obj_set_x(label, SLOT_X[slot]); + lv_obj_set_y(label, SLOT_Y[slot]); + + if (slot == 5 || slot == 9) + { + lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); + } + else + { + lv_obj_clear_flag(label, LV_OBJ_FLAG_HIDDEN); + } +} + + +// ============================================================ +// Correct all panel positions after animation ends +// ============================================================ + +static void snap_all_panels() +{ + for (int i = 0; i < 5; i++) + { + snap_panel_to_slot(launch_circle[i], i); + } + + for (int i = 5; i < 10; i++) + { + snap_label_to_slot(launch_circle[i], i); + } + + is_animating = false; + + // Reset border colors: center=bright, sides=dark + for (int i = 0; i < 5; i++) { + uint32_t color = (i == 2) ? BORDER_COLOR_CENTER : BORDER_COLOR_SIDE; + lv_obj_set_style_border_color(launch_circle[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); + } + + // Reset all label fonts to bold + for (int i = 5; i < 10; i++) { + lv_obj_set_style_text_font(launch_circle[i], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + } + + if (pending_switch) { + switch_cb_t cb = pending_switch; + pending_switch = NULL; + cb(NULL); + } +} + + +// ============================================================ +// Switch right; called when the right arrow is clicked +// ============================================================ + +void switch_right(lv_event_t *e) +{ + if (is_animating) + { + pending_switch = &switch_right; + return; + } + + is_animating = true; + + lv_obj_clear_flag(launch_circle[0], LV_OBJ_FLAG_HIDDEN); + + launcher_home_animation::animate_right(launch_circle, snap_all_panels); + + snap_panel_to_slot(launch_circle[4], 0); + + lv_obj_clear_flag(launch_circle[5], LV_OBJ_FLAG_HIDDEN); + + snap_label_to_slot(launch_circle[9], 5); + + cpp_app_right(launch_circle[4], launch_circle[9]); + + switchpanleEnableClick(2, 0); + ROTATE_RIGHT(launch_circle, 0, 4); + switchpanleEnableClick(2, 1); + + ROTATE_RIGHT(launch_circle, 5, 9); + + switchpanleEnable(switch_current_pos, 0); + + switch_current_pos = switch_current_pos == 10 ? 17 : switch_current_pos - 1; + + switchpanleEnable(switch_current_pos, 1); +} + + +// ============================================================ +// Switch left; called when the left arrow is clicked +// ============================================================ + +void switch_left(lv_event_t *e) +{ + if (is_animating) + { + pending_switch = &switch_left; + return; + } + + is_animating = true; + + lv_obj_clear_flag(launch_circle[4], LV_OBJ_FLAG_HIDDEN); + + launcher_home_animation::animate_left(launch_circle, snap_all_panels); + + snap_panel_to_slot(launch_circle[0], 4); + + lv_obj_clear_flag(launch_circle[9], LV_OBJ_FLAG_HIDDEN); + + snap_label_to_slot(launch_circle[5], 9); + + cpp_app_left(launch_circle[0], launch_circle[5]); + + switchpanleEnableClick(2, 0); + ROTATE_LEFT(launch_circle, 0, 4); + switchpanleEnableClick(2, 1); + + ROTATE_LEFT(launch_circle, 5, 9); + + switchpanleEnable(switch_current_pos, 0); + + switch_current_pos = switch_current_pos == 17 ? 10 : switch_current_pos + 1; + + switchpanleEnable(switch_current_pos, 1); +} + + + +// ============================================================ +// screen / app +// ============================================================ + +void go_back_home(lv_event_t *e) +{ + lv_disp_load_scr(ui_Screen1); + lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); +} + + +void ui_event_Screen1(lv_event_t *e) +{ + if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) + { + main_key_switch(e); + } +} + + +void app_launch(lv_event_t *e) +{ + cpp_app_launch(); +} + + +static uint32_t fzxc_to_arrow(uint32_t key) +{ + switch (key) + { + case KEY_F: + return KEY_UP; + + case KEY_X: + return KEY_DOWN; + + case KEY_Z: + return KEY_LEFT; + + case KEY_C: + return KEY_RIGHT; + + default: + return key; + } +} + + +// ============================================================ +// key handler +// ============================================================ + +static int lvping_lock = 0; + +void main_key_switch(lv_event_t *e) +{ + struct key_item *elm = (struct key_item *)lv_event_get_param(e); + uint32_t code = fzxc_to_arrow(elm->key_code); + + SLOGI("[LAUNCHER] main_key_switch raw=%u->code=%u state=%s sym=%s", + elm->key_code, + code, + kbd_state_name(elm->key_state), + elm->sym_name); + + if (elm->key_state) + { + switch (code) + { + case KEY_UP: + break; + + case KEY_DOWN: + break; + + case KEY_LEFT: + { + /* Play the preloaded sound effect directly before switching pages. */ + if (!lvping_lock) + { + audio_play_switch(); + switch_right(NULL); + } + } + break; + + case KEY_RIGHT: + { + if (!lvping_lock) + { + audio_play_switch(); + switch_left(NULL); + } + } + break; + + default: + break; + } + } + else if (code == KEY_ENTER) + { + audio_play_enter(); + app_launch(NULL); + } + else if (code == KEY_F12) + { + static lv_obj_t *green_bg; + if (lvping_lock == 0) + { + lvping_lock = 1; + green_bg = lv_obj_create(lv_scr_act()); + lv_obj_set_size(green_bg, 320, 170); + lv_obj_align(green_bg, LV_ALIGN_TOP_LEFT, 0, 0); + + lv_obj_set_style_bg_color(green_bg, lv_color_hex(0x00FF00), LV_PART_MAIN); + lv_obj_set_style_bg_opa(green_bg, LV_OPA_COVER, LV_PART_MAIN); + + lv_obj_set_style_border_width(green_bg, 0, LV_PART_MAIN); + lv_obj_set_style_radius(green_bg, 0, LV_PART_MAIN); + lv_obj_set_style_shadow_width(green_bg, 0, LV_PART_MAIN); + lv_obj_set_style_pad_all(green_bg, 0, LV_PART_MAIN); + } + else + { + lvping_lock = 0; + lv_obj_del(green_bg); + } + } +} + + +} // extern "C" + +namespace { + +char img_path_buf[16][256]; +char regular_font_path[512]; +char mono_font_path_buf[512]; +char gif_path[256]; +const char *font_path = nullptr; +const char *mono_font_path = nullptr; + +} // namespace + +lv_obj_t *startup_gif = nullptr; + +void UILaunchPage::init_images() +{ + struct ImagePath { + const char **ptr; + const char *name; + }; + + ImagePath table[] = { + {&ui_img_zero_png, "zero.png"}, + {&ui_img_time_png, "time_bg.png"}, + {&ui_img_battery_bg_png, "battery_bg.png"}, + {&ui_img_left_png, "left.png"}, + {&ui_img_right_png, "right.png"}, + {&ui_img_zero_logo_w_png, "zero_logo_w.png"}, + {&ui_img_left_logo_png, "left_logo.png"}, + {&ui_img_right_logo_png, "right_logo.png"}, + {&ui_img_detail_info_png, "detail_info.png"}, + {&ui_img_down_logo_png, "down_logo.png"}, + {&ui_img_up_logo_png, "up_logo.png"}, + {&ui_img_camera_png, "camera.png"}, + }; + + int count = sizeof(table) / sizeof(table[0]); + for (int i = 0; i < count && i < 16; ++i) { + snprintf(img_path_buf[i], sizeof(img_path_buf[i]), "%s", cp0_file_path(table[i].name).c_str()); + *table[i].ptr = img_path_buf[i]; + } +} + +void UILaunchPage::init_fonts() +{ + snprintf(regular_font_path, sizeof(regular_font_path), "%s", cp0_file_path("AlibabaPuHuiTi-3-55-Regular.ttf").c_str()); + snprintf(mono_font_path_buf, sizeof(mono_font_path_buf), "%s", cp0_file_path("LiberationMono-Regular.ttf").c_str()); + font_path = regular_font_path; + mono_font_path = mono_font_path_buf; + + g_font_cn_20 = lv_freetype_font_create(font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 20, + LV_FREETYPE_FONT_STYLE_NORMAL); + g_font_cn_14 = lv_freetype_font_create(font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 14, + LV_FREETYPE_FONT_STYLE_NORMAL); + g_font_cn_12 = lv_freetype_font_create(font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, + LV_FREETYPE_FONT_STYLE_BOLD); + g_font_mono_12 = lv_freetype_font_create(mono_font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, + LV_FREETYPE_FONT_STYLE_NORMAL); + + char bold_path[512]; + snprintf(bold_path, sizeof(bold_path), "%s", cp0_file_path("Montserrat-Bold.ttf").c_str()); + g_font_bold_20 = lv_freetype_font_create(bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 18, + LV_FREETYPE_FONT_STYLE_BOLD); + g_font_bold_14 = lv_freetype_font_create(bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, + LV_FREETYPE_FONT_STYLE_BOLD); + g_font_bold_12 = lv_freetype_font_create(bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, + LV_FREETYPE_FONT_STYLE_BOLD); + + if (!g_font_cn_20) g_font_cn_20 = (lv_font_t *)&lv_font_montserrat_20; + if (!g_font_cn_14) g_font_cn_14 = (lv_font_t *)&lv_font_montserrat_14; + if (!g_font_cn_12) g_font_cn_12 = (lv_font_t *)&lv_font_montserrat_12; + if (!g_font_mono_12) g_font_mono_12 = (lv_font_t *)&lv_font_montserrat_12; + if (!g_font_bold_20) g_font_bold_20 = (lv_font_t *)&lv_font_montserrat_18; + if (!g_font_bold_14) g_font_bold_14 = (lv_font_t *)&lv_font_montserrat_14; + if (!g_font_bold_12) g_font_bold_12 = (lv_font_t *)&lv_font_montserrat_12; +} + +void UILaunchPage::load_home_screen() { + SLOGI("[HOME] home_screen_load() - loading launcher home screen"); + ui____initial_actions0 = lv_obj_create(NULL); + lv_disp_load_scr(ui_Screen1); + lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); + + cp0_signal_audio_api_play_asset("startup.mp3"); +} + +static void ui_event_logo_over(lv_event_t *e) +{ + static int done = 0; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_READY && !done) { + done = 1; + SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); + if (startup_gif) lv_gif_pause(startup_gif); + + UILaunchPage::load_home_screen(); + } +} + +void UILaunchPage::start_startup_gif() +{ + snprintf(gif_path, sizeof(gif_path), "%s", cp0_file_path("logo_output.gif").c_str()); + startup_gif = lv_gif_create(NULL); + lv_gif_set_src(startup_gif, gif_path); + lv_obj_center(startup_gif); + lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); + lv_disp_load_scr(startup_gif); +} + +void UILaunchPage::init_ui() +{ + init_images(); + init_fonts(); + + LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); + + lv_disp_t *dispp = lv_disp_get_default(); + lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), + lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT); + lv_disp_set_theme(dispp, theme); + + create_screen(); + + ui_info_bind(); + launch_circle_init(); + + input_group_init(); + +#ifndef APPLAUNCH_STARTUP_ANIMATION + load_home_screen(); +#else +#ifdef HAL_PLATFORM_SDL + load_home_screen(); +#else + char gif_check[256]; + snprintf(gif_check, sizeof(gif_check), "%s", cp0_file_path("logo_output.gif").c_str()); + FILE *gif_file = fopen(gif_check, "r"); + if (gif_file) { + fclose(gif_file); + start_startup_gif(); + } else { + load_home_screen(); + } +#endif +#endif +} + +extern "C" void home_screen_load() +{ + UILaunchPage::load_home_screen(); +} + +extern "C" void start_startup_gif() +{ + UILaunchPage::start_startup_gif(); +} + +extern "C" void ui_inita(void) +{ + UILaunchPage::init_ui(); +} + + +UILaunchPage::UILaunchPage(std::shared_ptr launch) + : home_base(), launch_(std::move(launch)) +{ +} + +UILaunchPage::~UILaunchPage() = default; + +void UILaunchPage::create_screen() +{ + if (ui_Screen1) + return; + ui_Screen1 = lv_obj_create(NULL); lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); /// Flags lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_Screen1, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + create_top(ui_Screen1); + create_app_container(ui_Screen1); + +} + +void UILaunchPage::create_top(lv_obj_t *parent) +{ #ifdef APPLAUNCH_LOGO_USE_PNG - ui_Image1 = lv_img_create(ui_Screen1); + ui_Image1 = lv_img_create(parent); lv_img_set_src(ui_Image1, ui_img_zero_png); lv_obj_set_width(ui_Image1, LV_SIZE_CONTENT); lv_obj_set_height(ui_Image1, LV_SIZE_CONTENT); @@ -22,7 +642,7 @@ void ui_Screen1_screen_init(void) lv_obj_add_flag(ui_Image1, LV_OBJ_FLAG_ADV_HITTEST); lv_obj_clear_flag(ui_Image1, LV_OBJ_FLAG_SCROLLABLE); #else - ui_Image1 = lv_label_create(ui_Screen1); + ui_Image1 = lv_label_create(parent); lv_label_set_text(ui_Image1, "ZERO"); lv_obj_set_x(ui_Image1, 5); lv_obj_set_y(ui_Image1, 2); @@ -31,7 +651,7 @@ void ui_Screen1_screen_init(void) #endif // --- WiFi signal strength bars (4 bars, hidden when disconnected) --- - ui_wifiPanel = lv_obj_create(ui_Screen1); + ui_wifiPanel = lv_obj_create(parent); lv_obj_set_width(ui_wifiPanel, 24); lv_obj_set_height(ui_wifiPanel, 15); lv_obj_set_x(ui_wifiPanel, 210); @@ -88,7 +708,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_width(ui_wifiBar4, 0, LV_PART_MAIN | LV_STATE_DEFAULT); // --- Time status icon --- - ui_Panel1 = lv_obj_create(ui_Screen1); + ui_Panel1 = lv_obj_create(parent); lv_obj_set_width(ui_Panel1, 40); lv_obj_set_height(ui_Panel1, 16); lv_obj_set_x(ui_Panel1, 236); @@ -109,7 +729,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_opa(ui_timeLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); // --- Battery status icon --- - ui_batteryPanel = lv_obj_create(ui_Screen1); + ui_batteryPanel = lv_obj_create(parent); lv_obj_set_width(ui_batteryPanel, 36); lv_obj_set_height(ui_batteryPanel, 16); lv_obj_set_x(ui_batteryPanel, 280); @@ -144,7 +764,20 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_color(ui_powerLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_powerLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel4 = lv_obj_create(ui_Screen1); +} + +void UILaunchPage::create_app_container(lv_obj_t *parent) +{ + ::ui_APP_Container = lv_obj_create(parent); + lv_obj_remove_style_all(::ui_APP_Container); + lv_obj_set_width(::ui_APP_Container, 320); + lv_obj_set_height(::ui_APP_Container, 150); + lv_obj_set_x(::ui_APP_Container, 0); + lv_obj_set_y(::ui_APP_Container, 10); + lv_obj_set_align(::ui_APP_Container, LV_ALIGN_CENTER); + lv_obj_clear_flag(::ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + + ui_Panel4 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel4, 5); lv_obj_set_height(ui_Panel4, 5); lv_obj_set_x(ui_Panel4, -35); @@ -157,7 +790,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel4, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel4, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel3 = lv_obj_create(ui_Screen1); + ui_Panel3 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel3, 8); lv_obj_set_height(ui_Panel3, 8); lv_obj_set_x(ui_Panel3, -25); @@ -170,7 +803,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel3, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel3, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel5 = lv_obj_create(ui_Screen1); + ui_Panel5 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel5, 5); lv_obj_set_height(ui_Panel5, 5); lv_obj_set_x(ui_Panel5, -15); @@ -183,7 +816,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel5, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel5, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel6 = lv_obj_create(ui_Screen1); + ui_Panel6 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel6, 5); lv_obj_set_height(ui_Panel6, 5); lv_obj_set_x(ui_Panel6, -5); @@ -196,7 +829,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel6, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel6, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel7 = lv_obj_create(ui_Screen1); + ui_Panel7 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel7, 5); lv_obj_set_height(ui_Panel7, 5); lv_obj_set_x(ui_Panel7, 5); @@ -209,7 +842,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel7, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel7, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel8 = lv_obj_create(ui_Screen1); + ui_Panel8 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel8, 5); lv_obj_set_height(ui_Panel8, 5); lv_obj_set_x(ui_Panel8, 15); @@ -222,7 +855,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel8, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel8, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel9 = lv_obj_create(ui_Screen1); + ui_Panel9 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel9, 5); lv_obj_set_height(ui_Panel9, 5); lv_obj_set_x(ui_Panel9, 25); @@ -235,7 +868,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel9, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel9, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Panel10 = lv_obj_create(ui_Screen1); + ui_Panel10 = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_Panel10, 5); lv_obj_set_height(ui_Panel10, 5); lv_obj_set_x(ui_Panel10, 35); @@ -248,7 +881,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_color(ui_Panel10, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_Panel10, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_switchLabel = lv_label_create(ui_Screen1); + ui_switchLabel = lv_label_create(::ui_APP_Container); lv_obj_set_width(ui_switchLabel, LV_SIZE_CONTENT); lv_obj_set_height(ui_switchLabel, LV_SIZE_CONTENT); /// 1 lv_obj_set_x(ui_switchLabel, 0); @@ -259,7 +892,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_color(ui_switchLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_switchLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_rightLabel = lv_label_create(ui_Screen1); + ui_rightLabel = lv_label_create(::ui_APP_Container); lv_obj_set_width(ui_rightLabel, LV_SIZE_CONTENT); lv_obj_set_height(ui_rightLabel, LV_SIZE_CONTENT); /// 1 lv_obj_set_x(ui_rightLabel, 99); @@ -270,7 +903,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_font(ui_rightLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_rightLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftLabel = lv_label_create(ui_Screen1); + ui_leftLabel = lv_label_create(::ui_APP_Container); lv_obj_set_width(ui_leftLabel, LV_SIZE_CONTENT); lv_obj_set_height(ui_leftLabel, LV_SIZE_CONTENT); /// 1 lv_obj_set_x(ui_leftLabel, -99); @@ -281,20 +914,20 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_opa(ui_leftLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_font(ui_leftLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftPanel = lv_obj_create(ui_Screen1); + ui_leftPanel = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_leftPanel, 80); lv_obj_set_height(ui_leftPanel, 80); lv_obj_set_x(ui_leftPanel, -99); lv_obj_set_y(ui_leftPanel, -6); lv_obj_set_align(ui_leftPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_leftPanel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_clear_flag(ui_leftPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags lv_obj_set_style_radius(ui_leftPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_leftPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_leftPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(ui_leftPanel, lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_leftPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_switchPanel = lv_obj_create(ui_Screen1); + ui_switchPanel = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_switchPanel, 100); lv_obj_set_height(ui_switchPanel, 100); lv_obj_set_x(ui_switchPanel, 0); @@ -308,34 +941,34 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_border_opa(ui_switchPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_switchPanel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_rightPanel = lv_obj_create(ui_Screen1); + ui_rightPanel = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_rightPanel, 80); lv_obj_set_height(ui_rightPanel, 80); lv_obj_set_x(ui_rightPanel, 99); lv_obj_set_y(ui_rightPanel, -6); lv_obj_set_align(ui_rightPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_rightPanel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_clear_flag(ui_rightPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags lv_obj_set_style_radius(ui_rightPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_rightPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_rightPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(ui_rightPanel, lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_rightPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_rightOuterPanel = lv_obj_create(ui_Screen1); + ui_rightOuterPanel = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_rightOuterPanel, 61); lv_obj_set_height(ui_rightOuterPanel, 61); lv_obj_set_x(ui_rightOuterPanel, 177); lv_obj_set_y(ui_rightOuterPanel, 4); lv_obj_set_align(ui_rightOuterPanel, LV_ALIGN_CENTER); lv_obj_add_flag(ui_rightOuterPanel, LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_clear_flag(ui_rightOuterPanel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_clear_flag(ui_rightOuterPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags lv_obj_set_style_radius(ui_rightOuterPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_rightOuterPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_rightOuterPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(ui_rightOuterPanel, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_rightOuterPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftButton = lv_btn_create(ui_Screen1); + ui_leftButton = lv_btn_create(::ui_APP_Container); lv_obj_set_width(ui_leftButton, 17); lv_obj_set_height(ui_leftButton, 23); lv_obj_set_x(ui_leftButton, -151); @@ -350,7 +983,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_shadow_color(ui_leftButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_rightButton = lv_btn_create(ui_Screen1); + ui_rightButton = lv_btn_create(::ui_APP_Container); lv_obj_set_width(ui_rightButton, 17); lv_obj_set_height(ui_rightButton, 23); lv_obj_set_x(ui_rightButton, 150); @@ -365,21 +998,21 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_shadow_color(ui_rightButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftOuterPanel = lv_obj_create(ui_Screen1); + ui_leftOuterPanel = lv_obj_create(::ui_APP_Container); lv_obj_set_width(ui_leftOuterPanel, 61); lv_obj_set_height(ui_leftOuterPanel, 61); lv_obj_set_x(ui_leftOuterPanel, -177); lv_obj_set_y(ui_leftOuterPanel, 4); lv_obj_set_align(ui_leftOuterPanel, LV_ALIGN_CENTER); lv_obj_add_flag(ui_leftOuterPanel, LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_clear_flag(ui_leftOuterPanel, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_clear_flag(ui_leftOuterPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags lv_obj_set_style_radius(ui_leftOuterPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_leftOuterPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_leftOuterPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(ui_leftOuterPanel, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(ui_leftOuterPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftOuterLabel = lv_label_create(ui_Screen1); + ui_leftOuterLabel = lv_label_create(::ui_APP_Container); lv_obj_set_width(ui_leftOuterLabel, LV_SIZE_CONTENT); lv_obj_set_height(ui_leftOuterLabel, LV_SIZE_CONTENT); /// 1 lv_obj_set_x(ui_leftOuterLabel, -177); @@ -391,7 +1024,7 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_color(ui_leftOuterLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_leftOuterLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_rightOuterLabel = lv_label_create(ui_Screen1); + ui_rightOuterLabel = lv_label_create(::ui_APP_Container); lv_obj_set_width(ui_rightOuterLabel, LV_SIZE_CONTENT); lv_obj_set_height(ui_rightOuterLabel, LV_SIZE_CONTENT); /// 1 lv_obj_set_x(ui_rightOuterLabel, 177); @@ -403,13 +1036,19 @@ void ui_Screen1_screen_init(void) lv_obj_set_style_text_color(ui_rightOuterLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_rightOuterLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_event_cb(ui_leftPanel, ui_event_leftPanel, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_switchPanel, ui_event_switchPanel, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_rightPanel, ui_event_rightPanel, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_rightOuterPanel, ui_event_rightOuterPanel, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_leftButton, ui_event_leftButton, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_rightButton, ui_event_rightButton, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_leftOuterPanel, ui_event_leftOuterPanel, LV_EVENT_ALL, NULL); - lv_obj_add_event_cb(ui_Screen1, ui_event_Screen1, LV_EVENT_ALL, NULL); + lv_obj_add_event_cb(ui_leftPanel, app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_switchPanel, app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_rightPanel, app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_rightOuterPanel, app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_leftButton, switch_right, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_rightButton, switch_left, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_leftOuterPanel, app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(ui_Screen1, main_key_switch, (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); + } + +extern "C" void ui_Screen1_screen_init(void) +{ + UILaunchPage::create_screen(); +} diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h new file mode 100644 index 00000000..6f12b905 --- /dev/null +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -0,0 +1,26 @@ +#pragma once + +#include "components/ui_app_page.hpp" +#include + +class Launch; + +class UILaunchPage : public home_base +{ +public: + explicit UILaunchPage(std::shared_ptr launch); + ~UILaunchPage(); + + static void init_ui(); + static void load_home_screen(); + static void start_startup_gif(); + static void create_screen(); + +private: + static void init_images(); + static void init_fonts(); + static void create_top(lv_obj_t *parent); + static void create_app_container(lv_obj_t *parent); + + std::shared_ptr launch_; +}; diff --git a/projects/APPLaunch/main/ui/components/ui_comp.c b/projects/APPLaunch/main/ui/components/ui_comp.c index 98cb7f00..03c09177 100644 --- a/projects/APPLaunch/main/ui/components/ui_comp.c +++ b/projects/APPLaunch/main/ui/components/ui_comp.c @@ -4,7 +4,6 @@ // Project name: zero #include "../ui.h" -#include "../ui_helpers.h" #include "ui_comp.h" uint32_t LV_EVENT_GET_COMP_CHILD; diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index f7f5ffda..29540460 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -4,7 +4,6 @@ // Project name: zero #include "ui.h" -#include "ui_helpers.h" #include #include #include "lvgl/src/widgets/gif/lv_gif.h" @@ -26,7 +25,6 @@ #include "ui_obj.h" #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN -lv_obj_t * startup_gif; // CUSTOM VARIABLES int Animation_time = 200; @@ -50,42 +48,6 @@ const char *ui_img_down_logo_png; const char *ui_img_up_logo_png; const char *ui_img_camera_png; -static char _img_path_buf[16][256]; -static void ui_images_init(void) -{ - struct { const char **ptr; const char *name; } tbl[] = { - { &ui_img_zero_png, "zero.png" }, - { &ui_img_time_png, "time_bg.png" }, - { &ui_img_battery_bg_png, "battery_bg.png" }, - { &ui_img_left_png, "left.png" }, - { &ui_img_right_png, "right.png" }, - { &ui_img_zero_logo_w_png,"zero_logo_w.png" }, - { &ui_img_left_logo_png, "left_logo.png" }, - { &ui_img_right_logo_png, "right_logo.png" }, - { &ui_img_detail_info_png,"detail_info.png" }, - { &ui_img_down_logo_png, "down_logo.png" }, - { &ui_img_up_logo_png, "up_logo.png" }, - { &ui_img_camera_png, "camera.png" }, - }; - int n = sizeof(tbl) / sizeof(tbl[0]); - for (int i = 0; i < n && i < 16; i++) { - snprintf(_img_path_buf[i], sizeof(_img_path_buf[i]), "%s", cp0_file_path(tbl[i].name)); - *tbl[i].ptr = _img_path_buf[i]; - } -} - - - - - - -const char * font_path = NULL; -const char * mono_font_path = NULL; - - - - - static uint32_t EVT_TERM_KEY; lv_font_t *g_font_cn_20 = NULL; @@ -107,154 +69,8 @@ lv_font_t *g_font_bold_12 = NULL; /* bold app-name font - side */ #error "LV_COLOR_16_SWAP should be 0 to match SquareLine Studio's settings" #endif -///////////////////// ANIMATIONS //////////////////// -#include "Animation/Animation_panel.h" - -// ==================== Label Animations ==================== -// Labels only animate x and y (no width/height change) -// rightward direction: labels slide right (pos5→6→7→8→9) - -#include "Animation/Animation_lable.h" - ///////////////////// FUNCTIONS //////////////////// -#define UI_DEFINE_UI_EVENT_FUN(event_fun, call_fun) __attribute__((weak)) void event_fun(lv_event_t * e) { \ - lv_event_code_t event_code = lv_event_get_code(e); \ - if(event_code == LV_EVENT_CLICKED) { \ - call_fun(e); \ - } \ -} - -#include "ui_event_fun.h" - - - -#undef UI_DEFINE_UI_EVENT_FUN - - - -void font_manager_init(void) -{ - g_font_cn_20 = lv_freetype_font_create( - font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 20, - LV_FREETYPE_FONT_STYLE_NORMAL); - - g_font_cn_14 = lv_freetype_font_create( - font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 14, - LV_FREETYPE_FONT_STYLE_NORMAL); - - g_font_cn_12 = lv_freetype_font_create( - font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, - LV_FREETYPE_FONT_STYLE_BOLD); - - g_font_mono_12 = lv_freetype_font_create( - mono_font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, - LV_FREETYPE_FONT_STYLE_NORMAL); - - { - static char bold_path[512]; - snprintf(bold_path, sizeof(bold_path), "%s", cp0_file_path("Montserrat-Bold.ttf")); - g_font_bold_20 = lv_freetype_font_create( - bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 18, - LV_FREETYPE_FONT_STYLE_BOLD); - g_font_bold_14 = lv_freetype_font_create( - bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, - LV_FREETYPE_FONT_STYLE_BOLD); - g_font_bold_12 = lv_freetype_font_create( - bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, - LV_FREETYPE_FONT_STYLE_BOLD); - } - - // Fallback to built-in fonts if freetype loading failed (e.g. on macOS emulator) - if (!g_font_cn_20) g_font_cn_20 = (lv_font_t *)&lv_font_montserrat_20; - if (!g_font_cn_14) g_font_cn_14 = (lv_font_t *)&lv_font_montserrat_14; - if (!g_font_cn_12) g_font_cn_12 = (lv_font_t *)&lv_font_montserrat_12; - if (!g_font_mono_12) g_font_mono_12 = (lv_font_t *)&lv_font_montserrat_12; - if (!g_font_bold_20) g_font_bold_20 = (lv_font_t *)&lv_font_montserrat_18; - if (!g_font_bold_14) g_font_bold_14 = (lv_font_t *)&lv_font_montserrat_14; - if (!g_font_bold_12) g_font_bold_12 = (lv_font_t *)&lv_font_montserrat_12; -} - -///////////////////// SCREENS //////////////////// - -void home_screen_load() -{ - SLOGI("[HOME] home_screen_load() - loading launcher home screen"); - ui____initial_actions0 = lv_obj_create(NULL); - lv_disp_load_scr(ui_Screen1); - lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); - - cp0_signal_audio_api_play_asset("startup.mp3"); -} - -void ui_event_logo_over(lv_event_t * e) { - static int done = 0; - lv_event_code_t event_code = lv_event_get_code(e); - if(event_code == LV_EVENT_READY && !done) { - done = 1; - SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); - if (startup_gif) lv_gif_pause(startup_gif); - - home_screen_load(); - } -} - -static char _gif_path[256]; -void start_startup_gif() -{ - snprintf(_gif_path, sizeof(_gif_path), "%s", cp0_file_path("logo_output.gif")); - startup_gif = lv_gif_create(NULL); - lv_gif_set_src(startup_gif, _gif_path); - lv_obj_center(startup_gif); - lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); - lv_disp_load_scr(startup_gif); -} - -void ui_init(void) -{ - ui_images_init(); - static char regular_font_path[512]; - static char mono_font_path_buf[512]; - snprintf(regular_font_path, sizeof(regular_font_path), "%s", cp0_file_path("AlibabaPuHuiTi-3-55-Regular.ttf")); - snprintf(mono_font_path_buf, sizeof(mono_font_path_buf), "%s", cp0_file_path("LiberationMono-Regular.ttf")); - font_path = regular_font_path; - mono_font_path = mono_font_path_buf; - font_manager_init(); - - LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); - - lv_disp_t * dispp = lv_disp_get_default(); - lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), - false, LV_FONT_DEFAULT); - lv_disp_set_theme(dispp, theme); - - // Initialize each screen - ui_Screen1_screen_init(); - - // Bind screen metadata - ui_info_bind(); - launch_circle_init(); - - // Initialize the input group - input_group_init(); - - // Show the boot animation (requires share/images/logo_output.gif) -#ifndef APPLAUNCH_STARTUP_ANIMATION - home_screen_load(); -#else - #ifdef HAL_PLATFORM_SDL - home_screen_load(); - #else - { - char gif_check[256]; - snprintf(gif_check, sizeof(gif_check), "%s", cp0_file_path("logo_output.gif")); - FILE *_gif_f = fopen(gif_check, "r"); - if (_gif_f) { fclose(_gif_f); start_startup_gif(); } - else { home_screen_load(); } - } - #endif -#endif -} diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp new file mode 100644 index 00000000..d80b29ac --- /dev/null +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -0,0 +1,22 @@ +// This file was generated by SquareLine Studio +// SquareLine Studio version: SquareLine Studio 1.5.0 +// LVGL version: 8.3.11 +// Project name: zero + +#include "ui.h" +#include +#include +#include "lvgl/src/widgets/gif/lv_gif.h" +#include "cp0_lvgl_app.h" +#include "sample_log.h" + + + + + +std::unique_ptr home; +void ui_init(void) +{ + UILaunchPage::init_ui(); + home = std::make_unique(); +} diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index e61c41f7..13a1183d 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -9,12 +9,12 @@ #include "hal_lvgl_bsp.h" #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif #include "lvgl/lvgl.h" -#include "ui_helpers.h" #include "components/ui_comp.h" #include "components/ui_comp_hook.h" #include "ui_events.h" @@ -23,133 +23,91 @@ extern "C" { #include "cp0_lvgl_app.h" #define lv_mem_alloc lv_malloc -#define lv_mem_free lv_free +#define lv_mem_free lv_free #define lv_event_send(obj, evt, param) lv_obj_send_event(obj, evt, param) -// typedef void (*ui_anim_ready_cb_t)(lv_anim_t * a); -typedef void (*ui_anim_ready_cb_t)(lv_anim_t *); - -void leftPanelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void centerPanelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void rightPanelToRightOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void leftOuterPanelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void leftPanelToLeftOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void centerPanelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void rightPanelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void rightOuterPanelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -// Label animations - rightward direction -void leftOuterLabelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void leftLabelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void centerLabelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void rightLabelToRightOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -// Label animations - leftward direction -void rightOuterLabelToRight_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void rightLabelToCenter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void centerLabelToLeft_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -void leftLabelToLeftOuter_Animation(lv_obj_t * TargetObject, int delay, ui_anim_ready_cb_t ready_cb); -// SCREEN: ui_Screen1 - -void ui_Screen1_screen_init(void); - - -void launch_circle_init(); -void ui_info_bind(); - - - + // SCREEN: ui_Screen1 + void ui_Screen1_screen_init(void); + void launch_circle_init(); + void ui_info_bind(); #define LV_EVENT_KEYBOARD_GET_KEY(e) ((struct key_item *)lv_event_get_param(e))->key_code #define LV_EVENT_KEYBOARD_GET_KEY_STATE(e) ((struct key_item *)lv_event_get_param(e))->key_state -#define IS_KEY_PRESSED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD)&&(LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) -#define IS_KEY_RELEASED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD)&&(LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) +#define IS_KEY_PRESSED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) +#define IS_KEY_RELEASED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) #define LV_EVENT_BATTERY lv_c_event[CP0_C_EVENT_BATTERY] #define LV_EVENT_DELL_CPP_DATA lv_c_event[CP0_C_EVENT_DELL_CPP_DATA] #define LV_EVENT_BATTERY_GET_INFO(e) ((cp0_battery_info_t *)lv_event_get_param(e)) - #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN -#define UI_DEFINE_OBJECT( x ) extern lv_obj_t * x ; -#define UI_DEFINE_EVENT_FUN( x ) void x(lv_event_t * e); +#define UI_DEFINE_OBJECT(x) extern lv_obj_t *x; +#define UI_DEFINE_EVENT_FUN(x) void x(lv_event_t *e); #include "ui_obj.h" #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN - - - -#define UI_DEFINE_UI_EVENT_FUN(event_fun, call_fun) void event_fun(lv_event_t * e); - -#include "ui_event_fun.h" -#undef UI_DEFINE_UI_EVENT_FUN - - -lv_obj_t * ui_console_creat(lv_event_t * e); -void ui_console_exit(lv_event_t * e); -void ui_console_key(lv_event_t * e); - - - -// CUSTOM VARIABLES -extern const char *ui_img_zero_png; -extern const char *ui_img_time_png; -extern const char *ui_img_battery_bg_png; -extern const char *ui_img_left_png; -extern const char *ui_img_right_png; -extern const char *ui_img_store_logo_png ; -extern const char *ui_img_cli_logo_png ; -extern const char *ui_img_claw_logo_png ; -extern const char *ui_img_setting_logo_png ; -extern const char *ui_img_python_logo_png ; - - - - - -extern const char *ui_img_zero_logo_w_png ; -extern const char *ui_img_left_logo_png ; -extern const char *ui_img_right_logo_png ; -extern const char *ui_img_detail_info_png ; -extern const char *ui_img_down_logo_png ; -extern const char *ui_img_up_logo_png ; -extern const char *ui_img_camera_png ; - - -extern lv_font_t *g_font_cn_20; -extern lv_font_t *g_font_cn_14; -extern lv_font_t *g_font_cn_12; -extern lv_font_t *g_font_mono_12; -extern lv_font_t *g_font_bold_20; -extern lv_font_t *g_font_bold_14; -extern lv_font_t *g_font_bold_12; + lv_obj_t *ui_console_creat(lv_event_t *e); + void ui_console_exit(lv_event_t *e); + void ui_console_key(lv_event_t *e); + + // CUSTOM VARIABLES + extern const char *ui_img_zero_png; + extern const char *ui_img_time_png; + extern const char *ui_img_battery_bg_png; + extern const char *ui_img_left_png; + extern const char *ui_img_right_png; + extern const char *ui_img_store_logo_png; + extern const char *ui_img_cli_logo_png; + extern const char *ui_img_claw_logo_png; + extern const char *ui_img_setting_logo_png; + extern const char *ui_img_python_logo_png; + + extern const char *ui_img_zero_logo_w_png; + extern const char *ui_img_left_logo_png; + extern const char *ui_img_right_logo_png; + extern const char *ui_img_detail_info_png; + extern const char *ui_img_down_logo_png; + extern const char *ui_img_up_logo_png; + extern const char *ui_img_camera_png; + + extern lv_font_t *g_font_cn_20; + extern lv_font_t *g_font_cn_14; + extern lv_font_t *g_font_cn_12; + extern lv_font_t *g_font_mono_12; + extern lv_font_t *g_font_bold_20; + extern lv_font_t *g_font_bold_14; + extern lv_font_t *g_font_bold_12; // Launcher layout constants -#define BORDER_COLOR_CENTER 0x444444 -#define BORDER_COLOR_SIDE 0x222222 -#define LABEL_Y_CENTER 50 -#define LABEL_Y_SIDE 50 +#define BORDER_COLOR_CENTER 0x444444 +#define BORDER_COLOR_SIDE 0x222222 +#define LABEL_Y_CENTER 50 +#define LABEL_Y_SIDE 50 -// EVENTS -extern lv_obj_t * ui____initial_actions0; + // EVENTS + extern lv_obj_t *ui____initial_actions0; #ifdef _WIN32 - #define PATH_SEP "\\" +#define PATH_SEP "\\" #else - #define PATH_SEP "/" +#define PATH_SEP "/" #endif -char* cimg_path(const char *name); -char* caudio_path(const char *name); -char* cfont_path(const char *name); - + char *cimg_path(const char *name); + char *caudio_path(const char *name); + char *cfont_path(const char *name); -// UI INIT -void ui_init(void); + // UI INIT + void ui_init(void); #ifdef __cplusplus } /*extern "C"*/ +#include "Launch.h" +#include "UILaunchPage.h" +#include "zero_lvgl_os.h" #endif #endif diff --git a/projects/APPLaunch/main/ui/ui_event_fun.h b/projects/APPLaunch/main/ui/ui_event_fun.h deleted file mode 100644 index bcf9db19..00000000 --- a/projects/APPLaunch/main/ui/ui_event_fun.h +++ /dev/null @@ -1,25 +0,0 @@ -UI_DEFINE_UI_EVENT_FUN(ui_event_Screen1, main_key_switch) -UI_DEFINE_UI_EVENT_FUN(ui_event_leftPanel, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_switchPanel, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_rightPanel, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_rightOuterPanel, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_leftButton, switch_right) -UI_DEFINE_UI_EVENT_FUN(ui_event_rightButton, switch_left) -UI_DEFINE_UI_EVENT_FUN(ui_event_leftOuterPanel, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_AppStore, app_store_switch) -UI_DEFINE_UI_EVENT_FUN(ui_event_Image4, go_back_home) -UI_DEFINE_UI_EVENT_FUN(ui_event_APPNote, app_note_switch) -UI_DEFINE_UI_EVENT_FUN(ui_event_Image2, go_back_app_store) -UI_DEFINE_UI_EVENT_FUN(ui_event_appinstall, app_install) -UI_DEFINE_UI_EVENT_FUN(ui_event_appremove, app_remove) -UI_DEFINE_UI_EVENT_FUN(ui_event_appupdate, app_update) -UI_DEFINE_UI_EVENT_FUN(ui_event_clawapp, app_store_switch) -UI_DEFINE_UI_EVENT_FUN(ui_event_Image3, go_back_home) -UI_DEFINE_UI_EVENT_FUN(ui_event_Image5, go_back_home) -UI_DEFINE_UI_EVENT_FUN(ui_event_leftPanel2, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_rightButton2, switch_left) -UI_DEFINE_UI_EVENT_FUN(ui_event_leftOuterPanel2, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_rightOuterPanel2, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_switchPanel2, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_rightPanel2, app_launch) -UI_DEFINE_UI_EVENT_FUN(ui_event_leftButton2, switch_right) diff --git a/projects/APPLaunch/main/ui/ui_events.c b/projects/APPLaunch/main/ui/ui_events.c deleted file mode 100644 index 14d76611..00000000 --- a/projects/APPLaunch/main/ui/ui_events.c +++ /dev/null @@ -1,439 +0,0 @@ -#include "ui.h" -#include "sample_log.h" - -#include -#include -#include - -#include "compat/input_keys.h" -#include "Animation/ui_launcher_animation.h" - - -typedef void (*switch_cb_t)(lv_event_t *); - - -#define ROTATE_LEFT(arr, start, end) \ - do \ - { \ - typeof((arr)[0]) _tmp = (arr)[(start)]; \ - memmove(&(arr)[(start)], &(arr)[(start) + 1], \ - ((end) - (start)) * sizeof((arr)[0])); \ - (arr)[(end)] = _tmp; \ - } while (0) - -#define ROTATE_RIGHT(arr, start, end) \ - do \ - { \ - typeof((arr)[0]) _tmp = (arr)[(end)]; \ - memmove(&(arr)[(start) + 1], &(arr)[(start)], \ - ((end) - (start)) * sizeof((arr)[0])); \ - (arr)[(start)] = _tmp; \ - } while (0) - -lv_obj_t *launch_circle[100]; - -// ==================== standard coordinates for 5 slots ==================== - -static const lv_coord_t SLOT_X[] = {-177, -99, 0, 99, 177, -177, -99, 0, 99, 177}; -static const lv_coord_t SLOT_Y[] = {4, -6, -16, -6, 4, LABEL_Y_SIDE, LABEL_Y_SIDE, LABEL_Y_CENTER, LABEL_Y_SIDE, LABEL_Y_SIDE}; -static const lv_coord_t SLOT_W[] = {61, 80, 100, 80, 61}; -static const lv_coord_t SLOT_H[] = {61, 80, 100, 80, 61}; - -static bool is_animating = false; -static switch_cb_t pending_switch = NULL; - -static int Panel_current_pos = 2; -static int switch_current_pos = 11; - - -// ============================================================ -// audio -// ============================================================ - -static void audio_play_ui_asset(const char *name) -{ - cp0_signal_system_play_asset(name); -} - -static void audio_play_switch(void) -{ - audio_play_ui_asset("switch.wav"); -} - -static void audio_play_enter(void) -{ - audio_play_ui_asset("enter.wav"); -} - -// ============================================================ -// Initialize -// ============================================================ - -void launch_circle_init() -{ - launch_circle[0] = ui_leftOuterPanel; - launch_circle[1] = ui_leftPanel; - launch_circle[2] = ui_switchPanel; - launch_circle[3] = ui_rightPanel; - launch_circle[4] = ui_rightOuterPanel; - - launch_circle[5] = ui_leftOuterLabel; - launch_circle[6] = ui_leftLabel; - launch_circle[7] = ui_switchLabel; - launch_circle[8] = ui_rightLabel; - launch_circle[9] = ui_rightOuterLabel; - - launch_circle[10] = ui_Panel4; - launch_circle[11] = ui_Panel3; - launch_circle[12] = ui_Panel5; - launch_circle[13] = ui_Panel6; - launch_circle[14] = ui_Panel7; - launch_circle[15] = ui_Panel8; - launch_circle[16] = ui_Panel9; - launch_circle[17] = ui_Panel10; - - -} - - -// ============================================================ -// switch panel style -// ============================================================ - -static void switchpanleEnable(int obj_index, int enable) -{ - lv_obj_t *obj = launch_circle[obj_index]; - - if (enable) - { - lv_obj_set_width(obj, 10); - lv_obj_set_height(obj, 10); - lv_obj_set_align(obj, LV_ALIGN_CENTER); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_set_style_bg_color(obj, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(obj, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } - else - { - lv_obj_set_width(obj, 5); - lv_obj_set_height(obj, 5); - lv_obj_set_align(obj, LV_ALIGN_CENTER); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_set_style_bg_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(obj, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } -} - - -static void switchpanleEnableClick(int obj_index, int enable) -{ - lv_obj_t *obj = launch_circle[obj_index]; - - if (enable) - { - lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE); - } - else - { - lv_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE); - } -} - - -// ============================================================ -// Force the panel to the specified slot -// ============================================================ - -static void snap_panel_to_slot(lv_obj_t *panel, int slot) -{ - lv_obj_set_x(panel, SLOT_X[slot]); - lv_obj_set_y(panel, SLOT_Y[slot]); - lv_obj_set_width(panel, SLOT_W[slot]); - lv_obj_set_height(panel, SLOT_H[slot]); - - if (slot == 0 || slot == 4) - { - lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); - } - else - { - lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); - } -} - - -// ============================================================ -// Force the label to the specified slot -// ============================================================ - -static void snap_label_to_slot(lv_obj_t *label, int slot) -{ - lv_obj_set_x(label, SLOT_X[slot]); - lv_obj_set_y(label, SLOT_Y[slot]); - - if (slot == 5 || slot == 9) - { - lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); - } - else - { - lv_obj_clear_flag(label, LV_OBJ_FLAG_HIDDEN); - } -} - - -// ============================================================ -// Correct all panel positions after animation ends -// ============================================================ - -static void snap_all_panels(lv_anim_t *a) -{ - for (int i = 0; i < 5; i++) - { - snap_panel_to_slot(launch_circle[i], i); - } - - for (int i = 5; i < 10; i++) - { - snap_label_to_slot(launch_circle[i], i); - } - - is_animating = false; - - // Reset border colors: center=bright, sides=dark - for (int i = 0; i < 5; i++) { - uint32_t color = (i == 2) ? BORDER_COLOR_CENTER : BORDER_COLOR_SIDE; - lv_obj_set_style_border_color(launch_circle[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); - } - - // Reset all label fonts to bold - for (int i = 5; i < 10; i++) { - lv_obj_set_style_text_font(launch_circle[i], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - } - - if (pending_switch) { - switch_cb_t cb = pending_switch; - pending_switch = NULL; - cb(NULL); - } -} - - -// ============================================================ -// Switch right; called when the right arrow is clicked -// ============================================================ - -void switch_right(lv_event_t *e) -{ - if (is_animating) - { - pending_switch = &switch_right; - return; - } - - is_animating = true; - - lv_obj_clear_flag(launch_circle[0], LV_OBJ_FLAG_HIDDEN); - - launcher_home_animate_right(launch_circle, snap_all_panels); - - snap_panel_to_slot(launch_circle[4], 0); - - lv_obj_clear_flag(launch_circle[5], LV_OBJ_FLAG_HIDDEN); - - snap_label_to_slot(launch_circle[9], 5); - - cpp_app_right(launch_circle[4], launch_circle[9]); - - switchpanleEnableClick(2, 0); - ROTATE_RIGHT(launch_circle, 0, 4); - switchpanleEnableClick(2, 1); - - ROTATE_RIGHT(launch_circle, 5, 9); - - switchpanleEnable(switch_current_pos, 0); - - switch_current_pos = switch_current_pos == 10 ? 17 : switch_current_pos - 1; - - switchpanleEnable(switch_current_pos, 1); -} - - -// ============================================================ -// Switch left; called when the left arrow is clicked -// ============================================================ - -void switch_left(lv_event_t *e) -{ - if (is_animating) - { - pending_switch = &switch_left; - return; - } - - is_animating = true; - - lv_obj_clear_flag(launch_circle[4], LV_OBJ_FLAG_HIDDEN); - - launcher_home_animate_left(launch_circle, snap_all_panels); - - snap_panel_to_slot(launch_circle[0], 4); - - lv_obj_clear_flag(launch_circle[9], LV_OBJ_FLAG_HIDDEN); - - snap_label_to_slot(launch_circle[5], 9); - - cpp_app_left(launch_circle[0], launch_circle[5]); - - switchpanleEnableClick(2, 0); - ROTATE_LEFT(launch_circle, 0, 4); - switchpanleEnableClick(2, 1); - - ROTATE_LEFT(launch_circle, 5, 9); - - switchpanleEnable(switch_current_pos, 0); - - switch_current_pos = switch_current_pos == 17 ? 10 : switch_current_pos + 1; - - switchpanleEnable(switch_current_pos, 1); -} - - -// ============================================================ -// screen / app -// ============================================================ - -void go_back_home(lv_event_t *e) -{ - lv_disp_load_scr(ui_Screen1); - lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); -} - - -void ui_event_Screen1(lv_event_t *e) -{ - if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) - { - main_key_switch(e); - } -} - - -void app_launch(lv_event_t *e) -{ - cpp_app_launch(); -} - - -static uint32_t fzxc_to_arrow(uint32_t key) -{ - switch (key) - { - case KEY_F: - return KEY_UP; - - case KEY_X: - return KEY_DOWN; - - case KEY_Z: - return KEY_LEFT; - - case KEY_C: - return KEY_RIGHT; - - default: - return key; - } -} - - -// ============================================================ -// key handler -// ============================================================ - -int lvping_lock = 0; - -void main_key_switch(lv_event_t *e) -{ - struct key_item *elm = (struct key_item *)lv_event_get_param(e); - uint32_t code = fzxc_to_arrow(elm->key_code); - - SLOGI("[LAUNCHER] main_key_switch raw=%u->code=%u state=%s sym=%s", - elm->key_code, - code, - kbd_state_name(elm->key_state), - elm->sym_name); - - if (elm->key_state) - { - switch (code) - { - case KEY_UP: - break; - - case KEY_DOWN: - break; - - case KEY_LEFT: - { - /* Play the preloaded sound effect directly before switching pages. */ - if (!lvping_lock) - { - audio_play_switch(); - switch_right(NULL); - } - } - break; - - case KEY_RIGHT: - { - if (!lvping_lock) - { - audio_play_switch(); - switch_left(NULL); - } - } - break; - - default: - break; - } - } - else if (code == KEY_ENTER) - { - audio_play_enter(); - app_launch(NULL); - } - else if (code == KEY_F12) - { - static lv_obj_t *green_bg; - if (lvping_lock == 0) - { - lvping_lock = 1; - green_bg = lv_obj_create(lv_scr_act()); - lv_obj_set_size(green_bg, 320, 170); - lv_obj_align(green_bg, LV_ALIGN_TOP_LEFT, 0, 0); - - lv_obj_set_style_bg_color(green_bg, lv_color_hex(0x00FF00), LV_PART_MAIN); - lv_obj_set_style_bg_opa(green_bg, LV_OPA_COVER, LV_PART_MAIN); - - lv_obj_set_style_border_width(green_bg, 0, LV_PART_MAIN); - lv_obj_set_style_radius(green_bg, 0, LV_PART_MAIN); - lv_obj_set_style_shadow_width(green_bg, 0, LV_PART_MAIN); - lv_obj_set_style_pad_all(green_bg, 0, LV_PART_MAIN); - } - else - { - lvping_lock = 0; - lv_obj_del(green_bg); - } - } -} diff --git a/projects/APPLaunch/main/ui/ui_events.h b/projects/APPLaunch/main/ui/ui_events.h index bbbb8140..19e37652 100644 --- a/projects/APPLaunch/main/ui/ui_events.h +++ b/projects/APPLaunch/main/ui/ui_events.h @@ -12,10 +12,12 @@ extern "C" { -#define UI_DEFINE_UI_EVENT_FUN(event_fun, call_fun) void call_fun(lv_event_t * e); +void switch_left(lv_event_t *e); +void switch_right(lv_event_t *e); +void app_launch(lv_event_t *e); +void go_back_home(lv_event_t *e); +void main_key_switch(lv_event_t *e); -#include "ui_event_fun.h" -#undef UI_DEFINE_UI_EVENT_FUN void app_card_click(lv_event_t * e); diff --git a/projects/APPLaunch/main/ui/ui_events_weak.c b/projects/APPLaunch/main/ui/ui_events_weak.c deleted file mode 100644 index a45d32bd..00000000 --- a/projects/APPLaunch/main/ui/ui_events_weak.c +++ /dev/null @@ -1,66 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#include "ui.h" - -__attribute__((weak)) void switch_left(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void switch_right(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void app_launch(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void go_back_home(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void app_install(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void app_remove(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void app_update(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void go_back_app_store(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void main_key_switch(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void app_store_switch(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void app_note_switch(lv_event_t * e) -{ - // Your code here -} - -__attribute__((weak)) void python_console_input(lv_event_t * e) -{ - // Your code here -} diff --git a/projects/APPLaunch/main/ui/ui_helpers.c b/projects/APPLaunch/main/ui/ui_helpers.c deleted file mode 100644 index 52a3c27c..00000000 --- a/projects/APPLaunch/main/ui/ui_helpers.c +++ /dev/null @@ -1,277 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#include "ui_helpers.h" - -void _ui_bar_set_property(lv_obj_t * target, int id, int val) -{ - if(id == _UI_BAR_PROPERTY_VALUE_WITH_ANIM) lv_bar_set_value(target, val, LV_ANIM_ON); - if(id == _UI_BAR_PROPERTY_VALUE) lv_bar_set_value(target, val, LV_ANIM_OFF); -} - -void _ui_basic_set_property(lv_obj_t * target, int id, int val) -{ - if(id == _UI_BASIC_PROPERTY_POSITION_X) lv_obj_set_x(target, val); - if(id == _UI_BASIC_PROPERTY_POSITION_Y) lv_obj_set_y(target, val); - if(id == _UI_BASIC_PROPERTY_WIDTH) lv_obj_set_width(target, val); - if(id == _UI_BASIC_PROPERTY_HEIGHT) lv_obj_set_height(target, val); -} - -void _ui_dropdown_set_property(lv_obj_t * target, int id, int val) -{ - if(id == _UI_DROPDOWN_PROPERTY_SELECTED) lv_dropdown_set_selected(target, val); -} - -void _ui_image_set_property(lv_obj_t * target, int id, uint8_t * val) -{ - if(id == _UI_IMAGE_PROPERTY_IMAGE) lv_img_set_src(target, val); -} - -void _ui_label_set_property(lv_obj_t * target, int id, const char * val) -{ - if(id == _UI_LABEL_PROPERTY_TEXT) lv_label_set_text(target, val); -} - -void _ui_roller_set_property(lv_obj_t * target, int id, int val) -{ - if(id == _UI_ROLLER_PROPERTY_SELECTED_WITH_ANIM) lv_roller_set_selected(target, val, LV_ANIM_ON); - if(id == _UI_ROLLER_PROPERTY_SELECTED) lv_roller_set_selected(target, val, LV_ANIM_OFF); -} - -void _ui_slider_set_property(lv_obj_t * target, int id, int val) -{ - if(id == _UI_SLIDER_PROPERTY_VALUE_WITH_ANIM) lv_slider_set_value(target, val, LV_ANIM_ON); - if(id == _UI_SLIDER_PROPERTY_VALUE) lv_slider_set_value(target, val, LV_ANIM_OFF); -} - -void _ui_screen_change(lv_obj_t ** target, lv_scr_load_anim_t fademode, int spd, int delay, void (*target_init)(void)) -{ - if(*target == NULL) - target_init(); - lv_scr_load_anim(*target, fademode, spd, delay, false); -} - -void _ui_screen_delete(lv_obj_t ** target) -{ - if(*target == NULL) { - lv_obj_del(*target); - target = NULL; - } -} - -void _ui_arc_increment(lv_obj_t * target, int val) -{ - int old = lv_arc_get_value(target); - lv_arc_set_value(target, old + val); - lv_obj_send_event(target, LV_EVENT_VALUE_CHANGED, NULL); -} - -void _ui_bar_increment(lv_obj_t * target, int val, int anm) -{ - int old = lv_bar_get_value(target); - lv_bar_set_value(target, old + val, anm); -} - -void _ui_slider_increment(lv_obj_t * target, int val, int anm) -{ - int old = lv_slider_get_value(target); - lv_slider_set_value(target, old + val, anm); - lv_obj_send_event(target, LV_EVENT_VALUE_CHANGED, NULL); -} - -void _ui_keyboard_set_target(lv_obj_t * keyboard, lv_obj_t * textarea) -{ - lv_keyboard_set_textarea(keyboard, textarea); -} - -void _ui_flag_modify(lv_obj_t * target, int32_t flag, int value) -{ - if(value == _UI_MODIFY_FLAG_TOGGLE) { - if(lv_obj_has_flag(target, flag)) lv_obj_clear_flag(target, flag); - else lv_obj_add_flag(target, flag); - } - else if(value == _UI_MODIFY_FLAG_ADD) lv_obj_add_flag(target, flag); - else lv_obj_clear_flag(target, flag); -} -void _ui_state_modify(lv_obj_t * target, int32_t state, int value) -{ - if(value == _UI_MODIFY_STATE_TOGGLE) { - if(lv_obj_has_state(target, state)) lv_obj_clear_state(target, state); - else lv_obj_add_state(target, state); - } - else if(value == _UI_MODIFY_STATE_ADD) lv_obj_add_state(target, state); - else lv_obj_clear_state(target, state); -} - -void _ui_textarea_move_cursor(lv_obj_t * target, int val) -{ - if(val == UI_MOVE_CURSOR_UP) lv_textarea_cursor_up(target); - if(val == UI_MOVE_CURSOR_RIGHT) lv_textarea_cursor_right(target); - if(val == UI_MOVE_CURSOR_DOWN) lv_textarea_cursor_down(target); - if(val == UI_MOVE_CURSOR_LEFT) lv_textarea_cursor_left(target); - lv_obj_add_state(target, LV_STATE_FOCUSED); -} - -void scr_unloaded_delete_cb(lv_event_t * e) -{ - lv_obj_t ** var = lv_event_get_user_data(e); - lv_obj_del(*var); - (*var) = NULL; -} - -void _ui_opacity_set(lv_obj_t * target, int val) -{ - lv_obj_set_style_opa(target, val, 0); -} - -void _ui_anim_callback_free_user_data(lv_anim_t * a) -{ - lv_mem_free(a->user_data); - a->user_data = NULL; -} - -void _ui_anim_callback_free_user_data_and_ready_cb(lv_anim_t * a) -{ - ui_anim_user_data_t *anim_data = (ui_anim_user_data_t *)a->user_data; - if(anim_data->ready_cb) - anim_data->ready_cb(a); - _ui_anim_callback_free_user_data(a); -} - -void _ui_anim_callback_set_x(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_obj_set_x(usr->target, v); -} - -void _ui_anim_callback_set_y(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_obj_set_y(usr->target, v); -} - -void _ui_anim_callback_set_width(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_obj_set_width(usr->target, v); -} - -void _ui_anim_callback_set_height(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_obj_set_height(usr->target, v); -} - -void _ui_anim_callback_set_opacity(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_obj_set_style_opa(usr->target, v, 0); -} - - - -void _ui_anim_callback_set_image_zoom(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_img_set_zoom(usr->target, v); -} - -void _ui_anim_callback_set_image_angle(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - lv_img_set_angle(usr->target, v); -} - -void _ui_anim_callback_set_image_frame(lv_anim_t * a, int32_t v) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - usr->val = v; - if(v < 0) v = 0; - if(v >= usr->imgset_size) v = usr->imgset_size - 1; - lv_img_set_src(usr->target, usr->imgset[v]); -} - -int32_t _ui_anim_callback_get_x(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_obj_get_x_aligned(usr->target); -} - -int32_t _ui_anim_callback_get_y(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_obj_get_y_aligned(usr->target); -} - -int32_t _ui_anim_callback_get_width(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_obj_get_width(usr->target); -} - -int32_t _ui_anim_callback_get_height(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_obj_get_height(usr->target); -} - -int32_t _ui_anim_callback_get_opacity(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_obj_get_style_opa(usr->target, 0); -} - -int32_t _ui_anim_callback_get_image_zoom(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_img_get_zoom(usr->target); -} - -int32_t _ui_anim_callback_get_image_angle(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return lv_img_get_angle(usr->target); -} - -int32_t _ui_anim_callback_get_image_frame(lv_anim_t * a) -{ - ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data; - return usr->val; -} - -void _ui_arc_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix) -{ - char buf[_UI_TEMPORARY_STRING_BUFFER_SIZE]; - lv_snprintf(buf, sizeof(buf), "%s%d%s", prefix, (int)lv_arc_get_value(src), postfix); - lv_label_set_text(trg, buf); -} - -void _ui_slider_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix) -{ - char buf[_UI_TEMPORARY_STRING_BUFFER_SIZE]; - lv_snprintf(buf, sizeof(buf), "%s%d%s", prefix, (int)lv_slider_get_value(src), postfix); - lv_label_set_text(trg, buf); -} -void _ui_checked_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * txt_on, const char * txt_off) -{ - if(lv_obj_has_state(src, LV_STATE_CHECKED)) lv_label_set_text(trg, txt_on); - else lv_label_set_text(trg, txt_off); -} - -void _ui_spinbox_step(lv_obj_t * target, int val) -{ - if(val > 0) lv_spinbox_increment(target); - else lv_spinbox_decrement(target); - - lv_obj_send_event(target, LV_EVENT_VALUE_CHANGED, NULL); -} - -void _ui_switch_theme(int val) -{ -#ifdef UI_THEME_ACTIVE - ui_theme_set(val); -#endif -} - diff --git a/projects/APPLaunch/main/ui/ui_helpers.h b/projects/APPLaunch/main/ui/ui_helpers.h deleted file mode 100644 index aad74282..00000000 --- a/projects/APPLaunch/main/ui/ui_helpers.h +++ /dev/null @@ -1,141 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#ifndef _ZERO_UI_HELPERS_H -#define _ZERO_UI_HELPERS_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "ui.h" - -#define _UI_TEMPORARY_STRING_BUFFER_SIZE 32 -#define _UI_BAR_PROPERTY_VALUE 0 -#define _UI_BAR_PROPERTY_VALUE_WITH_ANIM 1 -void _ui_bar_set_property(lv_obj_t * target, int id, int val); - -#define _UI_BASIC_PROPERTY_POSITION_X 0 -#define _UI_BASIC_PROPERTY_POSITION_Y 1 -#define _UI_BASIC_PROPERTY_WIDTH 2 -#define _UI_BASIC_PROPERTY_HEIGHT 3 -void _ui_basic_set_property(lv_obj_t * target, int id, int val); - -#define _UI_DROPDOWN_PROPERTY_SELECTED 0 -void _ui_dropdown_set_property(lv_obj_t * target, int id, int val); - -#define _UI_IMAGE_PROPERTY_IMAGE 0 -void _ui_image_set_property(lv_obj_t * target, int id, uint8_t * val); - -#define _UI_LABEL_PROPERTY_TEXT 0 -void _ui_label_set_property(lv_obj_t * target, int id, const char * val); - -#define _UI_ROLLER_PROPERTY_SELECTED 0 -#define _UI_ROLLER_PROPERTY_SELECTED_WITH_ANIM 1 -void _ui_roller_set_property(lv_obj_t * target, int id, int val); - -#define _UI_SLIDER_PROPERTY_VALUE 0 -#define _UI_SLIDER_PROPERTY_VALUE_WITH_ANIM 1 -void _ui_slider_set_property(lv_obj_t * target, int id, int val); - -void _ui_screen_change(lv_obj_t ** target, lv_scr_load_anim_t fademode, int spd, int delay, void (*target_init)(void)); - -void _ui_screen_delete(lv_obj_t ** target); - -void _ui_arc_increment(lv_obj_t * target, int val); - -void _ui_bar_increment(lv_obj_t * target, int val, int anm); - -void _ui_slider_increment(lv_obj_t * target, int val, int anm); - -void _ui_keyboard_set_target(lv_obj_t * keyboard, lv_obj_t * textarea); - -#define _UI_MODIFY_FLAG_ADD 0 -#define _UI_MODIFY_FLAG_REMOVE 1 -#define _UI_MODIFY_FLAG_TOGGLE 2 -void _ui_flag_modify(lv_obj_t * target, int32_t flag, int value); - -#define _UI_MODIFY_STATE_ADD 0 -#define _UI_MODIFY_STATE_REMOVE 1 -#define _UI_MODIFY_STATE_TOGGLE 2 -void _ui_state_modify(lv_obj_t * target, int32_t state, int value); - -#define UI_MOVE_CURSOR_UP 0 -#define UI_MOVE_CURSOR_RIGHT 1 -#define UI_MOVE_CURSOR_DOWN 2 -#define UI_MOVE_CURSOR_LEFT 3 -void _ui_textarea_move_cursor(lv_obj_t * target, int val) -; - -void scr_unloaded_delete_cb(lv_event_t * e); - -void _ui_opacity_set(lv_obj_t * target, int val); - -/** Describes an animation*/ -typedef void (*ui_anim_ready_cb_t)(lv_anim_t *); -typedef struct _ui_anim_user_data_t { - lv_obj_t * target; - lv_img_dsc_t ** imgset; - int32_t imgset_size; - int32_t val; - ui_anim_ready_cb_t ready_cb; -} ui_anim_user_data_t; -void _ui_anim_callback_free_user_data(lv_anim_t * a); - -void _ui_anim_callback_free_user_data_and_ready_cb(lv_anim_t * a); - -void _ui_anim_callback_set_x(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_y(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_width(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_height(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_opacity(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_transform_scale(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_image_zoom(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_image_angle(lv_anim_t * a, int32_t v); - -void _ui_anim_callback_set_image_frame(lv_anim_t * a, int32_t v); - -int32_t _ui_anim_callback_get_x(lv_anim_t * a); - -int32_t _ui_anim_callback_get_y(lv_anim_t * a); - -int32_t _ui_anim_callback_get_width(lv_anim_t * a); - -int32_t _ui_anim_callback_get_height(lv_anim_t * a); - -int32_t _ui_anim_callback_get_opacity(lv_anim_t * a); - -int32_t _ui_anim_callback_get_transform_scale(lv_anim_t * a); - -int32_t _ui_anim_callback_get_image_zoom(lv_anim_t * a); - -int32_t _ui_anim_callback_get_image_angle(lv_anim_t * a); - -int32_t _ui_anim_callback_get_image_frame(lv_anim_t * a); - -void _ui_arc_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix); - -void _ui_slider_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix); - -void _ui_checked_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * txt_on, const char * txt_off); - -void _ui_spinbox_step(lv_obj_t * target, int val) -; - -void _ui_switch_theme(int val) -; - -#ifdef __cplusplus -} /*extern "C"*/ -#endif - -#endif diff --git a/projects/APPLaunch/main/ui/ui_obj.h b/projects/APPLaunch/main/ui/ui_obj.h index 909efbf9..0ebfce23 100644 --- a/projects/APPLaunch/main/ui/ui_obj.h +++ b/projects/APPLaunch/main/ui/ui_obj.h @@ -10,6 +10,7 @@ UI_DEFINE_OBJECT(ui_timeLabel) UI_DEFINE_OBJECT(ui_batteryPanel) UI_DEFINE_OBJECT(ui_Bar1) UI_DEFINE_OBJECT(ui_powerLabel) +UI_DEFINE_OBJECT(ui_APP_Container) UI_DEFINE_OBJECT(ui_Panel4) UI_DEFINE_OBJECT(ui_Panel3) UI_DEFINE_OBJECT(ui_Panel5) @@ -126,28 +127,3 @@ UI_DEFINE_OBJECT(ui_leftButton2) UI_DEFINE_OBJECT(ui_Container7) UI_DEFINE_OBJECT(ui_Panel33) UI_DEFINE_OBJECT(ui____initial_actions0) -UI_DEFINE_EVENT_FUN(ui_event_Screen1) -UI_DEFINE_EVENT_FUN(ui_event_leftPanel) -UI_DEFINE_EVENT_FUN(ui_event_switchPanel) -UI_DEFINE_EVENT_FUN(ui_event_rightPanel) -UI_DEFINE_EVENT_FUN(ui_event_rightOuterPanel) -UI_DEFINE_EVENT_FUN(ui_event_leftButton) -UI_DEFINE_EVENT_FUN(ui_event_rightButton) -UI_DEFINE_EVENT_FUN(ui_event_leftOuterPanel) -UI_DEFINE_EVENT_FUN(ui_event_AppStore) -UI_DEFINE_EVENT_FUN(ui_event_Image4) -UI_DEFINE_EVENT_FUN(ui_event_APPNote) -UI_DEFINE_EVENT_FUN(ui_event_Image2) -UI_DEFINE_EVENT_FUN(ui_event_appinstall) -UI_DEFINE_EVENT_FUN(ui_event_appremove) -UI_DEFINE_EVENT_FUN(ui_event_appupdate) -UI_DEFINE_EVENT_FUN(ui_event_clawapp) -UI_DEFINE_EVENT_FUN(ui_event_Image3) -UI_DEFINE_EVENT_FUN(ui_event_Image5) -UI_DEFINE_EVENT_FUN(ui_event_leftPanel2) -UI_DEFINE_EVENT_FUN(ui_event_rightButton2) -UI_DEFINE_EVENT_FUN(ui_event_leftOuterPanel2) -UI_DEFINE_EVENT_FUN(ui_event_rightOuterPanel2) -UI_DEFINE_EVENT_FUN(ui_event_switchPanel2) -UI_DEFINE_EVENT_FUN(ui_event_rightPanel2) -UI_DEFINE_EVENT_FUN(ui_event_leftButton2) diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp new file mode 100644 index 00000000..81ac4098 --- /dev/null +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp @@ -0,0 +1,23 @@ +#include "zero_lvgl_os.h" + +#include "Launch.h" +#include "UILaunchPage.h" + +void zero_lvgl_os::creat_display() +{ + dispp_ = lv_disp_get_default(); + theme_ = lv_theme_default_init(dispp_, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), + false, LV_FONT_DEFAULT); + lv_disp_set_theme(dispp_, theme_); +} + +zero_lvgl_os::zero_lvgl_os() +{ + creat_display(); + launch_ = std::make_shared(); + launch_page_ = std::make_shared(launch_); + launch_->set_launch_page(launch_page_); + +} + +zero_lvgl_os::~zero_lvgl_os() = default; diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.h b/projects/APPLaunch/main/ui/zero_lvgl_os.h new file mode 100644 index 00000000..1f145cf6 --- /dev/null +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.h @@ -0,0 +1,22 @@ +#pragma once + +#include "lvgl/lvgl.h" +#include + +class Launch; +class UILaunchPage; + +class zero_lvgl_os +{ +public: + zero_lvgl_os(); + ~zero_lvgl_os(); + +private: + void creat_display(); + + lv_disp_t *dispp_ = nullptr; + lv_theme_t *theme_ = nullptr; + std::shared_ptr launch_page_; + std::shared_ptr launch_; +}; From 4b61bb546349e92225e1b8c6dc42c9d9c74f4b3f Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 14:36:48 +0800 Subject: [PATCH 23/70] refactor(APPLaunch): abstract app page base layout --- .../APPLaunch/main/ui/components/stb_ds.h | 1895 ----------------- .../main/ui/components/ui_app_page.hpp | 632 ++++-- .../APPLaunch/main/ui/fonts/ui_font_mono13.c | 481 ----- .../APPLaunch/main/ui/fonts/ui_font_mono14.c | 483 ----- .../APPLaunch/main/ui/fonts/ui_font_mono15.c | 484 ----- .../APPLaunch/main/ui/fonts/ui_font_mono20.c | 569 ----- 6 files changed, 396 insertions(+), 4148 deletions(-) delete mode 100644 projects/APPLaunch/main/ui/components/stb_ds.h delete mode 100644 projects/APPLaunch/main/ui/fonts/ui_font_mono13.c delete mode 100644 projects/APPLaunch/main/ui/fonts/ui_font_mono14.c delete mode 100644 projects/APPLaunch/main/ui/fonts/ui_font_mono15.c delete mode 100644 projects/APPLaunch/main/ui/fonts/ui_font_mono20.c diff --git a/projects/APPLaunch/main/ui/components/stb_ds.h b/projects/APPLaunch/main/ui/components/stb_ds.h deleted file mode 100644 index 391e8cff..00000000 --- a/projects/APPLaunch/main/ui/components/stb_ds.h +++ /dev/null @@ -1,1895 +0,0 @@ -/* stb_ds.h - v0.67 - public domain data structures - Sean Barrett 2019 - - This is a single-header-file library that provides easy-to-use - dynamic arrays and hash tables for C (also works in C++). - - For a gentle introduction: - http://nothings.org/stb_ds - - To use this library, do this in *one* C or C++ file: - #define STB_DS_IMPLEMENTATION - #include "stb_ds.h" - -TABLE OF CONTENTS - - Table of Contents - Compile-time options - License - Documentation - Notes - Notes - Dynamic arrays - Notes - Hash maps - Credits - -COMPILE-TIME OPTIONS - - #define STBDS_NO_SHORT_NAMES - - This flag needs to be set globally. - - By default stb_ds exposes shorter function names that are not qualified - with the "stbds_" prefix. If these names conflict with the names in your - code, define this flag. - - #define STBDS_SIPHASH_2_4 - - This flag only needs to be set in the file containing #define STB_DS_IMPLEMENTATION. - - By default stb_ds.h hashes using a weaker variant of SipHash and a custom hash for - 4- and 8-byte keys. On 64-bit platforms, you can define the above flag to force - stb_ds.h to use specification-compliant SipHash-2-4 for all keys. Doing so makes - hash table insertion about 20% slower on 4- and 8-byte keys, 5% slower on - 64-byte keys, and 10% slower on 256-byte keys on my test computer. - - #define STBDS_REALLOC(context,ptr,size) better_realloc - #define STBDS_FREE(context,ptr) better_free - - These defines only need to be set in the file containing #define STB_DS_IMPLEMENTATION. - - By default stb_ds uses stdlib realloc() and free() for memory management. You can - substitute your own functions instead by defining these symbols. You must either - define both, or neither. Note that at the moment, 'context' will always be NULL. - @TODO add an array/hash initialization function that takes a memory context pointer. - - #define STBDS_UNIT_TESTS - - Defines a function stbds_unit_tests() that checks the functioning of the data structures. - - Note that on older versions of gcc (e.g. 5.x.x) you may need to build with '-std=c++0x' - (or equivalentally '-std=c++11') when using anonymous structures as seen on the web - page or in STBDS_UNIT_TESTS. - -LICENSE - - Placed in the public domain and also MIT licensed. - See end of file for detailed license information. - -DOCUMENTATION - - Dynamic Arrays - - Non-function interface: - - Declare an empty dynamic array of type T - T* foo = NULL; - - Access the i'th item of a dynamic array 'foo' of type T, T* foo: - foo[i] - - Functions (actually macros) - - arrfree: - void arrfree(T*); - Frees the array. - - arrlen: - ptrdiff_t arrlen(T*); - Returns the number of elements in the array. - - arrlenu: - size_t arrlenu(T*); - Returns the number of elements in the array as an unsigned type. - - arrpop: - T arrpop(T* a) - Removes the final element of the array and returns it. - - arrput: - T arrput(T* a, T b); - Appends the item b to the end of array a. Returns b. - - arrins: - T arrins(T* a, int p, T b); - Inserts the item b into the middle of array a, into a[p], - moving the rest of the array over. Returns b. - - arrinsn: - void arrinsn(T* a, int p, int n); - Inserts n uninitialized items into array a starting at a[p], - moving the rest of the array over. - - arraddnptr: - T* arraddnptr(T* a, int n) - Appends n uninitialized items onto array at the end. - Returns a pointer to the first uninitialized item added. - - arraddnindex: - size_t arraddnindex(T* a, int n) - Appends n uninitialized items onto array at the end. - Returns the index of the first uninitialized item added. - - arrdel: - void arrdel(T* a, int p); - Deletes the element at a[p], moving the rest of the array over. - - arrdeln: - void arrdeln(T* a, int p, int n); - Deletes n elements starting at a[p], moving the rest of the array over. - - arrdelswap: - void arrdelswap(T* a, int p); - Deletes the element at a[p], replacing it with the element from - the end of the array. O(1) performance. - - arrsetlen: - void arrsetlen(T* a, int n); - Changes the length of the array to n. Allocates uninitialized - slots at the end if necessary. - - arrsetcap: - size_t arrsetcap(T* a, int n); - Sets the length of allocated storage to at least n. It will not - change the length of the array. - - arrcap: - size_t arrcap(T* a); - Returns the number of total elements the array can contain without - needing to be reallocated. - - Hash maps & String hash maps - - Given T is a structure type: struct { TK key; TV value; }. Note that some - functions do not require TV value and can have other fields. For string - hash maps, TK must be 'char *'. - - Special interface: - - stbds_rand_seed: - void stbds_rand_seed(size_t seed); - For security against adversarially chosen data, you should seed the - library with a strong random number. Or at least seed it with time(). - - stbds_hash_string: - size_t stbds_hash_string(char *str, size_t seed); - Returns a hash value for a string. - - stbds_hash_bytes: - size_t stbds_hash_bytes(void *p, size_t len, size_t seed); - These functions hash an arbitrary number of bytes. The function - uses a custom hash for 4- and 8-byte data, and a weakened version - of SipHash for everything else. On 64-bit platforms you can get - specification-compliant SipHash-2-4 on all data by defining - STBDS_SIPHASH_2_4, at a significant cost in speed. - - Non-function interface: - - Declare an empty hash map of type T - T* foo = NULL; - - Access the i'th entry in a hash table T* foo: - foo[i] - - Function interface (actually macros): - - hmfree - shfree - void hmfree(T*); - void shfree(T*); - Frees the hashmap and sets the pointer to NULL. - - hmlen - shlen - ptrdiff_t hmlen(T*) - ptrdiff_t shlen(T*) - Returns the number of elements in the hashmap. - - hmlenu - shlenu - size_t hmlenu(T*) - size_t shlenu(T*) - Returns the number of elements in the hashmap. - - hmgeti - shgeti - hmgeti_ts - ptrdiff_t hmgeti(T*, TK key) - ptrdiff_t shgeti(T*, char* key) - ptrdiff_t hmgeti_ts(T*, TK key, ptrdiff_t tempvar) - Returns the index in the hashmap which has the key 'key', or -1 - if the key is not present. - - hmget - hmget_ts - shget - TV hmget(T*, TK key) - TV shget(T*, char* key) - TV hmget_ts(T*, TK key, ptrdiff_t tempvar) - Returns the value corresponding to 'key' in the hashmap. - The structure must have a 'value' field - - hmgets - shgets - T hmgets(T*, TK key) - T shgets(T*, char* key) - Returns the structure corresponding to 'key' in the hashmap. - - hmgetp - shgetp - hmgetp_ts - hmgetp_null - shgetp_null - T* hmgetp(T*, TK key) - T* shgetp(T*, char* key) - T* hmgetp_ts(T*, TK key, ptrdiff_t tempvar) - T* hmgetp_null(T*, TK key) - T* shgetp_null(T*, char *key) - Returns a pointer to the structure corresponding to 'key' in - the hashmap. Functions ending in "_null" return NULL if the key - is not present in the hashmap; the others return a pointer to a - structure holding the default value (but not the searched-for key). - - hmdefault - shdefault - TV hmdefault(T*, TV value) - TV shdefault(T*, TV value) - Sets the default value for the hashmap, the value which will be - returned by hmget/shget if the key is not present. - - hmdefaults - shdefaults - TV hmdefaults(T*, T item) - TV shdefaults(T*, T item) - Sets the default struct for the hashmap, the contents which will be - returned by hmgets/shgets if the key is not present. - - hmput - shput - TV hmput(T*, TK key, TV value) - TV shput(T*, char* key, TV value) - Inserts a pair into the hashmap. If the key is already - present in the hashmap, updates its value. - - hmputs - shputs - T hmputs(T*, T item) - T shputs(T*, T item) - Inserts a struct with T.key into the hashmap. If the struct is already - present in the hashmap, updates it. - - hmdel - shdel - int hmdel(T*, TK key) - int shdel(T*, char* key) - If 'key' is in the hashmap, deletes its entry and returns 1. - Otherwise returns 0. - - Function interface (actually macros) for strings only: - - sh_new_strdup - void sh_new_strdup(T*); - Overwrites the existing pointer with a newly allocated - string hashmap which will automatically allocate and free - each string key using realloc/free - - sh_new_arena - void sh_new_arena(T*); - Overwrites the existing pointer with a newly allocated - string hashmap which will automatically allocate each string - key to a string arena. Every string key ever used by this - hash table remains in the arena until the arena is freed. - Additionally, any key which is deleted and reinserted will - be allocated multiple times in the string arena. - -NOTES - - * These data structures are realloc'd when they grow, and the macro - "functions" write to the provided pointer. This means: (a) the pointer - must be an lvalue, and (b) the pointer to the data structure is not - stable, and you must maintain it the same as you would a realloc'd - pointer. For example, if you pass a pointer to a dynamic array to a - function which updates it, the function must return back the new - pointer to the caller. This is the price of trying to do this in C. - - * The following are the only functions that are thread-safe on a single data - structure, i.e. can be run in multiple threads simultaneously on the same - data structure - hmlen shlen - hmlenu shlenu - hmget_ts shget_ts - hmgeti_ts shgeti_ts - hmgets_ts shgets_ts - - * You iterate over the contents of a dynamic array and a hashmap in exactly - the same way, using arrlen/hmlen/shlen: - - for (i=0; i < arrlen(foo); ++i) - ... foo[i] ... - - * All operations except arrins/arrdel are O(1) amortized, but individual - operations can be slow, so these data structures may not be suitable - for real time use. Dynamic arrays double in capacity as needed, so - elements are copied an average of once. Hash tables double/halve - their size as needed, with appropriate hysteresis to maintain O(1) - performance. - -NOTES - DYNAMIC ARRAY - - * If you know how long a dynamic array is going to be in advance, you can avoid - extra memory allocations by using arrsetlen to allocate it to that length in - advance and use foo[n] while filling it out, or arrsetcap to allocate the memory - for that length and use arrput/arrpush as normal. - - * Unlike some other versions of the dynamic array, this version should - be safe to use with strict-aliasing optimizations. - -NOTES - HASH MAP - - * For compilers other than GCC and clang (e.g. Visual Studio), for hmput/hmget/hmdel - and variants, the key must be an lvalue (so the macro can take the address of it). - Extensions are used that eliminate this requirement if you're using C99 and later - in GCC or clang, or if you're using C++ in GCC. But note that this can make your - code less portable. - - * To test for presence of a key in a hashmap, just do 'hmgeti(foo,key) >= 0'. - - * The iteration order of your data in the hashmap is determined solely by the - order of insertions and deletions. In particular, if you never delete, new - keys are always added at the end of the array. This will be consistent - across all platforms and versions of the library. However, you should not - attempt to serialize the internal hash table, as the hash is not consistent - between different platforms, and may change with future versions of the library. - - * Use sh_new_arena() for string hashmaps that you never delete from. Initialize - with NULL if you're managing the memory for your strings, or your strings are - never freed (at least until the hashmap is freed). Otherwise, use sh_new_strdup(). - @TODO: make an arena variant that garbage collects the strings with a trivial - copy collector into a new arena whenever the table shrinks / rebuilds. Since - current arena recommendation is to only use arena if it never deletes, then - this can just replace current arena implementation. - - * If adversarial input is a serious concern and you're on a 64-bit platform, - enable STBDS_SIPHASH_2_4 (see the 'Compile-time options' section), and pass - a strong random number to stbds_rand_seed. - - * The default value for the hash table is stored in foo[-1], so if you - use code like 'hmget(T,k)->value = 5' you can accidentally overwrite - the value stored by hmdefault if 'k' is not present. - -CREDITS - - Sean Barrett -- library, idea for dynamic array API/implementation - Per Vognsen -- idea for hash table API/implementation - Rafael Sachetto -- arrpop() - github:HeroicKatora -- arraddn() reworking - - Bugfixes: - Andy Durdin - Shane Liesegang - Vinh Truong - Andreas Molzer - github:hashitaku - github:srdjanstipic - Macoy Madson - Andreas Vennstrom - Tobias Mansfield-Williams -*/ - -#ifdef STBDS_UNIT_TESTS -#define _CRT_SECURE_NO_WARNINGS -#endif - -#ifndef INCLUDE_STB_DS_H -#define INCLUDE_STB_DS_H - -#include -#include - -#ifndef STBDS_NO_SHORT_NAMES -#define arrlen stbds_arrlen -#define arrlenu stbds_arrlenu -#define arrput stbds_arrput -#define arrpush stbds_arrput -#define arrpop stbds_arrpop -#define arrfree stbds_arrfree -#define arraddn stbds_arraddn // deprecated, use one of the following instead: -#define arraddnptr stbds_arraddnptr -#define arraddnindex stbds_arraddnindex -#define arrsetlen stbds_arrsetlen -#define arrlast stbds_arrlast -#define arrins stbds_arrins -#define arrinsn stbds_arrinsn -#define arrdel stbds_arrdel -#define arrdeln stbds_arrdeln -#define arrdelswap stbds_arrdelswap -#define arrcap stbds_arrcap -#define arrsetcap stbds_arrsetcap - -#define hmput stbds_hmput -#define hmputs stbds_hmputs -#define hmget stbds_hmget -#define hmget_ts stbds_hmget_ts -#define hmgets stbds_hmgets -#define hmgetp stbds_hmgetp -#define hmgetp_ts stbds_hmgetp_ts -#define hmgetp_null stbds_hmgetp_null -#define hmgeti stbds_hmgeti -#define hmgeti_ts stbds_hmgeti_ts -#define hmdel stbds_hmdel -#define hmlen stbds_hmlen -#define hmlenu stbds_hmlenu -#define hmfree stbds_hmfree -#define hmdefault stbds_hmdefault -#define hmdefaults stbds_hmdefaults - -#define shput stbds_shput -#define shputi stbds_shputi -#define shputs stbds_shputs -#define shget stbds_shget -#define shgeti stbds_shgeti -#define shgets stbds_shgets -#define shgetp stbds_shgetp -#define shgetp_null stbds_shgetp_null -#define shdel stbds_shdel -#define shlen stbds_shlen -#define shlenu stbds_shlenu -#define shfree stbds_shfree -#define shdefault stbds_shdefault -#define shdefaults stbds_shdefaults -#define sh_new_arena stbds_sh_new_arena -#define sh_new_strdup stbds_sh_new_strdup - -#define stralloc stbds_stralloc -#define strreset stbds_strreset -#endif - -#if defined(STBDS_REALLOC) && !defined(STBDS_FREE) || !defined(STBDS_REALLOC) && defined(STBDS_FREE) -#error "You must define both STBDS_REALLOC and STBDS_FREE, or neither." -#endif -#if !defined(STBDS_REALLOC) && !defined(STBDS_FREE) -#include -#define STBDS_REALLOC(c,p,s) realloc(p,s) -#define STBDS_FREE(c,p) free(p) -#endif - -#ifdef _MSC_VER -#define STBDS_NOTUSED(v) (void)(v) -#else -#define STBDS_NOTUSED(v) (void)sizeof(v) -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -// for security against attackers, seed the library with a random number, at least time() but stronger is better -extern void stbds_rand_seed(size_t seed); - -// these are the hash functions used internally if you want to test them or use them for other purposes -extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed); -extern size_t stbds_hash_string(char *str, size_t seed); - -// this is a simple string arena allocator, initialize with e.g. 'stbds_string_arena my_arena={0}'. -typedef struct stbds_string_arena stbds_string_arena; -extern char * stbds_stralloc(stbds_string_arena *a, char *str); -extern void stbds_strreset(stbds_string_arena *a); - -// have to #define STBDS_UNIT_TESTS to call this -extern void stbds_unit_tests(void); - -/////////////// -// -// Everything below here is implementation details -// - -extern void * stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); -extern void stbds_arrfreef(void *a); -extern void stbds_hmfree_func(void *p, size_t elemsize); -extern void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); -extern void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode); -extern void * stbds_hmput_default(void *a, size_t elemsize); -extern void * stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); -extern void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode); -extern void * stbds_shmode_func(size_t elemsize, int mode); - -#ifdef __cplusplus -} -#endif - -#if defined(__GNUC__) || defined(__clang__) -#define STBDS_HAS_TYPEOF -#ifdef __cplusplus -//#define STBDS_HAS_LITERAL_ARRAY // this is currently broken for clang -#endif -#endif - -#if !defined(__cplusplus) -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -#define STBDS_HAS_LITERAL_ARRAY -#endif -#endif - -// this macro takes the address of the argument, but on gcc/clang can accept rvalues -#if defined(STBDS_HAS_LITERAL_ARRAY) && defined(STBDS_HAS_TYPEOF) - #if __clang__ - #define STBDS_ADDRESSOF(typevar, value) ((__typeof__(typevar)[1]){value}) // literal array decays to pointer to value - #else - #define STBDS_ADDRESSOF(typevar, value) ((typeof(typevar)[1]){value}) // literal array decays to pointer to value - #endif -#else -#define STBDS_ADDRESSOF(typevar, value) &(value) -#endif - -#define STBDS_OFFSETOF(var,field) ((char *) &(var)->field - (char *) (var)) - -#define stbds_header(t) ((stbds_array_header *) (t) - 1) -#define stbds_temp(t) stbds_header(t)->temp -#define stbds_temp_key(t) (*(char **) stbds_header(t)->hash_table) - -#define stbds_arrsetcap(a,n) (stbds_arrgrow(a,0,n)) -#define stbds_arrsetlen(a,n) ((stbds_arrcap(a) < (size_t) (n) ? stbds_arrsetcap((a),(size_t)(n)),0 : 0), (a) ? stbds_header(a)->length = (size_t) (n) : 0) -#define stbds_arrcap(a) ((a) ? stbds_header(a)->capacity : 0) -#define stbds_arrlen(a) ((a) ? (ptrdiff_t) stbds_header(a)->length : 0) -#define stbds_arrlenu(a) ((a) ? stbds_header(a)->length : 0) -#define stbds_arrput(a,v) (stbds_arrmaybegrow(a,1), (a)[stbds_header(a)->length++] = (v)) -#define stbds_arrpush stbds_arrput // synonym -#define stbds_arrpop(a) (stbds_header(a)->length--, (a)[stbds_header(a)->length]) -#define stbds_arraddn(a,n) ((void)(stbds_arraddnindex(a, n))) // deprecated, use one of the following instead: -#define stbds_arraddnptr(a,n) (stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), &(a)[stbds_header(a)->length-(n)]) : (a)) -#define stbds_arraddnindex(a,n)(stbds_arrmaybegrow(a,n), (n) ? (stbds_header(a)->length += (n), stbds_header(a)->length-(n)) : stbds_arrlen(a)) -#define stbds_arraddnoff stbds_arraddnindex -#define stbds_arrlast(a) ((a)[stbds_header(a)->length-1]) -#define stbds_arrfree(a) ((void) ((a) ? STBDS_FREE(NULL,stbds_header(a)) : (void)0), (a)=NULL) -#define stbds_arrdel(a,i) stbds_arrdeln(a,i,1) -#define stbds_arrdeln(a,i,n) (memmove(&(a)[i], &(a)[(i)+(n)], sizeof *(a) * (stbds_header(a)->length-(n)-(i))), stbds_header(a)->length -= (n)) -#define stbds_arrdelswap(a,i) ((a)[i] = stbds_arrlast(a), stbds_header(a)->length -= 1) -#define stbds_arrinsn(a,i,n) (stbds_arraddn((a),(n)), memmove(&(a)[(i)+(n)], &(a)[i], sizeof *(a) * (stbds_header(a)->length-(n)-(i)))) -#define stbds_arrins(a,i,v) (stbds_arrinsn((a),(i),1), (a)[i]=(v)) - -#define stbds_arrmaybegrow(a,n) ((!(a) || stbds_header(a)->length + (n) > stbds_header(a)->capacity) \ - ? (stbds_arrgrow(a,n,0),0) : 0) - -#define stbds_arrgrow(a,b,c) ((a) = stbds_arrgrowf_wrapper((a), sizeof *(a), (b), (c))) - -#define stbds_hmput(t, k, v) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, 0), \ - (t)[stbds_temp((t)-1)].key = (k), \ - (t)[stbds_temp((t)-1)].value = (v)) - -#define stbds_hmputs(t, s) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), &(s).key, sizeof (s).key, STBDS_HM_BINARY), \ - (t)[stbds_temp((t)-1)] = (s)) - -#define stbds_hmgeti(t,k) \ - ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_HM_BINARY), \ - stbds_temp((t)-1)) - -#define stbds_hmgeti_ts(t,k,temp) \ - ((t) = stbds_hmget_key_ts_wrapper((t), sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, &(temp), STBDS_HM_BINARY), \ - (temp)) - -#define stbds_hmgetp(t, k) \ - ((void) stbds_hmgeti(t,k), &(t)[stbds_temp((t)-1)]) - -#define stbds_hmgetp_ts(t, k, temp) \ - ((void) stbds_hmgeti_ts(t,k,temp), &(t)[temp]) - -#define stbds_hmdel(t,k) \ - (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) STBDS_ADDRESSOF((t)->key, (k)), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_BINARY)),(t)?stbds_temp((t)-1):0) - -#define stbds_hmdefault(t, v) \ - ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1].value = (v)) - -#define stbds_hmdefaults(t, s) \ - ((t) = stbds_hmput_default_wrapper((t), sizeof *(t)), (t)[-1] = (s)) - -#define stbds_hmfree(p) \ - ((void) ((p) != NULL ? stbds_hmfree_func((p)-1,sizeof*(p)),0 : 0),(p)=NULL) - -#define stbds_hmgets(t, k) (*stbds_hmgetp(t,k)) -#define stbds_hmget(t, k) (stbds_hmgetp(t,k)->value) -#define stbds_hmget_ts(t, k, temp) (stbds_hmgetp_ts(t,k,temp)->value) -#define stbds_hmlen(t) ((t) ? (ptrdiff_t) stbds_header((t)-1)->length-1 : 0) -#define stbds_hmlenu(t) ((t) ? stbds_header((t)-1)->length-1 : 0) -#define stbds_hmgetp_null(t,k) (stbds_hmgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) - -#define stbds_shput(t, k, v) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ - (t)[stbds_temp((t)-1)].value = (v)) - -#define stbds_shputi(t, k, v) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ - (t)[stbds_temp((t)-1)].value = (v), stbds_temp((t)-1)) - -#define stbds_shputs(t, s) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (s).key, sizeof (s).key, STBDS_HM_STRING), \ - (t)[stbds_temp((t)-1)] = (s), \ - (t)[stbds_temp((t)-1)].key = stbds_temp_key((t)-1)) // above line overwrites whole structure, so must rewrite key here if it was allocated internally - -#define stbds_pshput(t, p) \ - ((t) = stbds_hmput_key_wrapper((t), sizeof *(t), (void*) (p)->key, sizeof (p)->key, STBDS_HM_PTR_TO_STRING), \ - (t)[stbds_temp((t)-1)] = (p)) - -#define stbds_shgeti(t,k) \ - ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_HM_STRING), \ - stbds_temp((t)-1)) - -#define stbds_pshgeti(t,k) \ - ((t) = stbds_hmget_key_wrapper((t), sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_HM_PTR_TO_STRING), \ - stbds_temp((t)-1)) - -#define stbds_shgetp(t, k) \ - ((void) stbds_shgeti(t,k), &(t)[stbds_temp((t)-1)]) - -#define stbds_pshget(t, k) \ - ((void) stbds_pshgeti(t,k), (t)[stbds_temp((t)-1)]) - -#define stbds_shdel(t,k) \ - (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (t)->key, STBDS_OFFSETOF((t),key), STBDS_HM_STRING)),(t)?stbds_temp((t)-1):0) -#define stbds_pshdel(t,k) \ - (((t) = stbds_hmdel_key_wrapper((t),sizeof *(t), (void*) (k), sizeof (*(t))->key, STBDS_OFFSETOF(*(t),key), STBDS_HM_PTR_TO_STRING)),(t)?stbds_temp((t)-1):0) - -#define stbds_sh_new_arena(t) \ - ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_ARENA)) -#define stbds_sh_new_strdup(t) \ - ((t) = stbds_shmode_func_wrapper(t, sizeof *(t), STBDS_SH_STRDUP)) - -#define stbds_shdefault(t, v) stbds_hmdefault(t,v) -#define stbds_shdefaults(t, s) stbds_hmdefaults(t,s) - -#define stbds_shfree stbds_hmfree -#define stbds_shlenu stbds_hmlenu - -#define stbds_shgets(t, k) (*stbds_shgetp(t,k)) -#define stbds_shget(t, k) (stbds_shgetp(t,k)->value) -#define stbds_shgetp_null(t,k) (stbds_shgeti(t,k) == -1 ? NULL : &(t)[stbds_temp((t)-1)]) -#define stbds_shlen stbds_hmlen - -typedef struct -{ - size_t length; - size_t capacity; - void * hash_table; - ptrdiff_t temp; -} stbds_array_header; - -typedef struct stbds_string_block -{ - struct stbds_string_block *next; - char storage[8]; -} stbds_string_block; - -struct stbds_string_arena -{ - stbds_string_block *storage; - size_t remaining; - unsigned char block; - unsigned char mode; // this isn't used by the string arena itself -}; - -#define STBDS_HM_BINARY 0 -#define STBDS_HM_STRING 1 - -enum -{ - STBDS_SH_NONE, - STBDS_SH_DEFAULT, - STBDS_SH_STRDUP, - STBDS_SH_ARENA -}; - -#ifdef __cplusplus -// in C we use implicit assignment from these void*-returning functions to T*. -// in C++ these templates make the same code work -template static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap) { - return (T*)stbds_arrgrowf((void *)a, elemsize, addlen, min_cap); -} -template static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { - return (T*)stbds_hmget_key((void*)a, elemsize, key, keysize, mode); -} -template static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) { - return (T*)stbds_hmget_key_ts((void*)a, elemsize, key, keysize, temp, mode); -} -template static T * stbds_hmput_default_wrapper(T *a, size_t elemsize) { - return (T*)stbds_hmput_default((void *)a, elemsize); -} -template static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) { - return (T*)stbds_hmput_key((void*)a, elemsize, key, keysize, mode); -} -template static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode){ - return (T*)stbds_hmdel_key((void*)a, elemsize, key, keysize, keyoffset, mode); -} -template static T * stbds_shmode_func_wrapper(T *, size_t elemsize, int mode) { - return (T*)stbds_shmode_func(elemsize, mode); -} -#else -#define stbds_arrgrowf_wrapper stbds_arrgrowf -#define stbds_hmget_key_wrapper stbds_hmget_key -#define stbds_hmget_key_ts_wrapper stbds_hmget_key_ts -#define stbds_hmput_default_wrapper stbds_hmput_default -#define stbds_hmput_key_wrapper stbds_hmput_key -#define stbds_hmdel_key_wrapper stbds_hmdel_key -#define stbds_shmode_func_wrapper(t,e,m) stbds_shmode_func(e,m) -#endif - -#endif // INCLUDE_STB_DS_H - - -////////////////////////////////////////////////////////////////////////////// -// -// IMPLEMENTATION -// - -#ifdef STB_DS_IMPLEMENTATION -#include -#include - -#ifndef STBDS_ASSERT -#define STBDS_ASSERT_WAS_UNDEFINED -#define STBDS_ASSERT(x) ((void) 0) -#endif - -#ifdef STBDS_STATISTICS -#define STBDS_STATS(x) x -size_t stbds_array_grow; -size_t stbds_hash_grow; -size_t stbds_hash_shrink; -size_t stbds_hash_rebuild; -size_t stbds_hash_probes; -size_t stbds_hash_alloc; -size_t stbds_rehash_probes; -size_t stbds_rehash_items; -#else -#define STBDS_STATS(x) -#endif - -// -// stbds_arr implementation -// - -//int *prev_allocs[65536]; -//int num_prev; - -void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap) -{ - stbds_array_header temp={0}; // force debugging - void *b; - size_t min_len = stbds_arrlen(a) + addlen; - (void) sizeof(temp); - - // compute the minimum capacity needed - if (min_len > min_cap) - min_cap = min_len; - - if (min_cap <= stbds_arrcap(a)) - return a; - - // increase needed capacity to guarantee O(1) amortized - if (min_cap < 2 * stbds_arrcap(a)) - min_cap = 2 * stbds_arrcap(a); - else if (min_cap < 4) - min_cap = 4; - - //if (num_prev < 65536) if (a) prev_allocs[num_prev++] = (int *) ((char *) a+1); - //if (num_prev == 2201) - // num_prev = num_prev; - b = STBDS_REALLOC(NULL, (a) ? stbds_header(a) : 0, elemsize * min_cap + sizeof(stbds_array_header)); - //if (num_prev < 65536) prev_allocs[num_prev++] = (int *) (char *) b; - b = (char *) b + sizeof(stbds_array_header); - if (a == NULL) { - stbds_header(b)->length = 0; - stbds_header(b)->hash_table = 0; - stbds_header(b)->temp = 0; - } else { - STBDS_STATS(++stbds_array_grow); - } - stbds_header(b)->capacity = min_cap; - - return b; -} - -void stbds_arrfreef(void *a) -{ - STBDS_FREE(NULL, stbds_header(a)); -} - -// -// stbds_hm hash table implementation -// - -#ifdef STBDS_INTERNAL_SMALL_BUCKET -#define STBDS_BUCKET_LENGTH 4 -#else -#define STBDS_BUCKET_LENGTH 8 -#endif - -#define STBDS_BUCKET_SHIFT (STBDS_BUCKET_LENGTH == 8 ? 3 : 2) -#define STBDS_BUCKET_MASK (STBDS_BUCKET_LENGTH-1) -#define STBDS_CACHE_LINE_SIZE 64 - -#define STBDS_ALIGN_FWD(n,a) (((n) + (a) - 1) & ~((a)-1)) - -typedef struct -{ - size_t hash [STBDS_BUCKET_LENGTH]; - ptrdiff_t index[STBDS_BUCKET_LENGTH]; -} stbds_hash_bucket; // in 32-bit, this is one 64-byte cache line; in 64-bit, each array is one 64-byte cache line - -typedef struct -{ - char * temp_key; // this MUST be the first field of the hash table - size_t slot_count; - size_t used_count; - size_t used_count_threshold; - size_t used_count_shrink_threshold; - size_t tombstone_count; - size_t tombstone_count_threshold; - size_t seed; - size_t slot_count_log2; - stbds_string_arena string; - stbds_hash_bucket *storage; // not a separate allocation, just 64-byte aligned storage after this struct -} stbds_hash_index; - -#define STBDS_INDEX_EMPTY -1 -#define STBDS_INDEX_DELETED -2 -#define STBDS_INDEX_IN_USE(x) ((x) >= 0) - -#define STBDS_HASH_EMPTY 0 -#define STBDS_HASH_DELETED 1 - -static size_t stbds_hash_seed=0x31415926; - -void stbds_rand_seed(size_t seed) -{ - stbds_hash_seed = seed; -} - -#define stbds_load_32_or_64(var, temp, v32, v64_hi, v64_lo) \ - temp = v64_lo ^ v32, temp <<= 16, temp <<= 16, temp >>= 16, temp >>= 16, /* discard if 32-bit */ \ - var = v64_hi, var <<= 16, var <<= 16, /* discard if 32-bit */ \ - var ^= temp ^ v32 - -#define STBDS_SIZE_T_BITS ((sizeof (size_t)) * 8) - -static size_t stbds_probe_position(size_t hash, size_t slot_count, size_t slot_log2) -{ - size_t pos; - STBDS_NOTUSED(slot_log2); - pos = hash & (slot_count-1); - #ifdef STBDS_INTERNAL_BUCKET_START - pos &= ~STBDS_BUCKET_MASK; - #endif - return pos; -} - -static size_t stbds_log2(size_t slot_count) -{ - size_t n=0; - while (slot_count > 1) { - slot_count >>= 1; - ++n; - } - return n; -} - -static stbds_hash_index *stbds_make_hash_index(size_t slot_count, stbds_hash_index *ot) -{ - stbds_hash_index *t; - t = (stbds_hash_index *) STBDS_REALLOC(NULL,0,(slot_count >> STBDS_BUCKET_SHIFT) * sizeof(stbds_hash_bucket) + sizeof(stbds_hash_index) + STBDS_CACHE_LINE_SIZE-1); - t->storage = (stbds_hash_bucket *) STBDS_ALIGN_FWD((size_t) (t+1), STBDS_CACHE_LINE_SIZE); - t->slot_count = slot_count; - t->slot_count_log2 = stbds_log2(slot_count); - t->tombstone_count = 0; - t->used_count = 0; - - #if 0 // A1 - t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow - t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild - t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink - #elif 1 // A2 - //t->used_count_threshold = slot_count*12/16; // if 12/16th of table is occupied, grow - //t->tombstone_count_threshold = slot_count* 3/16; // if tombstones are 3/16th of table, rebuild - //t->used_count_shrink_threshold = slot_count* 4/16; // if table is only 4/16th full, shrink - - // compute without overflowing - t->used_count_threshold = slot_count - (slot_count>>2); - t->tombstone_count_threshold = (slot_count>>3) + (slot_count>>4); - t->used_count_shrink_threshold = slot_count >> 2; - - #elif 0 // B1 - t->used_count_threshold = slot_count*13/16; // if 13/16th of table is occupied, grow - t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild - t->used_count_shrink_threshold = slot_count* 5/16; // if table is only 5/16th full, shrink - #else // C1 - t->used_count_threshold = slot_count*14/16; // if 14/16th of table is occupied, grow - t->tombstone_count_threshold = slot_count* 2/16; // if tombstones are 2/16th of table, rebuild - t->used_count_shrink_threshold = slot_count* 6/16; // if table is only 6/16th full, shrink - #endif - // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 - // Note that the larger tables have high variance as they were run fewer times - // A1 A2 B1 C1 - // 0.10ms : 0.10ms : 0.10ms : 0.11ms : 2,000 inserts creating 2K table - // 0.96ms : 0.95ms : 0.97ms : 1.04ms : 20,000 inserts creating 20K table - // 14.48ms : 14.46ms : 10.63ms : 11.00ms : 200,000 inserts creating 200K table - // 195.74ms : 196.35ms : 203.69ms : 214.92ms : 2,000,000 inserts creating 2M table - // 2193.88ms : 2209.22ms : 2285.54ms : 2437.17ms : 20,000,000 inserts creating 20M table - // 65.27ms : 53.77ms : 65.33ms : 65.47ms : 500,000 inserts & deletes in 2K table - // 72.78ms : 62.45ms : 71.95ms : 72.85ms : 500,000 inserts & deletes in 20K table - // 89.47ms : 77.72ms : 96.49ms : 96.75ms : 500,000 inserts & deletes in 200K table - // 97.58ms : 98.14ms : 97.18ms : 97.53ms : 500,000 inserts & deletes in 2M table - // 118.61ms : 119.62ms : 120.16ms : 118.86ms : 500,000 inserts & deletes in 20M table - // 192.11ms : 194.39ms : 196.38ms : 195.73ms : 500,000 inserts & deletes in 200M table - - if (slot_count <= STBDS_BUCKET_LENGTH) - t->used_count_shrink_threshold = 0; - // to avoid infinite loop, we need to guarantee that at least one slot is empty and will terminate probes - STBDS_ASSERT(t->used_count_threshold + t->tombstone_count_threshold < t->slot_count); - STBDS_STATS(++stbds_hash_alloc); - if (ot) { - t->string = ot->string; - // reuse old seed so we can reuse old hashes so below "copy out old data" doesn't do any hashing - t->seed = ot->seed; - } else { - size_t a,b,temp; - memset(&t->string, 0, sizeof(t->string)); - t->seed = stbds_hash_seed; - // LCG - // in 32-bit, a = 2147001325 b = 715136305 - // in 64-bit, a = 2862933555777941757 b = 3037000493 - stbds_load_32_or_64(a,temp, 2147001325, 0x27bb2ee6, 0x87b0b0fd); - stbds_load_32_or_64(b,temp, 715136305, 0, 0xb504f32d); - stbds_hash_seed = stbds_hash_seed * a + b; - } - - { - size_t i,j; - for (i=0; i < slot_count >> STBDS_BUCKET_SHIFT; ++i) { - stbds_hash_bucket *b = &t->storage[i]; - for (j=0; j < STBDS_BUCKET_LENGTH; ++j) - b->hash[j] = STBDS_HASH_EMPTY; - for (j=0; j < STBDS_BUCKET_LENGTH; ++j) - b->index[j] = STBDS_INDEX_EMPTY; - } - } - - // copy out the old data, if any - if (ot) { - size_t i,j; - t->used_count = ot->used_count; - for (i=0; i < ot->slot_count >> STBDS_BUCKET_SHIFT; ++i) { - stbds_hash_bucket *ob = &ot->storage[i]; - for (j=0; j < STBDS_BUCKET_LENGTH; ++j) { - if (STBDS_INDEX_IN_USE(ob->index[j])) { - size_t hash = ob->hash[j]; - size_t pos = stbds_probe_position(hash, t->slot_count, t->slot_count_log2); - size_t step = STBDS_BUCKET_LENGTH; - STBDS_STATS(++stbds_rehash_items); - for (;;) { - size_t limit,z; - stbds_hash_bucket *bucket; - bucket = &t->storage[pos >> STBDS_BUCKET_SHIFT]; - STBDS_STATS(++stbds_rehash_probes); - - for (z=pos & STBDS_BUCKET_MASK; z < STBDS_BUCKET_LENGTH; ++z) { - if (bucket->hash[z] == 0) { - bucket->hash[z] = hash; - bucket->index[z] = ob->index[j]; - goto done; - } - } - - limit = pos & STBDS_BUCKET_MASK; - for (z = 0; z < limit; ++z) { - if (bucket->hash[z] == 0) { - bucket->hash[z] = hash; - bucket->index[z] = ob->index[j]; - goto done; - } - } - - pos += step; // quadratic probing - step += STBDS_BUCKET_LENGTH; - pos &= (t->slot_count-1); - } - } - done: - ; - } - } - } - - return t; -} - -#define STBDS_ROTATE_LEFT(val, n) (((val) << (n)) | ((val) >> (STBDS_SIZE_T_BITS - (n)))) -#define STBDS_ROTATE_RIGHT(val, n) (((val) >> (n)) | ((val) << (STBDS_SIZE_T_BITS - (n)))) - -size_t stbds_hash_string(char *str, size_t seed) -{ - size_t hash = seed; - while (*str) - hash = STBDS_ROTATE_LEFT(hash, 9) + (unsigned char) *str++; - - // Thomas Wang 64-to-32 bit mix function, hopefully also works in 32 bits - hash ^= seed; - hash = (~hash) + (hash << 18); - hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,31); - hash = hash * 21; - hash ^= hash ^ STBDS_ROTATE_RIGHT(hash,11); - hash += (hash << 6); - hash ^= STBDS_ROTATE_RIGHT(hash,22); - return hash+seed; -} - -#ifdef STBDS_SIPHASH_2_4 -#define STBDS_SIPHASH_C_ROUNDS 2 -#define STBDS_SIPHASH_D_ROUNDS 4 -typedef int STBDS_SIPHASH_2_4_can_only_be_used_in_64_bit_builds[sizeof(size_t) == 8 ? 1 : -1]; -#endif - -#ifndef STBDS_SIPHASH_C_ROUNDS -#define STBDS_SIPHASH_C_ROUNDS 1 -#endif -#ifndef STBDS_SIPHASH_D_ROUNDS -#define STBDS_SIPHASH_D_ROUNDS 1 -#endif - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable:4127) // conditional expression is constant, for do..while(0) and sizeof()== -#endif - -static size_t stbds_siphash_bytes(void *p, size_t len, size_t seed) -{ - unsigned char *d = (unsigned char *) p; - size_t i,j; - size_t v0,v1,v2,v3, data; - - // hash that works on 32- or 64-bit registers without knowing which we have - // (computes different results on 32-bit and 64-bit platform) - // derived from siphash, but on 32-bit platforms very different as it uses 4 32-bit state not 4 64-bit - v0 = ((((size_t) 0x736f6d65 << 16) << 16) + 0x70736575) ^ seed; - v1 = ((((size_t) 0x646f7261 << 16) << 16) + 0x6e646f6d) ^ ~seed; - v2 = ((((size_t) 0x6c796765 << 16) << 16) + 0x6e657261) ^ seed; - v3 = ((((size_t) 0x74656462 << 16) << 16) + 0x79746573) ^ ~seed; - - #ifdef STBDS_TEST_SIPHASH_2_4 - // hardcoded with key material in the siphash test vectors - v0 ^= 0x0706050403020100ull ^ seed; - v1 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; - v2 ^= 0x0706050403020100ull ^ seed; - v3 ^= 0x0f0e0d0c0b0a0908ull ^ ~seed; - #endif - - #define STBDS_SIPROUND() \ - do { \ - v0 += v1; v1 = STBDS_ROTATE_LEFT(v1, 13); v1 ^= v0; v0 = STBDS_ROTATE_LEFT(v0,STBDS_SIZE_T_BITS/2); \ - v2 += v3; v3 = STBDS_ROTATE_LEFT(v3, 16); v3 ^= v2; \ - v2 += v1; v1 = STBDS_ROTATE_LEFT(v1, 17); v1 ^= v2; v2 = STBDS_ROTATE_LEFT(v2,STBDS_SIZE_T_BITS/2); \ - v0 += v3; v3 = STBDS_ROTATE_LEFT(v3, 21); v3 ^= v0; \ - } while (0) - - for (i=0; i+sizeof(size_t) <= len; i += sizeof(size_t), d += sizeof(size_t)) { - data = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); - data |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // discarded if size_t == 4 - - v3 ^= data; - for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) - STBDS_SIPROUND(); - v0 ^= data; - } - data = len << (STBDS_SIZE_T_BITS-8); - switch (len - i) { - case 7: data |= ((size_t) d[6] << 24) << 24; // fall through - case 6: data |= ((size_t) d[5] << 20) << 20; // fall through - case 5: data |= ((size_t) d[4] << 16) << 16; // fall through - case 4: data |= (d[3] << 24); // fall through - case 3: data |= (d[2] << 16); // fall through - case 2: data |= (d[1] << 8); // fall through - case 1: data |= d[0]; // fall through - case 0: break; - } - v3 ^= data; - for (j=0; j < STBDS_SIPHASH_C_ROUNDS; ++j) - STBDS_SIPROUND(); - v0 ^= data; - v2 ^= 0xff; - for (j=0; j < STBDS_SIPHASH_D_ROUNDS; ++j) - STBDS_SIPROUND(); - -#ifdef STBDS_SIPHASH_2_4 - return v0^v1^v2^v3; -#else - return v1^v2^v3; // slightly stronger since v0^v3 in above cancels out final round operation? I tweeted at the authors of SipHash about this but they didn't reply -#endif -} - -size_t stbds_hash_bytes(void *p, size_t len, size_t seed) -{ -#ifdef STBDS_SIPHASH_2_4 - return stbds_siphash_bytes(p,len,seed); -#else - unsigned char *d = (unsigned char *) p; - - if (len == 4) { - unsigned int hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); - #if 0 - // HASH32-A Bob Jenkin's hash function w/o large constants - hash ^= seed; - hash -= (hash<<6); - hash ^= (hash>>17); - hash -= (hash<<9); - hash ^= seed; - hash ^= (hash<<4); - hash -= (hash<<3); - hash ^= (hash<<10); - hash ^= (hash>>15); - #elif 1 - // HASH32-BB Bob Jenkin's presumably-accidental version of Thomas Wang hash with rotates turned into shifts. - // Note that converting these back to rotates makes it run a lot slower, presumably due to collisions, so I'm - // not really sure what's going on. - hash ^= seed; - hash = (hash ^ 61) ^ (hash >> 16); - hash = hash + (hash << 3); - hash = hash ^ (hash >> 4); - hash = hash * 0x27d4eb2d; - hash ^= seed; - hash = hash ^ (hash >> 15); - #else // HASH32-C - Murmur3 - hash ^= seed; - hash *= 0xcc9e2d51; - hash = (hash << 17) | (hash >> 15); - hash *= 0x1b873593; - hash ^= seed; - hash = (hash << 19) | (hash >> 13); - hash = hash*5 + 0xe6546b64; - hash ^= hash >> 16; - hash *= 0x85ebca6b; - hash ^= seed; - hash ^= hash >> 13; - hash *= 0xc2b2ae35; - hash ^= hash >> 16; - #endif - // Following statistics were measured on a Core i7-6700 @ 4.00Ghz, compiled with clang 7.0.1 -O2 - // Note that the larger tables have high variance as they were run fewer times - // HASH32-A // HASH32-BB // HASH32-C - // 0.10ms // 0.10ms // 0.10ms : 2,000 inserts creating 2K table - // 0.96ms // 0.95ms // 0.99ms : 20,000 inserts creating 20K table - // 14.69ms // 14.43ms // 14.97ms : 200,000 inserts creating 200K table - // 199.99ms // 195.36ms // 202.05ms : 2,000,000 inserts creating 2M table - // 2234.84ms // 2187.74ms // 2240.38ms : 20,000,000 inserts creating 20M table - // 55.68ms // 53.72ms // 57.31ms : 500,000 inserts & deletes in 2K table - // 63.43ms // 61.99ms // 65.73ms : 500,000 inserts & deletes in 20K table - // 80.04ms // 77.96ms // 81.83ms : 500,000 inserts & deletes in 200K table - // 100.42ms // 97.40ms // 102.39ms : 500,000 inserts & deletes in 2M table - // 119.71ms // 120.59ms // 121.63ms : 500,000 inserts & deletes in 20M table - // 185.28ms // 195.15ms // 187.74ms : 500,000 inserts & deletes in 200M table - // 15.58ms // 14.79ms // 15.52ms : 200,000 inserts creating 200K table with varying key spacing - - return (((size_t) hash << 16 << 16) | hash) ^ seed; - } else if (len == 8 && sizeof(size_t) == 8) { - size_t hash = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); - hash |= (size_t) (d[4] | (d[5] << 8) | (d[6] << 16) | (d[7] << 24)) << 16 << 16; // avoid warning if size_t == 4 - hash ^= seed; - hash = (~hash) + (hash << 21); - hash ^= STBDS_ROTATE_RIGHT(hash,24); - hash *= 265; - hash ^= STBDS_ROTATE_RIGHT(hash,14); - hash ^= seed; - hash *= 21; - hash ^= STBDS_ROTATE_RIGHT(hash,28); - hash += (hash << 31); - hash = (~hash) + (hash << 18); - return hash; - } else { - return stbds_siphash_bytes(p,len,seed); - } -#endif -} -#ifdef _MSC_VER -#pragma warning(pop) -#endif - - -static int stbds_is_key_equal(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode, size_t i) -{ - if (mode >= STBDS_HM_STRING) - return 0==strcmp((char *) key, * (char **) ((char *) a + elemsize*i + keyoffset)); - else - return 0==memcmp(key, (char *) a + elemsize*i + keyoffset, keysize); -} - -#define STBDS_HASH_TO_ARR(x,elemsize) ((char*) (x) - (elemsize)) -#define STBDS_ARR_TO_HASH(x,elemsize) ((char*) (x) + (elemsize)) - -#define stbds_hash_table(a) ((stbds_hash_index *) stbds_header(a)->hash_table) - -void stbds_hmfree_func(void *a, size_t elemsize) -{ - if (a == NULL) return; - if (stbds_hash_table(a) != NULL) { - if (stbds_hash_table(a)->string.mode == STBDS_SH_STRDUP) { - size_t i; - // skip 0th element, which is default - for (i=1; i < stbds_header(a)->length; ++i) - STBDS_FREE(NULL, *(char**) ((char *) a + elemsize*i)); - } - stbds_strreset(&stbds_hash_table(a)->string); - } - STBDS_FREE(NULL, stbds_header(a)->hash_table); - STBDS_FREE(NULL, stbds_header(a)); -} - -static ptrdiff_t stbds_hm_find_slot(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) -{ - void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); - stbds_hash_index *table = stbds_hash_table(raw_a); - size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); - size_t step = STBDS_BUCKET_LENGTH; - size_t limit,i; - size_t pos; - stbds_hash_bucket *bucket; - - if (hash < 2) hash += 2; // stored hash values are forbidden from being 0, so we can detect empty slots - - pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); - - for (;;) { - STBDS_STATS(++stbds_hash_probes); - bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; - - // start searching from pos to end of bucket, this should help performance on small hash tables that fit in cache - for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - return (pos & ~STBDS_BUCKET_MASK)+i; - } - } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { - return -1; - } - } - - // search from beginning of bucket to pos - limit = pos & STBDS_BUCKET_MASK; - for (i = 0; i < limit; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - return (pos & ~STBDS_BUCKET_MASK)+i; - } - } else if (bucket->hash[i] == STBDS_HASH_EMPTY) { - return -1; - } - } - - // quadratic probing - pos += step; - step += STBDS_BUCKET_LENGTH; - pos &= (table->slot_count-1); - } - /* NOTREACHED */ -} - -void * stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) -{ - size_t keyoffset = 0; - if (a == NULL) { - // make it non-empty so we can return a temp - a = stbds_arrgrowf(0, elemsize, 0, 1); - stbds_header(a)->length += 1; - memset(a, 0, elemsize); - *temp = STBDS_INDEX_EMPTY; - // adjust a to point after the default element - return STBDS_ARR_TO_HASH(a,elemsize); - } else { - stbds_hash_index *table; - void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); - // adjust a to point to the default element - table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; - if (table == 0) { - *temp = -1; - } else { - ptrdiff_t slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); - if (slot < 0) { - *temp = STBDS_INDEX_EMPTY; - } else { - stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; - *temp = b->index[slot & STBDS_BUCKET_MASK]; - } - } - return a; - } -} - -void * stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) -{ - ptrdiff_t temp; - void *p = stbds_hmget_key_ts(a, elemsize, key, keysize, &temp, mode); - stbds_temp(STBDS_HASH_TO_ARR(p,elemsize)) = temp; - return p; -} - -void * stbds_hmput_default(void *a, size_t elemsize) -{ - // three cases: - // a is NULL <- allocate - // a has a hash table but no entries, because of shmode <- grow - // a has entries <- do nothing - if (a == NULL || stbds_header(STBDS_HASH_TO_ARR(a,elemsize))->length == 0) { - a = stbds_arrgrowf(a ? STBDS_HASH_TO_ARR(a,elemsize) : NULL, elemsize, 0, 1); - stbds_header(a)->length += 1; - memset(a, 0, elemsize); - a=STBDS_ARR_TO_HASH(a,elemsize); - } - return a; -} - -static char *stbds_strdup(char *str); - -void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode) -{ - size_t keyoffset=0; - void *raw_a; - stbds_hash_index *table; - - if (a == NULL) { - a = stbds_arrgrowf(0, elemsize, 0, 1); - memset(a, 0, elemsize); - stbds_header(a)->length += 1; - // adjust a to point AFTER the default element - a = STBDS_ARR_TO_HASH(a,elemsize); - } - - // adjust a to point to the default element - raw_a = a; - a = STBDS_HASH_TO_ARR(a,elemsize); - - table = (stbds_hash_index *) stbds_header(a)->hash_table; - - if (table == NULL || table->used_count >= table->used_count_threshold) { - stbds_hash_index *nt; - size_t slot_count; - - slot_count = (table == NULL) ? STBDS_BUCKET_LENGTH : table->slot_count*2; - nt = stbds_make_hash_index(slot_count, table); - if (table) - STBDS_FREE(NULL, table); - else - nt->string.mode = mode >= STBDS_HM_STRING ? STBDS_SH_DEFAULT : 0; - stbds_header(a)->hash_table = table = nt; - STBDS_STATS(++stbds_hash_grow); - } - - // we iterate hash table explicitly because we want to track if we saw a tombstone - { - size_t hash = mode >= STBDS_HM_STRING ? stbds_hash_string((char*)key,table->seed) : stbds_hash_bytes(key, keysize,table->seed); - size_t step = STBDS_BUCKET_LENGTH; - size_t pos; - ptrdiff_t tombstone = -1; - stbds_hash_bucket *bucket; - - // stored hash values are forbidden from being 0, so we can detect empty slots to early out quickly - if (hash < 2) hash += 2; - - pos = stbds_probe_position(hash, table->slot_count, table->slot_count_log2); - - for (;;) { - size_t limit, i; - STBDS_STATS(++stbds_hash_probes); - bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; - - // start searching from pos to end of bucket - for (i=pos & STBDS_BUCKET_MASK; i < STBDS_BUCKET_LENGTH; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - stbds_temp(a) = bucket->index[i]; - if (mode >= STBDS_HM_STRING) - stbds_temp_key(a) = * (char **) ((char *) raw_a + elemsize*bucket->index[i] + keyoffset); - return STBDS_ARR_TO_HASH(a,elemsize); - } - } else if (bucket->hash[i] == 0) { - pos = (pos & ~STBDS_BUCKET_MASK) + i; - goto found_empty_slot; - } else if (tombstone < 0) { - if (bucket->index[i] == STBDS_INDEX_DELETED) - tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); - } - } - - // search from beginning of bucket to pos - limit = pos & STBDS_BUCKET_MASK; - for (i = 0; i < limit; ++i) { - if (bucket->hash[i] == hash) { - if (stbds_is_key_equal(raw_a, elemsize, key, keysize, keyoffset, mode, bucket->index[i])) { - stbds_temp(a) = bucket->index[i]; - return STBDS_ARR_TO_HASH(a,elemsize); - } - } else if (bucket->hash[i] == 0) { - pos = (pos & ~STBDS_BUCKET_MASK) + i; - goto found_empty_slot; - } else if (tombstone < 0) { - if (bucket->index[i] == STBDS_INDEX_DELETED) - tombstone = (ptrdiff_t) ((pos & ~STBDS_BUCKET_MASK) + i); - } - } - - // quadratic probing - pos += step; - step += STBDS_BUCKET_LENGTH; - pos &= (table->slot_count-1); - } - found_empty_slot: - if (tombstone >= 0) { - pos = tombstone; - --table->tombstone_count; - } - ++table->used_count; - - { - ptrdiff_t i = (ptrdiff_t) stbds_arrlen(a); - // we want to do stbds_arraddn(1), but we can't use the macros since we don't have something of the right type - if ((size_t) i+1 > stbds_arrcap(a)) - *(void **) &a = stbds_arrgrowf(a, elemsize, 1, 0); - raw_a = STBDS_ARR_TO_HASH(a,elemsize); - - STBDS_ASSERT((size_t) i+1 <= stbds_arrcap(a)); - stbds_header(a)->length = i+1; - bucket = &table->storage[pos >> STBDS_BUCKET_SHIFT]; - bucket->hash[pos & STBDS_BUCKET_MASK] = hash; - bucket->index[pos & STBDS_BUCKET_MASK] = i-1; - stbds_temp(a) = i-1; - - switch (table->string.mode) { - case STBDS_SH_STRDUP: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_strdup((char*) key); break; - case STBDS_SH_ARENA: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = stbds_stralloc(&table->string, (char*)key); break; - case STBDS_SH_DEFAULT: stbds_temp_key(a) = *(char **) ((char *) a + elemsize*i) = (char *) key; break; - default: memcpy((char *) a + elemsize*i, key, keysize); break; - } - } - return STBDS_ARR_TO_HASH(a,elemsize); - } -} - -void * stbds_shmode_func(size_t elemsize, int mode) -{ - void *a = stbds_arrgrowf(0, elemsize, 0, 1); - stbds_hash_index *h; - memset(a, 0, elemsize); - stbds_header(a)->length = 1; - stbds_header(a)->hash_table = h = (stbds_hash_index *) stbds_make_hash_index(STBDS_BUCKET_LENGTH, NULL); - h->string.mode = (unsigned char) mode; - return STBDS_ARR_TO_HASH(a,elemsize); -} - -void * stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode) -{ - if (a == NULL) { - return 0; - } else { - stbds_hash_index *table; - void *raw_a = STBDS_HASH_TO_ARR(a,elemsize); - table = (stbds_hash_index *) stbds_header(raw_a)->hash_table; - stbds_temp(raw_a) = 0; - if (table == 0) { - return a; - } else { - ptrdiff_t slot; - slot = stbds_hm_find_slot(a, elemsize, key, keysize, keyoffset, mode); - if (slot < 0) - return a; - else { - stbds_hash_bucket *b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; - int i = slot & STBDS_BUCKET_MASK; - ptrdiff_t old_index = b->index[i]; - ptrdiff_t final_index = (ptrdiff_t) stbds_arrlen(raw_a)-1-1; // minus one for the raw_a vs a, and minus one for 'last' - STBDS_ASSERT(slot < (ptrdiff_t) table->slot_count); - --table->used_count; - ++table->tombstone_count; - stbds_temp(raw_a) = 1; - STBDS_ASSERT(table->used_count >= 0); - //STBDS_ASSERT(table->tombstone_count < table->slot_count/4); - b->hash[i] = STBDS_HASH_DELETED; - b->index[i] = STBDS_INDEX_DELETED; - - if (mode == STBDS_HM_STRING && table->string.mode == STBDS_SH_STRDUP) - STBDS_FREE(NULL, *(char**) ((char *) a+elemsize*old_index)); - - // if indices are the same, memcpy is a no-op, but back-pointer-fixup will fail, so skip - if (old_index != final_index) { - // swap delete - memmove((char*) a + elemsize*old_index, (char*) a + elemsize*final_index, elemsize); - - // now find the slot for the last element - if (mode == STBDS_HM_STRING) - slot = stbds_hm_find_slot(a, elemsize, *(char**) ((char *) a+elemsize*old_index + keyoffset), keysize, keyoffset, mode); - else - slot = stbds_hm_find_slot(a, elemsize, (char* ) a+elemsize*old_index + keyoffset, keysize, keyoffset, mode); - STBDS_ASSERT(slot >= 0); - b = &table->storage[slot >> STBDS_BUCKET_SHIFT]; - i = slot & STBDS_BUCKET_MASK; - STBDS_ASSERT(b->index[i] == final_index); - b->index[i] = old_index; - } - stbds_header(raw_a)->length -= 1; - - if (table->used_count < table->used_count_shrink_threshold && table->slot_count > STBDS_BUCKET_LENGTH) { - stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count>>1, table); - STBDS_FREE(NULL, table); - STBDS_STATS(++stbds_hash_shrink); - } else if (table->tombstone_count > table->tombstone_count_threshold) { - stbds_header(raw_a)->hash_table = stbds_make_hash_index(table->slot_count , table); - STBDS_FREE(NULL, table); - STBDS_STATS(++stbds_hash_rebuild); - } - - return a; - } - } - } - /* NOTREACHED */ -} - -static char *stbds_strdup(char *str) -{ - // to keep replaceable allocator simple, we don't want to use strdup. - // rolling our own also avoids problem of strdup vs _strdup - size_t len = strlen(str)+1; - char *p = (char*) STBDS_REALLOC(NULL, 0, len); - memmove(p, str, len); - return p; -} - -#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MIN -#define STBDS_STRING_ARENA_BLOCKSIZE_MIN 512u -#endif -#ifndef STBDS_STRING_ARENA_BLOCKSIZE_MAX -#define STBDS_STRING_ARENA_BLOCKSIZE_MAX (1u<<20) -#endif - -char *stbds_stralloc(stbds_string_arena *a, char *str) -{ - char *p; - size_t len = strlen(str)+1; - if (len > a->remaining) { - // compute the next blocksize - size_t blocksize = a->block; - - // size is 512, 512, 1024, 1024, 2048, 2048, 4096, 4096, etc., so that - // there are log(SIZE) allocations to free when we destroy the table - blocksize = (size_t) (STBDS_STRING_ARENA_BLOCKSIZE_MIN) << (blocksize>>1); - - // if size is under 1M, advance to next blocktype - if (blocksize < (size_t)(STBDS_STRING_ARENA_BLOCKSIZE_MAX)) - ++a->block; - - if (len > blocksize) { - // if string is larger than blocksize, then just allocate the full size. - // note that we still advance string_block so block size will continue - // increasing, so e.g. if somebody only calls this with 1000-long strings, - // eventually the arena will start doubling and handling those as well - stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + len); - memmove(sb->storage, str, len); - if (a->storage) { - // insert it after the first element, so that we don't waste the space there - sb->next = a->storage->next; - a->storage->next = sb; - } else { - sb->next = 0; - a->storage = sb; - a->remaining = 0; // this is redundant, but good for clarity - } - return sb->storage; - } else { - stbds_string_block *sb = (stbds_string_block *) STBDS_REALLOC(NULL, 0, sizeof(*sb)-8 + blocksize); - sb->next = a->storage; - a->storage = sb; - a->remaining = blocksize; - } - } - - STBDS_ASSERT(len <= a->remaining); - p = a->storage->storage + a->remaining - len; - a->remaining -= len; - memmove(p, str, len); - return p; -} - -void stbds_strreset(stbds_string_arena *a) -{ - stbds_string_block *x,*y; - x = a->storage; - while (x) { - y = x->next; - STBDS_FREE(NULL, x); - x = y; - } - memset(a, 0, sizeof(*a)); -} - -#endif - -////////////////////////////////////////////////////////////////////////////// -// -// UNIT TESTS -// - -#ifdef STBDS_UNIT_TESTS -#include -#ifdef STBDS_ASSERT_WAS_UNDEFINED -#undef STBDS_ASSERT -#endif -#ifndef STBDS_ASSERT -#define STBDS_ASSERT assert -#include -#endif - -typedef struct { int key,b,c,d; } stbds_struct; -typedef struct { int key[2],b,c,d; } stbds_struct2; - -static char buffer[256]; -char *strkey(int n) -{ -#if defined(_WIN32) && defined(__STDC_WANT_SECURE_LIB__) - sprintf_s(buffer, sizeof(buffer), "test_%d", n); -#else - sprintf(buffer, "test_%d", n); -#endif - return buffer; -} - -void stbds_unit_tests(void) -{ -#if defined(_MSC_VER) && _MSC_VER <= 1200 && defined(__cplusplus) - // VC6 C++ doesn't like the template<> trick on unnamed structures, so do nothing! - STBDS_ASSERT(0); -#else - const int testsize = 100000; - const int testsize2 = testsize/20; - int *arr=NULL; - struct { int key; int value; } *intmap = NULL; - struct { char *key; int value; } *strmap = NULL, s; - struct { stbds_struct key; int value; } *map = NULL; - stbds_struct *map2 = NULL; - stbds_struct2 *map3 = NULL; - stbds_string_arena sa = { 0 }; - int key3[2] = { 1,2 }; - ptrdiff_t temp; - - int i,j; - - STBDS_ASSERT(arrlen(arr)==0); - for (i=0; i < 20000; i += 50) { - for (j=0; j < i; ++j) - arrpush(arr,j); - arrfree(arr); - } - - for (i=0; i < 4; ++i) { - arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); - arrdel(arr,i); - arrfree(arr); - arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); - arrdelswap(arr,i); - arrfree(arr); - } - - for (i=0; i < 5; ++i) { - arrpush(arr,1); arrpush(arr,2); arrpush(arr,3); arrpush(arr,4); - stbds_arrins(arr,i,5); - STBDS_ASSERT(arr[i] == 5); - if (i < 4) - STBDS_ASSERT(arr[4] == 4); - arrfree(arr); - } - - i = 1; - STBDS_ASSERT(hmgeti(intmap,i) == -1); - hmdefault(intmap, -2); - STBDS_ASSERT(hmgeti(intmap, i) == -1); - STBDS_ASSERT(hmget (intmap, i) == -2); - for (i=0; i < testsize; i+=2) - hmput(intmap, i, i*5); - for (i=0; i < testsize; i+=1) { - if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); - else STBDS_ASSERT(hmget(intmap, i) == i*5); - if (i & 1) STBDS_ASSERT(hmget_ts(intmap, i, temp) == -2 ); - else STBDS_ASSERT(hmget_ts(intmap, i, temp) == i*5); - } - for (i=0; i < testsize; i+=2) - hmput(intmap, i, i*3); - for (i=0; i < testsize; i+=1) - if (i & 1) STBDS_ASSERT(hmget(intmap, i) == -2 ); - else STBDS_ASSERT(hmget(intmap, i) == i*3); - for (i=2; i < testsize; i+=4) - hmdel(intmap, i); // delete half the entries - for (i=0; i < testsize; i+=1) - if (i & 3) STBDS_ASSERT(hmget(intmap, i) == -2 ); - else STBDS_ASSERT(hmget(intmap, i) == i*3); - for (i=0; i < testsize; i+=1) - hmdel(intmap, i); // delete the rest of the entries - for (i=0; i < testsize; i+=1) - STBDS_ASSERT(hmget(intmap, i) == -2 ); - hmfree(intmap); - for (i=0; i < testsize; i+=2) - hmput(intmap, i, i*3); - hmfree(intmap); - - #if defined(__clang__) || defined(__GNUC__) - #ifndef __cplusplus - intmap = NULL; - hmput(intmap, 15, 7); - hmput(intmap, 11, 3); - hmput(intmap, 9, 5); - STBDS_ASSERT(hmget(intmap, 9) == 5); - STBDS_ASSERT(hmget(intmap, 11) == 3); - STBDS_ASSERT(hmget(intmap, 15) == 7); - #endif - #endif - - for (i=0; i < testsize; ++i) - stralloc(&sa, strkey(i)); - strreset(&sa); - - { - s.key = "a", s.value = 1; - shputs(strmap, s); - STBDS_ASSERT(*strmap[0].key == 'a'); - STBDS_ASSERT(strmap[0].key == s.key); - STBDS_ASSERT(strmap[0].value == s.value); - shfree(strmap); - } - - { - s.key = "a", s.value = 1; - sh_new_strdup(strmap); - shputs(strmap, s); - STBDS_ASSERT(*strmap[0].key == 'a'); - STBDS_ASSERT(strmap[0].key != s.key); - STBDS_ASSERT(strmap[0].value == s.value); - shfree(strmap); - } - - { - s.key = "a", s.value = 1; - sh_new_arena(strmap); - shputs(strmap, s); - STBDS_ASSERT(*strmap[0].key == 'a'); - STBDS_ASSERT(strmap[0].key != s.key); - STBDS_ASSERT(strmap[0].value == s.value); - shfree(strmap); - } - - for (j=0; j < 2; ++j) { - STBDS_ASSERT(shgeti(strmap,"foo") == -1); - if (j == 0) - sh_new_strdup(strmap); - else - sh_new_arena(strmap); - STBDS_ASSERT(shgeti(strmap,"foo") == -1); - shdefault(strmap, -2); - STBDS_ASSERT(shgeti(strmap,"foo") == -1); - for (i=0; i < testsize; i+=2) - shput(strmap, strkey(i), i*3); - for (i=0; i < testsize; i+=1) - if (i & 1) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); - else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); - for (i=2; i < testsize; i+=4) - shdel(strmap, strkey(i)); // delete half the entries - for (i=0; i < testsize; i+=1) - if (i & 3) STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); - else STBDS_ASSERT(shget(strmap, strkey(i)) == i*3); - for (i=0; i < testsize; i+=1) - shdel(strmap, strkey(i)); // delete the rest of the entries - for (i=0; i < testsize; i+=1) - STBDS_ASSERT(shget(strmap, strkey(i)) == -2 ); - shfree(strmap); - } - - { - struct { char *key; char value; } *hash = NULL; - char name[4] = "jen"; - shput(hash, "bob" , 'h'); - shput(hash, "sally" , 'e'); - shput(hash, "fred" , 'l'); - shput(hash, "jen" , 'x'); - shput(hash, "doug" , 'o'); - - shput(hash, name , 'l'); - shfree(hash); - } - - for (i=0; i < testsize; i += 2) { - stbds_struct s = { i,i*2,i*3,i*4 }; - hmput(map, s, i*5); - } - - for (i=0; i < testsize; i += 1) { - stbds_struct s = { i,i*2,i*3 ,i*4 }; - stbds_struct t = { i,i*2,i*3+1,i*4 }; - if (i & 1) STBDS_ASSERT(hmget(map, s) == 0); - else STBDS_ASSERT(hmget(map, s) == i*5); - if (i & 1) STBDS_ASSERT(hmget_ts(map, s, temp) == 0); - else STBDS_ASSERT(hmget_ts(map, s, temp) == i*5); - //STBDS_ASSERT(hmget(map, t.key) == 0); - } - - for (i=0; i < testsize; i += 2) { - stbds_struct s = { i,i*2,i*3,i*4 }; - hmputs(map2, s); - } - hmfree(map); - - for (i=0; i < testsize; i += 1) { - stbds_struct s = { i,i*2,i*3,i*4 }; - stbds_struct t = { i,i*2,i*3+1,i*4 }; - if (i & 1) STBDS_ASSERT(hmgets(map2, s.key).d == 0); - else STBDS_ASSERT(hmgets(map2, s.key).d == i*4); - //STBDS_ASSERT(hmgetp(map2, t.key) == 0); - } - hmfree(map2); - - for (i=0; i < testsize; i += 2) { - stbds_struct2 s = { { i,i*2 }, i*3,i*4, i*5 }; - hmputs(map3, s); - } - for (i=0; i < testsize; i += 1) { - stbds_struct2 s = { { i,i*2}, i*3, i*4, i*5 }; - stbds_struct2 t = { { i,i*2}, i*3+1, i*4, i*5 }; - if (i & 1) STBDS_ASSERT(hmgets(map3, s.key).d == 0); - else STBDS_ASSERT(hmgets(map3, s.key).d == i*5); - //STBDS_ASSERT(hmgetp(map3, t.key) == 0); - } -#endif -} -#endif - - -/* ------------------------------------------------------------------------------- -This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------- -ALTERNATIVE A - MIT License -Copyright (c) 2019 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ------------------------------------------------------------------------------- -ALTERNATIVE B - Public Domain (www.unlicense.org) -This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, -commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to -this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------- -*/ \ No newline at end of file diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index b669d4fe..2afcb8ef 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -12,13 +12,13 @@ #include #include #include +#include #include #include #include "cp0_lvgl_app.h" #include "cp0_lvgl_file.hpp" #define APP_CONSOLE_EXIT_EVENT (lv_event_code_t)(LV_EVENT_LAST + 1) - static inline std::string img_path(const char *name) { return cp0_file_path(name); @@ -28,6 +28,225 @@ static inline std::string audio_path(const char *name) return cp0_file_path(name); } +class UIAppTopBar +{ +public: + UIAppTopBar() = default; + explicit UIAppTopBar(std::string title) : title_(std::move(title)) {} + + lv_obj_t *create(lv_obj_t *parent) + { + return create(parent, title_); + } + + lv_obj_t *create(lv_obj_t *parent, const std::string &title) + { + title_ = title; + ui_TOP_Container = lv_obj_create(parent); + lv_obj_remove_style_all(ui_TOP_Container); + lv_obj_set_size(ui_TOP_Container, 320, 20); + lv_obj_set_pos(ui_TOP_Container, 0, 0); + lv_obj_set_align(ui_TOP_Container, LV_ALIGN_TOP_LEFT); + lv_obj_clear_flag(ui_TOP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_set_flex_flow(ui_TOP_Container, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(ui_TOP_Container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(ui_TOP_Container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(ui_TOP_Container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + title_label_ = lv_label_create(ui_TOP_Container); + lv_obj_set_width(title_label_, 110); + lv_obj_set_height(title_label_, 20); + lv_label_set_long_mode(title_label_, LV_LABEL_LONG_DOT); + lv_label_set_text(title_label_, title.c_str()); + lv_obj_set_style_text_color(title_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(title_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(title_label_, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(title_label_, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); + + status_bar_ = lv_obj_create(ui_TOP_Container); + lv_obj_remove_style_all(status_bar_); + lv_obj_set_width(status_bar_, 210); + lv_obj_set_height(status_bar_, 20); + lv_obj_clear_flag(status_bar_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW_REVERSE); + lv_obj_set_flex_align(status_bar_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(status_bar_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(status_bar_, 4, LV_PART_MAIN | LV_STATE_DEFAULT); + + create_battery(status_bar_); + create_time(status_bar_); + create_wifi(status_bar_); + return ui_TOP_Container; + } + + void set_title(const std::string &title) + { + title_ = title; + if (title_label_) + lv_label_set_text(title_label_, title.c_str()); + } + + void update_time() + { + if (!time_label_) + return; + char time_buf[16]; + cp0_time_str(time_buf, sizeof(time_buf)); + lv_label_set_text(time_label_, time_buf); + } + + void update_wifi() + { + cp0_wifi_status_t ws = cp0_wifi_get_status(); + set_wifi_signal(ws.connected ? ws.signal : 0); + } + + void update_battery(const cp0_battery_info_t &bat) + { + if (!bat.valid || !power_label_) + return; + int soc = bat.soc; + if (soc > 100) + soc = 100; + if (soc < 0) + soc = 0; + + char pwr_buf[16]; + snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); + lv_label_set_text(power_label_, pwr_buf); + } + + void update_status() + { + update_time(); + update_wifi(); + } + +private: + std::string title_ = "APP"; + lv_obj_t *ui_TOP_Container = nullptr; + lv_obj_t *title_label_ = nullptr; + lv_obj_t *status_bar_ = nullptr; + lv_obj_t *battery_panel_ = nullptr; + lv_obj_t *power_label_ = nullptr; + lv_obj_t *time_panel_ = nullptr; + lv_obj_t *time_label_ = nullptr; + lv_obj_t *wifi_panel_ = nullptr; + lv_obj_t *wifi_bars_[4] = {}; + + static void clear_panel_style(lv_obj_t *obj) + { + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + } + + void create_battery(lv_obj_t *parent) + { + battery_panel_ = lv_obj_create(parent); + lv_obj_set_size(battery_panel_, 38, 13); + clear_panel_style(battery_panel_); + + power_label_ = lv_label_create(battery_panel_); + lv_obj_set_align(power_label_, LV_ALIGN_CENTER); + lv_label_set_text(power_label_, "100%"); + lv_obj_set_style_text_color(power_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(power_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(power_label_, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + } + + void create_time(lv_obj_t *parent) + { + time_panel_ = lv_obj_create(parent); + lv_obj_set_size(time_panel_, 40, 13); + clear_panel_style(time_panel_); + + time_label_ = lv_label_create(time_panel_); + lv_obj_set_align(time_label_, LV_ALIGN_CENTER); + lv_label_set_text(time_label_, "19:45"); + lv_obj_set_style_text_color(time_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(time_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(time_label_, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + } + + void create_wifi(lv_obj_t *parent) + { + static const int bar_heights[4] = {3, 6, 7, 9}; + + wifi_panel_ = lv_obj_create(parent); + lv_obj_set_size(wifi_panel_, 30, 13); + clear_panel_style(wifi_panel_); + lv_obj_set_flex_flow(wifi_panel_, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(wifi_panel_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_END); + lv_obj_set_style_pad_column(wifi_panel_, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + + for (int i = 0; i < 4; ++i) + { + wifi_bars_[i] = lv_obj_create(wifi_panel_); + lv_obj_remove_style_all(wifi_bars_[i]); + lv_obj_set_size(wifi_bars_[i], 5, bar_heights[i]); + lv_obj_set_style_radius(wifi_bars_[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(wifi_bars_[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(wifi_bars_[i], lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_clear_flag(wifi_bars_[i], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + } + } + + void set_wifi_signal(int signal) + { + static const int thresholds[4] = {1, 30, 60, 80}; + const uint32_t on_color = 0x00CCFF; + const uint32_t off_color = 0x4D4D4D; + + for (int i = 0; i < 4; ++i) + { + if (!wifi_bars_[i]) + continue; + lv_obj_set_style_bg_color(wifi_bars_[i], + lv_color_hex(signal >= thresholds[i] ? on_color : off_color), + LV_PART_MAIN | LV_STATE_DEFAULT); + } + } +}; + +class UIAppContainer +{ +public: + UIAppContainer() = default; + explicit UIAppContainer(int height) : height_(height) {} + + void set_height(int height) + { + height_ = height; + if (ui_APP_Container) + lv_obj_set_height(ui_APP_Container, height_); + } + + lv_obj_t *create(lv_obj_t *parent) + { + ui_APP_Container = lv_obj_create(parent); + lv_obj_remove_style_all(ui_APP_Container); + lv_obj_set_width(ui_APP_Container, 320); + lv_obj_set_height(ui_APP_Container, height_); + lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + return ui_APP_Container; + } + + lv_obj_t *get() const + { + return ui_APP_Container; + } + +private: + int height_ = 150; + lv_obj_t *ui_APP_Container = nullptr; +}; + +using UIAPPCOM = UIAppContainer; + class app_ { @@ -38,6 +257,7 @@ class app_ lv_obj_t *get_ui() { return ui_root; } lv_group_t *get_key_group() { return key_group; } std::function go_back_home; + bool have_bottom = false; public: app_() @@ -45,11 +265,17 @@ class app_ creat_base_UI(); creat_input_group(); } - ~app_() + virtual ~app_() { lv_obj_del(ui_root); + lv_group_delete(key_group); } + template + lv_obj_t *add_bar(Component &&component) + { + return component.create(ui_root); + } private: /* ================================================================== */ @@ -58,7 +284,11 @@ class app_ void creat_base_UI() { ui_root = lv_obj_create(NULL); - lv_obj_clear_flag(ui_root, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_clear_flag(ui_root, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_flex_flow(ui_root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(ui_root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(ui_root, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(ui_root, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_root, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_root, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } @@ -71,6 +301,133 @@ class app_ } }; +class UIAppTopBara : virtual public app_ +{ +public: + UIAppTopBara() + { + top_bar_.set_title(app_name); + add_bar(top_bar_); + UI_bind_event(); + update_status_bar(); + status_timer_ = lv_timer_create(app_status_timer_cb, 5000, this); + } + + virtual ~UIAppTopBara() + { + if (status_timer_) + lv_timer_delete(status_timer_); + } + + void update_status_bar() + { + top_bar_.update_status(); + } + + void update_battery_status(const cp0_battery_info_t &bat) + { + top_bar_.update_battery(bat); + } + + void set_page_title(const std::string &title) + { + app_name = title; + top_bar_.set_title(title); + } + +private: + UIAppTopBar top_bar_; + lv_timer_t *status_timer_ = nullptr; + + static void app_battery_event_cb(lv_event_t *e) + { + UIAppTopBara *self = static_cast(lv_event_get_user_data(e)); + if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) + return; + const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); + if (bat) + self->update_battery_status(*bat); + } + + static void app_status_timer_cb(lv_timer_t *timer) + { + UIAppTopBara *self = static_cast(lv_timer_get_user_data(timer)); + if (self) + self->update_status_bar(); + } + + void UI_bind_event() + { + lv_obj_add_event_cb(ui_root, app_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); + } +}; + +class UIAppAPPBara : virtual public app_ +{ +public: + UIAppAPPBara() + { + refresh(); + ui_APP_Container = add_bar(app_container_); + } + + void refresh() + { + app_container_.set_height(have_bottom ? 130 : 150); + } + + void refash() + { + refresh(); + } + + virtual ~UIAppAPPBara() = default; + + lv_obj_t *ui_APP_Container = nullptr; + +private: + UIAppContainer app_container_; +}; + +class UIAppbottomBara : virtual public app_, virtual public UIAppAPPBara +{ +public: + UIAppbottomBara() + { + have_bottom = true; + refresh(); + + ui_BOTTOM_Container = lv_obj_create(ui_root); + lv_obj_remove_style_all(ui_BOTTOM_Container); + lv_obj_set_width(ui_BOTTOM_Container, 320); + lv_obj_set_height(ui_BOTTOM_Container, 20); + lv_obj_clear_flag(ui_BOTTOM_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + } + + virtual ~UIAppbottomBara() = default; + + lv_obj_t *ui_BOTTOM_Container = nullptr; +}; + +class app_page_base : virtual public UIAppTopBara, virtual public UIAppAPPBara +{ +public: + app_page_base() : app_(), UIAppTopBara(), UIAppAPPBara() + { + } + + virtual ~app_page_base() = default; +}; + +class app_with_bottom_bar_base : virtual public UIAppTopBara, virtual public UIAppAPPBara, virtual public UIAppbottomBara +{ +public: + app_with_bottom_bar_base() : app_(), UIAppTopBara(), UIAppAPPBara(), UIAppbottomBara() + { + } + + virtual ~app_with_bottom_bar_base() = default; +}; class home_base : public app_ { @@ -85,9 +442,8 @@ class home_base : public app_ public: lv_obj_t *ui_APP_Container; - public: - home_base(): app_() + home_base() : app_() { creat_Top_UI(); UI_bind_event(); @@ -96,21 +452,25 @@ class home_base : public app_ } ~home_base() { - if (status_timer_) lv_timer_delete(status_timer_); + if (status_timer_) + lv_timer_delete(status_timer_); } static void home_battery_event_cb(lv_event_t *e) { home_base *self = static_cast(lv_event_get_user_data(e)); - if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) return; + if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) + return; const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); - if (bat) self->update_battery_status(*bat); + if (bat) + self->update_battery_status(*bat); } static void home_status_timer_cb(lv_timer_t *timer) { home_base *self = static_cast(lv_timer_get_user_data(timer)); - if (self) self->update_status_bar(); + if (self) + self->update_status_bar(); } void update_status_bar() @@ -118,23 +478,27 @@ class home_base : public app_ char time_buf[16]; cp0_time_str(time_buf, sizeof(time_buf)); lv_label_set_text(ui_TOP_time_Label, time_buf); - } void update_battery_status(const cp0_battery_info_t &bat) { - if (bat.valid) { + if (bat.valid) + { int soc = bat.soc; - if (soc > 100) soc = 100; - if (soc < 0) soc = 0; + if (soc > 100) + soc = 100; + if (soc < 0) + soc = 0; lv_bar_set_value(ui_TOP_Power, soc, LV_ANIM_ON); char pwr_buf[16]; snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); lv_label_set_text(ui_TOP_power_Label, pwr_buf); uint32_t color = 0x66CC33; - if (soc <= 20) color = 0xE74C3C; - else if (soc <= 50) color = 0xF39C12; + if (soc <= 20) + color = 0xE74C3C; + else if (soc <= 50) + color = 0xF39C12; lv_obj_set_style_bg_color(ui_TOP_Power, lv_color_hex(color), LV_PART_INDICATOR | LV_STATE_DEFAULT); } @@ -148,12 +512,12 @@ class home_base : public app_ { ui_TOP_logo = lv_img_create(ui_root); lv_img_set_src(ui_TOP_logo, ui_img_zero_png); - lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); /// 49 - lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); /// 12 + lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); /// 49 + lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); /// 12 lv_obj_set_x(ui_TOP_logo, 5); lv_obj_set_y(ui_TOP_logo, 5); - lv_obj_add_flag(ui_TOP_logo, LV_OBJ_FLAG_ADV_HITTEST); /// Flags - lv_obj_clear_flag(ui_TOP_logo, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_add_flag(ui_TOP_logo, LV_OBJ_FLAG_ADV_HITTEST); /// Flags + lv_obj_clear_flag(ui_TOP_logo, LV_OBJ_FLAG_SCROLLABLE); /// Flags ui_TOP_time = lv_obj_create(ui_root); lv_obj_set_width(ui_TOP_time, 45); @@ -167,8 +531,8 @@ class home_base : public app_ lv_obj_set_style_bg_img_src(ui_TOP_time, ui_img_time_png, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); ui_TOP_time_Label = lv_label_create(ui_TOP_time); - lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 + lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 + lv_obj_set_height(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 lv_obj_set_align(ui_TOP_time_Label, LV_ALIGN_CENTER); lv_label_set_text(ui_TOP_time_Label, "15:21"); lv_obj_set_style_text_color(ui_TOP_time_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -190,8 +554,8 @@ class home_base : public app_ lv_obj_set_style_bg_opa(ui_TOP_Power, 255, LV_PART_INDICATOR | LV_STATE_DEFAULT); ui_TOP_power_Label = lv_label_create(ui_TOP_Power); - lv_obj_set_width(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 + lv_obj_set_width(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 + lv_obj_set_height(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 lv_obj_set_align(ui_TOP_power_Label, LV_ALIGN_CENTER); lv_label_set_text(ui_TOP_power_Label, "96%"); lv_obj_set_style_text_color(ui_TOP_power_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -204,7 +568,7 @@ class home_base : public app_ lv_obj_set_x(ui_APP_Container, 0); lv_obj_set_y(ui_APP_Container, 10); lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags } void UI_bind_event() @@ -213,224 +577,20 @@ class home_base : public app_ } }; - - -class app_base : public app_ +class tmp_app_bash : public app_page_base { -private: - lv_obj_t *ui_TOP_logo; - lv_obj_t *ui_TOP_time; - lv_obj_t *ui_TOP_time_Label; - lv_obj_t *ui_TOP_SignalStrength; - lv_obj_t *ui_TOP_SignalStrength_one; - lv_obj_t *ui_TOP_SignalStrength_two; - lv_obj_t *ui_TOP_SignalStrength_three; - lv_obj_t *ui_TOP_SignalStrength_four; - lv_obj_t *ui_TOP_Power; - lv_obj_t *ui_TOP_power_Label; - lv_timer_t *status_timer_ = nullptr; - public: - lv_obj_t *ui_APP_Container; - - -public: - app_base(): app_() - { - creat_Top_UI(); - UI_bind_event(); - update_status_bar(); - status_timer_ = lv_timer_create(app_status_timer_cb, 5000, this); - } - ~app_base() - { - if (status_timer_) lv_timer_delete(status_timer_); - } - - static void app_battery_event_cb(lv_event_t *e) - { - app_base *self = static_cast(lv_event_get_user_data(e)); - if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) return; - const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); - if (bat) self->update_battery_status(*bat); - } - - static void app_status_timer_cb(lv_timer_t *timer) - { - app_base *self = static_cast(lv_timer_get_user_data(timer)); - if (self) self->update_status_bar(); - } - - void update_status_bar() + tmp_app_bash() : app_page_base() { - char time_buf[16]; - cp0_time_str(time_buf, sizeof(time_buf)); - lv_label_set_text(ui_TOP_time_Label, time_buf); - - cp0_wifi_status_t ws = cp0_wifi_get_status(); - int sig = ws.connected ? ws.signal : 0; - uint32_t on_color = 0x00CCFF; - uint32_t off_color = 0x4D4D4D; - fprintf(stderr, "[STATUS_BAR] connected=%d sig=%d ssid=%s ip=%s bars=%d%d%d%d\n", - ws.connected, sig, ws.ssid, ws.ip, - sig > 0 ? 1 : 0, sig >= 30 ? 1 : 0, sig >= 60 ? 1 : 0, sig >= 80 ? 1 : 0); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_one, - lv_color_hex(sig > 0 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_two, - lv_color_hex(sig >= 30 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_three, - lv_color_hex(sig >= 60 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_four, - lv_color_hex(sig >= 80 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - } - - void update_battery_status(const cp0_battery_info_t &bat) - { - if (bat.valid) { - int soc = bat.soc; - if (soc > 100) soc = 100; - if (soc < 0) soc = 0; - char pwr_buf[16]; - snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); - lv_label_set_text(ui_TOP_power_Label, pwr_buf); - } } +}; - void set_page_title(const std::string &title) +class app_base : public app_page_base +{ +public: + app_base() : app_page_base() { - lv_label_set_text(ui_TOP_logo, title.c_str()); } -private: - /* ================================================================== */ - /* UI construction */ - /* ================================================================== */ - void creat_Top_UI() - { - ui_TOP_logo = lv_label_create(ui_root); - lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(ui_TOP_logo, 4); - lv_obj_set_y(ui_TOP_logo, 0); - lv_obj_set_align(ui_TOP_logo, LV_ALIGN_TOP_LEFT); - lv_label_set_text(ui_TOP_logo, app_name.c_str()); - lv_obj_set_style_text_color(ui_TOP_logo, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_TOP_logo, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(ui_TOP_logo, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(ui_TOP_logo, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); - - - ui_TOP_time = lv_obj_create(ui_root); - lv_obj_set_width(ui_TOP_time, 40); - lv_obj_set_height(ui_TOP_time, 13); - lv_obj_set_x(ui_TOP_time, 206); - lv_obj_set_y(ui_TOP_time, 3); - lv_obj_clear_flag(ui_TOP_time, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_time, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_time, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_time_Label = lv_label_create(ui_TOP_time); - lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_align(ui_TOP_time_Label, LV_ALIGN_CENTER); - lv_label_set_text(ui_TOP_time_Label, "19:45"); - lv_obj_set_style_text_color(ui_TOP_time_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_TOP_time_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(ui_TOP_time_Label, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_SignalStrength = lv_obj_create(ui_root); - lv_obj_set_width(ui_TOP_SignalStrength, 30); - lv_obj_set_height(ui_TOP_SignalStrength, 13); - lv_obj_set_x(ui_TOP_SignalStrength, 248); - lv_obj_set_y(ui_TOP_SignalStrength, 3); - lv_obj_clear_flag(ui_TOP_SignalStrength, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_SignalStrength, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_SignalStrength, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_SignalStrength, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_SignalStrength_one = lv_obj_create(ui_TOP_SignalStrength); - lv_obj_set_width(ui_TOP_SignalStrength_one, 5); - lv_obj_set_height(ui_TOP_SignalStrength_one, 3); - lv_obj_set_x(ui_TOP_SignalStrength_one, -11); - lv_obj_set_y(ui_TOP_SignalStrength_one, 2); - lv_obj_set_align(ui_TOP_SignalStrength_one, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_TOP_SignalStrength_one, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_SignalStrength_one, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_one, lv_color_hex(0x00CCFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_SignalStrength_one, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_SignalStrength_one, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_SignalStrength_two = lv_obj_create(ui_TOP_SignalStrength); - lv_obj_set_width(ui_TOP_SignalStrength_two, 5); - lv_obj_set_height(ui_TOP_SignalStrength_two, 6); - lv_obj_set_x(ui_TOP_SignalStrength_two, -4); - lv_obj_set_y(ui_TOP_SignalStrength_two, 1); - lv_obj_set_align(ui_TOP_SignalStrength_two, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_TOP_SignalStrength_two, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_SignalStrength_two, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_two, lv_color_hex(0x00CCFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_SignalStrength_two, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_SignalStrength_two, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_SignalStrength_three = lv_obj_create(ui_TOP_SignalStrength); - lv_obj_set_width(ui_TOP_SignalStrength_three, 5); - lv_obj_set_height(ui_TOP_SignalStrength_three, 7); - lv_obj_set_x(ui_TOP_SignalStrength_three, 3); - lv_obj_set_y(ui_TOP_SignalStrength_three, 0); - lv_obj_set_align(ui_TOP_SignalStrength_three, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_TOP_SignalStrength_three, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_SignalStrength_three, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_three, lv_color_hex(0x00CCFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_SignalStrength_three, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_SignalStrength_three, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_SignalStrength_four = lv_obj_create(ui_TOP_SignalStrength); - lv_obj_set_width(ui_TOP_SignalStrength_four, 5); - lv_obj_set_height(ui_TOP_SignalStrength_four, 9); - lv_obj_set_x(ui_TOP_SignalStrength_four, 10); - lv_obj_set_y(ui_TOP_SignalStrength_four, -1); - lv_obj_set_align(ui_TOP_SignalStrength_four, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_TOP_SignalStrength_four, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_SignalStrength_four, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_SignalStrength_four, lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_SignalStrength_four, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_SignalStrength_four, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_Power = lv_obj_create(ui_root); - lv_obj_set_width(ui_TOP_Power, 38); - lv_obj_set_height(ui_TOP_Power, 13); - lv_obj_set_x(ui_TOP_Power, 280); - lv_obj_set_y(ui_TOP_Power, 3); - lv_obj_clear_flag(ui_TOP_Power, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_TOP_Power, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_TOP_Power, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_Power, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_TOP_Power, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_power_Label = lv_label_create(ui_TOP_Power); - lv_obj_set_width(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_align(ui_TOP_power_Label, LV_ALIGN_CENTER); - lv_label_set_text(ui_TOP_power_Label, "100%"); - lv_obj_set_style_text_color(ui_TOP_power_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_TOP_power_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(ui_TOP_power_Label, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_APP_Container = lv_obj_create(ui_root); - lv_obj_remove_style_all(ui_APP_Container); - lv_obj_set_width(ui_APP_Container, 320); - lv_obj_set_height(ui_APP_Container, 150); - lv_obj_set_x(ui_APP_Container, 0); - lv_obj_set_y(ui_APP_Container, 10); - lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - } - - void UI_bind_event() - { - lv_obj_add_event_cb(ui_root, app_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); - } + ~app_base() override = default; }; diff --git a/projects/APPLaunch/main/ui/fonts/ui_font_mono13.c b/projects/APPLaunch/main/ui/fonts/ui_font_mono13.c deleted file mode 100644 index a5ecc69b..00000000 --- a/projects/APPLaunch/main/ui/fonts/ui_font_mono13.c +++ /dev/null @@ -1,481 +0,0 @@ -/******************************************************************************* - * Size: 13 px - * Bpp: 1 - * Opts: --bpp 1 --size 13 --font /home/nihao/SquareLine/assets/UbuntuMono-R.ttf -o /home/nihao/SquareLine/assets/ui_font_mono13.c --format lvgl -r 0x20-0x7f --no-compress --no-prefilter - ******************************************************************************/ - -#include "../ui.h" - -#ifndef UI_FONT_MONO13 -#define UI_FONT_MONO13 1 -#endif - -#if UI_FONT_MONO13 - -/*----------------- - * BITMAPS - *----------------*/ - -/*Store the image of the glyphs*/ -static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { - /* U+0020 " " */ - 0x0, - - /* U+0021 "!" */ - 0xf9, - - /* U+0022 "\"" */ - 0xb6, 0x80, - - /* U+0023 "#" */ - 0x24, 0xaf, 0xca, 0x4b, 0xf5, 0x14, - - /* U+0024 "$" */ - 0x21, 0x1f, 0x8, 0x38, 0x21, 0xf1, 0x8, - - /* U+0025 "%" */ - 0xe6, 0xab, 0x3c, 0x3c, 0xd5, 0x67, - - /* U+0026 "&" */ - 0x72, 0x94, 0xc6, 0xce, 0x6d, - - /* U+0027 "'" */ - 0xe0, - - /* U+0028 "(" */ - 0x26, 0x48, 0x88, 0x88, 0x46, 0x20, - - /* U+0029 ")" */ - 0x46, 0x21, 0x11, 0x11, 0x26, 0x40, - - /* U+002A "*" */ - 0x25, 0x7c, 0xa5, 0x0, - - /* U+002B "+" */ - 0x21, 0x3e, 0x42, 0x0, - - /* U+002C "," */ - 0x6d, 0xc0, - - /* U+002D "-" */ - 0xe0, - - /* U+002E "." */ - 0xf0, - - /* U+002F "/" */ - 0x10, 0x84, 0x42, 0x11, 0x8, 0x44, 0x20, - - /* U+0030 "0" */ - 0x76, 0xe3, 0x58, 0xc7, 0x6e, - - /* U+0031 "1" */ - 0x27, 0x8, 0x42, 0x10, 0x9f, - - /* U+0032 "2" */ - 0x70, 0x42, 0x33, 0x31, 0x1f, - - /* U+0033 "3" */ - 0x70, 0x42, 0x60, 0x84, 0x3e, - - /* U+0034 "4" */ - 0x8, 0x62, 0x92, 0x4b, 0xf0, 0x82, - - /* U+0035 "5" */ - 0x7a, 0x10, 0xe1, 0x84, 0x3e, - - /* U+0036 "6" */ - 0x3a, 0x21, 0xe8, 0xc6, 0x2e, - - /* U+0037 "7" */ - 0xf8, 0xc4, 0x42, 0x31, 0x8, - - /* U+0038 "8" */ - 0x74, 0x62, 0xa7, 0xc6, 0x2e, - - /* U+0039 "9" */ - 0x74, 0x63, 0x17, 0x84, 0x5c, - - /* U+003A ":" */ - 0xf0, 0xf0, - - /* U+003B ";" */ - 0x6c, 0x6, 0xdc, - - /* U+003C "<" */ - 0x9, 0xb0, 0xc1, 0x80, - - /* U+003D "=" */ - 0xf8, 0x3e, - - /* U+003E ">" */ - 0x83, 0x6, 0x6c, 0x0, - - /* U+003F "?" */ - 0xe1, 0x12, 0x44, 0x4, - - /* U+0040 "@" */ - 0x39, 0x18, 0x67, 0xa6, 0x9a, 0x77, 0x40, 0xe0, - - /* U+0041 "A" */ - 0x30, 0xc2, 0x92, 0x49, 0xfc, 0x61, - - /* U+0042 "B" */ - 0xf4, 0x63, 0xe8, 0xc6, 0x3e, - - /* U+0043 "C" */ - 0x3a, 0x21, 0x8, 0x41, 0xf, - - /* U+0044 "D" */ - 0xf4, 0xa3, 0x18, 0xc6, 0x5e, - - /* U+0045 "E" */ - 0xf4, 0x21, 0xe8, 0x42, 0x1f, - - /* U+0046 "F" */ - 0xfc, 0x21, 0xe8, 0x42, 0x10, - - /* U+0047 "G" */ - 0x7a, 0x21, 0x8, 0xc5, 0x2f, - - /* U+0048 "H" */ - 0x8c, 0x63, 0xf8, 0xc6, 0x31, - - /* U+0049 "I" */ - 0xf9, 0x8, 0x42, 0x10, 0x9f, - - /* U+004A "J" */ - 0x78, 0x42, 0x10, 0x84, 0x2e, - - /* U+004B "K" */ - 0x8a, 0x4a, 0x30, 0xe2, 0x49, 0xa2, - - /* U+004C "L" */ - 0x84, 0x21, 0x8, 0x42, 0x1f, - - /* U+004D "M" */ - 0xde, 0xf7, 0xda, 0xc6, 0x31, - - /* U+004E "N" */ - 0x8e, 0x73, 0x5a, 0xce, 0x71, - - /* U+004F "O" */ - 0x7b, 0x28, 0x61, 0x86, 0x1c, 0x9e, - - /* U+0050 "P" */ - 0xf4, 0x63, 0x1f, 0x42, 0x10, - - /* U+0051 "Q" */ - 0x79, 0x28, 0x61, 0x86, 0x18, 0x5e, 0x30, 0x70, - - /* U+0052 "R" */ - 0xf4, 0x63, 0x1f, 0x4a, 0x31, - - /* U+0053 "S" */ - 0x7c, 0x20, 0x83, 0x4, 0x3e, - - /* U+0054 "T" */ - 0xf9, 0x8, 0x42, 0x10, 0x84, - - /* U+0055 "U" */ - 0x8c, 0x63, 0x18, 0xc6, 0x2e, - - /* U+0056 "V" */ - 0x85, 0x14, 0x52, 0x48, 0xa3, 0xc, - - /* U+0057 "W" */ - 0x8c, 0x63, 0x5e, 0xef, 0x7b, - - /* U+0058 "X" */ - 0x45, 0x22, 0x8c, 0x30, 0xa4, 0x91, - - /* U+0059 "Y" */ - 0x44, 0x88, 0xa1, 0x41, 0x2, 0x4, 0x8, - - /* U+005A "Z" */ - 0xf8, 0xc4, 0x42, 0x22, 0x1f, - - /* U+005B "[" */ - 0xf2, 0x49, 0x24, 0x93, 0x80, - - /* U+005C "\\" */ - 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, - - /* U+005D "]" */ - 0xe4, 0x92, 0x49, 0x27, 0x80, - - /* U+005E "^" */ - 0x63, 0xa5, 0x10, - - /* U+005F "_" */ - 0xfc, - - /* U+0060 "`" */ - 0x10, - - /* U+0061 "a" */ - 0x70, 0x5f, 0x18, 0xbc, - - /* U+0062 "b" */ - 0x84, 0x21, 0xe9, 0xc6, 0x33, 0xf0, - - /* U+0063 "c" */ - 0x7e, 0x21, 0xc, 0x3c, - - /* U+0064 "d" */ - 0x8, 0x42, 0xfc, 0xc6, 0x39, 0x78, - - /* U+0065 "e" */ - 0x74, 0x7f, 0xc, 0x3c, - - /* U+0066 "f" */ - 0x3a, 0x11, 0xe4, 0x21, 0x8, 0x40, - - /* U+0067 "g" */ - 0x7e, 0x63, 0x18, 0xbc, 0x3e, - - /* U+0068 "h" */ - 0x84, 0x21, 0xe8, 0xc6, 0x31, 0x88, - - /* U+0069 "i" */ - 0x60, 0x1, 0xc2, 0x10, 0x84, 0x38, - - /* U+006A "j" */ - 0x30, 0xf, 0x11, 0x11, 0x11, 0xe0, - - /* U+006B "k" */ - 0x84, 0x21, 0x3a, 0x62, 0x92, 0x88, - - /* U+006C "l" */ - 0xe1, 0x8, 0x42, 0x10, 0x84, 0x38, - - /* U+006D "m" */ - 0xdd, 0x6b, 0x58, 0xc4, - - /* U+006E "n" */ - 0xf4, 0x63, 0x18, 0xc4, - - /* U+006F "o" */ - 0x74, 0x63, 0x18, 0xb8, - - /* U+0070 "p" */ - 0xf4, 0xe3, 0x19, 0xfa, 0x10, - - /* U+0071 "q" */ - 0x7e, 0x63, 0x18, 0xbc, 0x21, - - /* U+0072 "r" */ - 0xf8, 0x88, 0x88, - - /* U+0073 "s" */ - 0x7c, 0x30, 0x70, 0xf8, - - /* U+0074 "t" */ - 0x42, 0x3e, 0x84, 0x21, 0xf, - - /* U+0075 "u" */ - 0x8c, 0x63, 0x18, 0xbc, - - /* U+0076 "v" */ - 0x45, 0x34, 0x8a, 0x30, 0xc0, - - /* U+0077 "w" */ - 0x86, 0x55, 0x5d, 0x6d, 0x20, - - /* U+0078 "x" */ - 0x4c, 0xa3, 0xc, 0x69, 0x10, - - /* U+0079 "y" */ - 0x45, 0x36, 0x8a, 0x28, 0x41, 0x18, - - /* U+007A "z" */ - 0xf0, 0x88, 0x8c, 0x7c, - - /* U+007B "{" */ - 0x74, 0x44, 0x48, 0x44, 0x44, 0x70, - - /* U+007C "|" */ - 0xff, 0xe0, - - /* U+007D "}" */ - 0xe2, 0x22, 0x21, 0x22, 0x22, 0xe0, - - /* U+007E "~" */ - 0x64, 0x60 -}; - - -/*--------------------- - * GLYPH DESCRIPTION - *--------------------*/ - -static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { - {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 104, .box_w = 1, .box_h = 1, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1, .adv_w = 104, .box_w = 1, .box_h = 8, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 2, .adv_w = 104, .box_w = 3, .box_h = 3, .ofs_x = 2, .ofs_y = 6}, - {.bitmap_index = 4, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 10, .adv_w = 104, .box_w = 5, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 17, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 23, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 28, .adv_w = 104, .box_w = 1, .box_h = 3, .ofs_x = 3, .ofs_y = 6}, - {.bitmap_index = 29, .adv_w = 104, .box_w = 4, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 35, .adv_w = 104, .box_w = 4, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 41, .adv_w = 104, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 45, .adv_w = 104, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 49, .adv_w = 104, .box_w = 3, .box_h = 4, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 51, .adv_w = 104, .box_w = 3, .box_h = 1, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 52, .adv_w = 104, .box_w = 2, .box_h = 2, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 53, .adv_w = 104, .box_w = 5, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 60, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 65, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 70, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 75, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 80, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 86, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 91, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 96, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 101, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 106, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 111, .adv_w = 104, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 113, .adv_w = 104, .box_w = 3, .box_h = 8, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 116, .adv_w = 104, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 120, .adv_w = 104, .box_w = 5, .box_h = 3, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 122, .adv_w = 104, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 126, .adv_w = 104, .box_w = 4, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 130, .adv_w = 104, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 138, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 144, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 149, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 154, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 159, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 164, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 169, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 174, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 179, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 184, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 189, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 195, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 200, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 205, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 210, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 216, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 221, .adv_w = 104, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 229, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 234, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 239, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 244, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 249, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 255, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 260, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 266, .adv_w = 104, .box_w = 7, .box_h = 8, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 273, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 278, .adv_w = 104, .box_w = 3, .box_h = 11, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 283, .adv_w = 104, .box_w = 5, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 290, .adv_w = 104, .box_w = 3, .box_h = 11, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 295, .adv_w = 104, .box_w = 5, .box_h = 4, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 298, .adv_w = 104, .box_w = 6, .box_h = 1, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 299, .adv_w = 104, .box_w = 2, .box_h = 2, .ofs_x = 2, .ofs_y = 7}, - {.bitmap_index = 300, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 304, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 310, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 314, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 320, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 324, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 330, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 335, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 341, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 347, .adv_w = 104, .box_w = 4, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 353, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 359, .adv_w = 104, .box_w = 5, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 365, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 369, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 373, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 377, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 382, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 387, .adv_w = 104, .box_w = 4, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 390, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 394, .adv_w = 104, .box_w = 5, .box_h = 8, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 399, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 403, .adv_w = 104, .box_w = 6, .box_h = 6, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 408, .adv_w = 104, .box_w = 6, .box_h = 6, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 413, .adv_w = 104, .box_w = 6, .box_h = 6, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 418, .adv_w = 104, .box_w = 6, .box_h = 8, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 424, .adv_w = 104, .box_w = 5, .box_h = 6, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 428, .adv_w = 104, .box_w = 4, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 434, .adv_w = 104, .box_w = 1, .box_h = 11, .ofs_x = 3, .ofs_y = -2}, - {.bitmap_index = 436, .adv_w = 104, .box_w = 4, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 442, .adv_w = 104, .box_w = 6, .box_h = 2, .ofs_x = 0, .ofs_y = 2} -}; - -/*--------------------- - * CHARACTER MAPPING - *--------------------*/ - - - -/*Collect the unicode lists and glyph_id offsets*/ -static const lv_font_fmt_txt_cmap_t cmaps[] = -{ - { - .range_start = 32, .range_length = 95, .glyph_id_start = 1, - .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY - } -}; - - - -/*-------------------- - * ALL CUSTOM DATA - *--------------------*/ - -#if LV_VERSION_CHECK(8, 0, 0) -/*Store all the custom data of the font*/ -static lv_font_fmt_txt_glyph_cache_t cache; -static const lv_font_fmt_txt_dsc_t font_dsc = { -#else -static lv_font_fmt_txt_dsc_t font_dsc = { -#endif - .glyph_bitmap = glyph_bitmap, - .glyph_dsc = glyph_dsc, - .cmaps = cmaps, - .kern_dsc = NULL, - .kern_scale = 0, - .cmap_num = 1, - .bpp = 1, - .kern_classes = 0, - .bitmap_format = 0, -#if LV_VERSION_CHECK(8, 0, 0) - .cache = &cache -#endif -}; - - -/*----------------- - * PUBLIC FONT - *----------------*/ - -/*Initialize a public general font descriptor*/ -#if LV_VERSION_CHECK(8, 0, 0) -const lv_font_t ui_font_mono13 = { -#else -lv_font_t ui_font_mono13 = { -#endif - .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ - .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ - .line_height = 11, /*The maximum line height required by the font*/ - .base_line = 2, /*Baseline measured from the bottom of the line*/ -#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) - .subpx = LV_FONT_SUBPX_NONE, -#endif -#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 - .underline_position = -2, - .underline_thickness = 0, -#endif - .dsc = &font_dsc /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */ -}; - - - -#endif /*#if UI_FONT_MONO13*/ - diff --git a/projects/APPLaunch/main/ui/fonts/ui_font_mono14.c b/projects/APPLaunch/main/ui/fonts/ui_font_mono14.c deleted file mode 100644 index 8470627c..00000000 --- a/projects/APPLaunch/main/ui/fonts/ui_font_mono14.c +++ /dev/null @@ -1,483 +0,0 @@ -/******************************************************************************* - * Size: 14 px - * Bpp: 1 - * Opts: --bpp 1 --size 14 --font /home/nihao/SquareLine/assets/UbuntuMono-R.ttf -o /home/nihao/SquareLine/assets/ui_font_mono14.c --format lvgl -r 0x20-0x7f --no-compress --no-prefilter - ******************************************************************************/ - -#include "../ui.h" - -#ifndef UI_FONT_MONO14 -#define UI_FONT_MONO14 1 -#endif - -#if UI_FONT_MONO14 - -/*----------------- - * BITMAPS - *----------------*/ - -/*Store the image of the glyphs*/ -static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { - /* U+0020 " " */ - 0x0, - - /* U+0021 "!" */ - 0xaa, 0xa3, 0xc0, - - /* U+0022 "\"" */ - 0xb6, 0x80, - - /* U+0023 "#" */ - 0x24, 0x4b, 0xf9, 0x42, 0x85, 0x3f, 0xa4, 0x48, - - /* U+0024 "$" */ - 0x21, 0x1f, 0x8, 0x30, 0x41, 0xf, 0x88, 0x40, - - /* U+0025 "%" */ - 0xe7, 0x4a, 0xa7, 0xc1, 0x5, 0x8c, 0xa9, 0xcc, - - /* U+0026 "&" */ - 0x30, 0x91, 0x22, 0x86, 0x52, 0xa3, 0x46, 0x74, - - /* U+0027 "'" */ - 0xf0, - - /* U+0028 "(" */ - 0x36, 0x48, 0x88, 0x88, 0x84, 0x63, - - /* U+0029 ")" */ - 0xc6, 0x23, 0x11, 0x11, 0x12, 0x6c, - - /* U+002A "*" */ - 0x25, 0x7e, 0xa5, 0x0, - - /* U+002B "+" */ - 0x20, 0x8f, 0xc8, 0x20, 0x80, - - /* U+002C "," */ - 0x6c, 0xe0, - - /* U+002D "-" */ - 0xe0, - - /* U+002E "." */ - 0xf0, - - /* U+002F "/" */ - 0x8, 0xc4, 0x23, 0x10, 0x8c, 0x42, 0x31, 0x0, - - /* U+0030 "0" */ - 0x79, 0x28, 0x69, 0xa6, 0x18, 0x52, 0x78, - - /* U+0031 "1" */ - 0x27, 0x28, 0x42, 0x10, 0x84, 0xf8, - - /* U+0032 "2" */ - 0x74, 0x42, 0x11, 0x11, 0x18, 0xf8, - - /* U+0033 "3" */ - 0x70, 0x42, 0x61, 0x84, 0x21, 0xf0, - - /* U+0034 "4" */ - 0x8, 0x62, 0x9a, 0x4a, 0x2f, 0xc2, 0x8, - - /* U+0035 "5" */ - 0x7a, 0x10, 0x83, 0x4, 0x21, 0xf0, - - /* U+0036 "6" */ - 0x3a, 0x21, 0xe8, 0xc6, 0x39, 0x70, - - /* U+0037 "7" */ - 0xfc, 0x30, 0x84, 0x10, 0x82, 0x8, 0x60, - - /* U+0038 "8" */ - 0x7a, 0x18, 0x73, 0x7a, 0x38, 0x61, 0x78, - - /* U+0039 "9" */ - 0x74, 0xe3, 0x17, 0x84, 0x66, 0xe0, - - /* U+003A ":" */ - 0xf0, 0x3c, - - /* U+003B ";" */ - 0x6c, 0x0, 0xda, 0x80, - - /* U+003C "<" */ - 0x0, 0x66, 0x30, 0x30, 0x30, - - /* U+003D "=" */ - 0xfc, 0x0, 0x3f, - - /* U+003E ">" */ - 0x1, 0x81, 0x83, 0x33, 0x0, - - /* U+003F "?" */ - 0xe1, 0x13, 0x24, 0x6, 0x60, - - /* U+0040 "@" */ - 0x39, 0x18, 0x67, 0xa6, 0x9a, 0x69, 0x5d, 0x83, - 0x80, - - /* U+0041 "A" */ - 0x10, 0x50, 0xa1, 0x44, 0xc8, 0x9f, 0x62, 0x82, - - /* U+0042 "B" */ - 0xf4, 0x63, 0x1f, 0x46, 0x31, 0xf0, - - /* U+0043 "C" */ - 0x39, 0x8, 0x20, 0x82, 0x8, 0x10, 0x3c, - - /* U+0044 "D" */ - 0xf2, 0x28, 0x61, 0x86, 0x18, 0x62, 0xf0, - - /* U+0045 "E" */ - 0xfc, 0x21, 0xf, 0x42, 0x10, 0xf8, - - /* U+0046 "F" */ - 0xfc, 0x21, 0xf, 0x42, 0x10, 0x80, - - /* U+0047 "G" */ - 0x3d, 0x18, 0x20, 0x82, 0x18, 0x51, 0x3c, - - /* U+0048 "H" */ - 0x86, 0x18, 0x61, 0xfe, 0x18, 0x61, 0x84, - - /* U+0049 "I" */ - 0xf9, 0x8, 0x42, 0x10, 0x84, 0xf8, - - /* U+004A "J" */ - 0x78, 0x42, 0x10, 0x84, 0x31, 0xf0, - - /* U+004B "K" */ - 0x8a, 0x6b, 0x38, 0xe2, 0xc9, 0x22, 0x8c, - - /* U+004C "L" */ - 0x84, 0x21, 0x8, 0x42, 0x10, 0xf8, - - /* U+004D "M" */ - 0xcf, 0x3c, 0xed, 0xb6, 0xd8, 0x61, 0x84, - - /* U+004E "N" */ - 0x8e, 0x73, 0x5a, 0xd6, 0x73, 0x88, - - /* U+004F "O" */ - 0x79, 0x28, 0x61, 0x86, 0x18, 0x52, 0x78, - - /* U+0050 "P" */ - 0xf4, 0x63, 0x19, 0xfa, 0x10, 0x80, - - /* U+0051 "Q" */ - 0x71, 0x28, 0x61, 0x86, 0x18, 0x63, 0x78, 0xc1, - 0x83, - - /* U+0052 "R" */ - 0xf4, 0x63, 0x1f, 0x4a, 0x51, 0x88, - - /* U+0053 "S" */ - 0x7c, 0x21, 0x87, 0xc, 0x21, 0xf0, - - /* U+0054 "T" */ - 0xfc, 0x82, 0x8, 0x20, 0x82, 0x8, 0x20, - - /* U+0055 "U" */ - 0x86, 0x18, 0x61, 0x86, 0x18, 0x73, 0x78, - - /* U+0056 "V" */ - 0xc6, 0x89, 0x12, 0x24, 0x45, 0xa, 0x14, 0x10, - - /* U+0057 "W" */ - 0x86, 0x18, 0x6d, 0xb6, 0xdc, 0xf3, 0xcc, - - /* U+0058 "X" */ - 0x44, 0xd8, 0xa1, 0xc1, 0x5, 0x1b, 0x22, 0x44, - - /* U+0059 "Y" */ - 0xc6, 0x89, 0x31, 0x42, 0x82, 0x4, 0x8, 0x10, - - /* U+005A "Z" */ - 0xfc, 0x31, 0x84, 0x31, 0x84, 0x30, 0xfc, - - /* U+005B "[" */ - 0xf2, 0x49, 0x24, 0x92, 0x70, - - /* U+005C "\\" */ - 0x86, 0x10, 0x86, 0x10, 0x86, 0x10, 0x86, 0x10, - - /* U+005D "]" */ - 0xe4, 0x92, 0x49, 0x24, 0xf0, - - /* U+005E "^" */ - 0x22, 0x95, 0x18, 0x80, - - /* U+005F "_" */ - 0xfe, - - /* U+0060 "`" */ - 0xc8, - - /* U+0061 "a" */ - 0x70, 0x42, 0xf8, 0xc5, 0xe0, - - /* U+0062 "b" */ - 0x84, 0x21, 0xe9, 0xc6, 0x31, 0x9f, 0x80, - - /* U+0063 "c" */ - 0x7e, 0x21, 0x8, 0x61, 0xe0, - - /* U+0064 "d" */ - 0x8, 0x42, 0xfc, 0xc6, 0x31, 0xcb, 0xc0, - - /* U+0065 "e" */ - 0x7b, 0x38, 0x7f, 0x83, 0x7, 0xc0, - - /* U+0066 "f" */ - 0x3d, 0x4, 0x3e, 0x41, 0x4, 0x10, 0x41, 0x0, - - /* U+0067 "g" */ - 0x7e, 0x63, 0x18, 0xe5, 0xe1, 0xf0, - - /* U+0068 "h" */ - 0x84, 0x21, 0xe8, 0xc6, 0x31, 0x8c, 0x40, - - /* U+0069 "i" */ - 0x63, 0x1, 0xc2, 0x10, 0x84, 0x20, 0xc0, - - /* U+006A "j" */ - 0x33, 0xf, 0x11, 0x11, 0x11, 0x1e, - - /* U+006B "k" */ - 0x82, 0x8, 0x26, 0xb3, 0x8e, 0x2c, 0x92, 0x20, - - /* U+006C "l" */ - 0xe0, 0x82, 0x8, 0x20, 0x82, 0x8, 0x20, 0x60, - - /* U+006D "m" */ - 0xdd, 0x6b, 0x58, 0xc6, 0x20, - - /* U+006E "n" */ - 0xf4, 0x63, 0x18, 0xc6, 0x20, - - /* U+006F "o" */ - 0x7b, 0x38, 0x61, 0x87, 0x37, 0x80, - - /* U+0070 "p" */ - 0xf4, 0xe3, 0x18, 0xcf, 0xd0, 0x80, - - /* U+0071 "q" */ - 0x7e, 0x63, 0x18, 0xe5, 0xe1, 0x8, - - /* U+0072 "r" */ - 0xf8, 0x88, 0x88, 0x80, - - /* U+0073 "s" */ - 0x7c, 0x30, 0xe0, 0x87, 0xc0, - - /* U+0074 "t" */ - 0x42, 0x3e, 0x84, 0x21, 0x8, 0x38, - - /* U+0075 "u" */ - 0x8c, 0x63, 0x18, 0xc5, 0xe0, - - /* U+0076 "v" */ - 0x44, 0x89, 0x11, 0x42, 0x87, 0x4, 0x0, - - /* U+0077 "w" */ - 0x83, 0x5, 0x53, 0xa6, 0xcd, 0x91, 0x0, - - /* U+0078 "x" */ - 0x44, 0x50, 0xe0, 0x82, 0x8d, 0x91, 0x0, - - /* U+0079 "y" */ - 0x45, 0x16, 0x4a, 0x28, 0x61, 0x4, 0x60, - - /* U+007A "z" */ - 0xf8, 0xcc, 0x44, 0x63, 0xe0, - - /* U+007B "{" */ - 0x39, 0x8, 0x42, 0x60, 0x84, 0x21, 0x8, 0x30, - - /* U+007C "|" */ - 0xff, 0xf0, - - /* U+007D "}" */ - 0xe1, 0x8, 0x42, 0xc, 0x84, 0x21, 0x9, 0x80, - - /* U+007E "~" */ - 0xed, 0xc0 -}; - - -/*--------------------- - * GLYPH DESCRIPTION - *--------------------*/ - -static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { - {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 112, .box_w = 1, .box_h = 1, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1, .adv_w = 112, .box_w = 2, .box_h = 9, .ofs_x = 3, .ofs_y = 0}, - {.bitmap_index = 4, .adv_w = 112, .box_w = 3, .box_h = 3, .ofs_x = 2, .ofs_y = 7}, - {.bitmap_index = 6, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 14, .adv_w = 112, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 22, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 30, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 38, .adv_w = 112, .box_w = 1, .box_h = 4, .ofs_x = 3, .ofs_y = 6}, - {.bitmap_index = 39, .adv_w = 112, .box_w = 4, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 45, .adv_w = 112, .box_w = 4, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 51, .adv_w = 112, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 55, .adv_w = 112, .box_w = 6, .box_h = 6, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 60, .adv_w = 112, .box_w = 3, .box_h = 4, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 62, .adv_w = 112, .box_w = 3, .box_h = 1, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 63, .adv_w = 112, .box_w = 2, .box_h = 2, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 64, .adv_w = 112, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 72, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 79, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 85, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 91, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 97, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 104, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 110, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 116, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 123, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 130, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 136, .adv_w = 112, .box_w = 2, .box_h = 7, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 138, .adv_w = 112, .box_w = 3, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 142, .adv_w = 112, .box_w = 6, .box_h = 6, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 147, .adv_w = 112, .box_w = 6, .box_h = 4, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 150, .adv_w = 112, .box_w = 6, .box_h = 6, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 155, .adv_w = 112, .box_w = 4, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 160, .adv_w = 112, .box_w = 6, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 169, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 177, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 183, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 190, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 197, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 203, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 209, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 216, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 223, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 229, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 235, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 242, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 248, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 255, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 261, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 268, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 274, .adv_w = 112, .box_w = 6, .box_h = 12, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 283, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 289, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 295, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 302, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 309, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 317, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 324, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 332, .adv_w = 112, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 340, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 347, .adv_w = 112, .box_w = 3, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 352, .adv_w = 112, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 360, .adv_w = 112, .box_w = 3, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 365, .adv_w = 112, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 369, .adv_w = 112, .box_w = 7, .box_h = 1, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 370, .adv_w = 112, .box_w = 3, .box_h = 2, .ofs_x = 2, .ofs_y = 8}, - {.bitmap_index = 371, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 376, .adv_w = 112, .box_w = 5, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 383, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 388, .adv_w = 112, .box_w = 5, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 395, .adv_w = 112, .box_w = 6, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 401, .adv_w = 112, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 409, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 415, .adv_w = 112, .box_w = 5, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 422, .adv_w = 112, .box_w = 5, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 429, .adv_w = 112, .box_w = 4, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 435, .adv_w = 112, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 443, .adv_w = 112, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 451, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 456, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 461, .adv_w = 112, .box_w = 6, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 467, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 473, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 479, .adv_w = 112, .box_w = 4, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 483, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 488, .adv_w = 112, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 494, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 499, .adv_w = 112, .box_w = 7, .box_h = 7, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 506, .adv_w = 112, .box_w = 7, .box_h = 7, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 513, .adv_w = 112, .box_w = 7, .box_h = 7, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 520, .adv_w = 112, .box_w = 6, .box_h = 9, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 527, .adv_w = 112, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 532, .adv_w = 112, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 540, .adv_w = 112, .box_w = 1, .box_h = 12, .ofs_x = 3, .ofs_y = -2}, - {.bitmap_index = 542, .adv_w = 112, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 550, .adv_w = 112, .box_w = 5, .box_h = 2, .ofs_x = 1, .ofs_y = 3} -}; - -/*--------------------- - * CHARACTER MAPPING - *--------------------*/ - - - -/*Collect the unicode lists and glyph_id offsets*/ -static const lv_font_fmt_txt_cmap_t cmaps[] = -{ - { - .range_start = 32, .range_length = 95, .glyph_id_start = 1, - .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY - } -}; - - - -/*-------------------- - * ALL CUSTOM DATA - *--------------------*/ - -#if LV_VERSION_CHECK(8, 0, 0) -/*Store all the custom data of the font*/ -static lv_font_fmt_txt_glyph_cache_t cache; -static const lv_font_fmt_txt_dsc_t font_dsc = { -#else -static lv_font_fmt_txt_dsc_t font_dsc = { -#endif - .glyph_bitmap = glyph_bitmap, - .glyph_dsc = glyph_dsc, - .cmaps = cmaps, - .kern_dsc = NULL, - .kern_scale = 0, - .cmap_num = 1, - .bpp = 1, - .kern_classes = 0, - .bitmap_format = 0, -#if LV_VERSION_CHECK(8, 0, 0) - .cache = &cache -#endif -}; - - -/*----------------- - * PUBLIC FONT - *----------------*/ - -/*Initialize a public general font descriptor*/ -#if LV_VERSION_CHECK(8, 0, 0) -const lv_font_t ui_font_mono14 = { -#else -lv_font_t ui_font_mono14 = { -#endif - .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ - .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ - .line_height = 13, /*The maximum line height required by the font*/ - .base_line = 3, /*Baseline measured from the bottom of the line*/ -#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) - .subpx = LV_FONT_SUBPX_NONE, -#endif -#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 - .underline_position = -2, - .underline_thickness = 0, -#endif - .dsc = &font_dsc /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */ -}; - - - -#endif /*#if UI_FONT_MONO14*/ - diff --git a/projects/APPLaunch/main/ui/fonts/ui_font_mono15.c b/projects/APPLaunch/main/ui/fonts/ui_font_mono15.c deleted file mode 100644 index 03fca30b..00000000 --- a/projects/APPLaunch/main/ui/fonts/ui_font_mono15.c +++ /dev/null @@ -1,484 +0,0 @@ -/******************************************************************************* - * Size: 15 px - * Bpp: 1 - * Opts: --bpp 1 --size 15 --font /home/nihao/SquareLine/assets/UbuntuMono-R.ttf -o /home/nihao/SquareLine/assets/ui_font_mono15.c --format lvgl -r 0x20-0x7f --no-compress --no-prefilter - ******************************************************************************/ - -#include "../ui.h" - -#ifndef UI_FONT_MONO15 -#define UI_FONT_MONO15 1 -#endif - -#if UI_FONT_MONO15 - -/*----------------- - * BITMAPS - *----------------*/ - -/*Store the image of the glyphs*/ -static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { - /* U+0020 " " */ - 0x0, - - /* U+0021 "!" */ - 0x55, 0x53, 0xc0, - - /* U+0022 "\"" */ - 0x99, 0x90, - - /* U+0023 "#" */ - 0x14, 0x4b, 0xf9, 0x22, 0x45, 0xbf, 0xa4, 0x48, - - /* U+0024 "$" */ - 0x21, 0x1f, 0x8, 0x30, 0x41, 0xf, 0x88, 0x40, - - /* U+0025 "%" */ - 0xe7, 0x4a, 0xa7, 0x41, 0x5, 0xca, 0xa5, 0xce, - - /* U+0026 "&" */ - 0x30, 0x91, 0x22, 0x86, 0x52, 0xa3, 0x46, 0x74, - - /* U+0027 "'" */ - 0xf0, - - /* U+0028 "(" */ - 0x36, 0x4c, 0x88, 0x88, 0x84, 0x63, - - /* U+0029 ")" */ - 0xc6, 0x23, 0x11, 0x11, 0x12, 0x6c, - - /* U+002A "*" */ - 0x25, 0x7e, 0xad, 0x0, - - /* U+002B "+" */ - 0x10, 0x4f, 0xc4, 0x10, 0x40, - - /* U+002C "," */ - 0x6c, 0xe0, - - /* U+002D "-" */ - 0xe0, - - /* U+002E "." */ - 0xf0, - - /* U+002F "/" */ - 0x8, 0x46, 0x21, 0x18, 0x84, 0x62, 0x11, 0x80, - - /* U+0030 "0" */ - 0x79, 0x28, 0x65, 0x96, 0x18, 0x52, 0x78, - - /* U+0031 "1" */ - 0x27, 0x28, 0x42, 0x10, 0x84, 0xf8, - - /* U+0032 "2" */ - 0xf2, 0x20, 0x82, 0x10, 0x84, 0x30, 0xfc, - - /* U+0033 "3" */ - 0xf0, 0x20, 0x8c, 0xc, 0x10, 0x43, 0xf8, - - /* U+0034 "4" */ - 0x8, 0x62, 0x9a, 0x4a, 0x2f, 0xc2, 0x8, - - /* U+0035 "5" */ - 0x7d, 0x4, 0x10, 0x38, 0x10, 0x43, 0xf8, - - /* U+0036 "6" */ - 0x1d, 0x84, 0x3e, 0x8e, 0x18, 0x73, 0x78, - - /* U+0037 "7" */ - 0xfc, 0x30, 0x84, 0x10, 0xc2, 0x8, 0x60, - - /* U+0038 "8" */ - 0x7a, 0x18, 0x73, 0x7a, 0x38, 0x61, 0x78, - - /* U+0039 "9" */ - 0x7a, 0x38, 0x61, 0x7c, 0x10, 0x86, 0xe0, - - /* U+003A ":" */ - 0xf0, 0x3c, - - /* U+003B ";" */ - 0x6c, 0x0, 0xda, 0x80, - - /* U+003C "<" */ - 0x4, 0x76, 0x30, 0x38, 0x30, - - /* U+003D "=" */ - 0xfc, 0x0, 0x3f, - - /* U+003E ">" */ - 0x83, 0x81, 0xc3, 0x73, 0x0, - - /* U+003F "?" */ - 0xf0, 0x42, 0x22, 0x30, 0xc, 0x60, - - /* U+0040 "@" */ - 0x39, 0x18, 0x67, 0xa6, 0x9a, 0x69, 0x5d, 0x83, - 0x80, - - /* U+0041 "A" */ - 0x18, 0x70, 0xa1, 0x66, 0x48, 0x9f, 0xa1, 0xc2, - - /* U+0042 "B" */ - 0xf2, 0x28, 0xa2, 0xf2, 0x38, 0x63, 0xf8, - - /* U+0043 "C" */ - 0x3d, 0x18, 0x20, 0x82, 0x8, 0x10, 0x3c, - - /* U+0044 "D" */ - 0xf2, 0x28, 0x61, 0x86, 0x18, 0x62, 0xf0, - - /* U+0045 "E" */ - 0xfc, 0x21, 0xf, 0x42, 0x10, 0xf8, - - /* U+0046 "F" */ - 0xfc, 0x21, 0xf, 0xc2, 0x10, 0x80, - - /* U+0047 "G" */ - 0x3d, 0x18, 0x20, 0x82, 0x18, 0x51, 0x3c, - - /* U+0048 "H" */ - 0x86, 0x18, 0x61, 0xfe, 0x18, 0x61, 0x84, - - /* U+0049 "I" */ - 0xf9, 0x8, 0x42, 0x10, 0x84, 0xf8, - - /* U+004A "J" */ - 0x78, 0x42, 0x10, 0x84, 0x31, 0xf0, - - /* U+004B "K" */ - 0x8a, 0x6b, 0x38, 0xe2, 0xc9, 0xa2, 0x8c, - - /* U+004C "L" */ - 0x84, 0x21, 0x8, 0x42, 0x10, 0xf8, - - /* U+004D "M" */ - 0xcf, 0x3c, 0xed, 0xb6, 0xd8, 0x61, 0x84, - - /* U+004E "N" */ - 0x87, 0x1e, 0x69, 0x96, 0x58, 0xe3, 0x84, - - /* U+004F "O" */ - 0x38, 0x8a, 0xc, 0x18, 0x30, 0x60, 0xa2, 0x38, - - /* U+0050 "P" */ - 0xfa, 0x38, 0x61, 0x8f, 0xe8, 0x20, 0x80, - - /* U+0051 "Q" */ - 0x38, 0x8a, 0xc, 0x18, 0x30, 0x60, 0xe3, 0x7c, - 0x20, 0x60, 0x30, - - /* U+0052 "R" */ - 0xfa, 0x38, 0x63, 0xfa, 0x68, 0xa1, 0x84, - - /* U+0053 "S" */ - 0x7e, 0x8, 0x30, 0x38, 0x30, 0x41, 0xf8, - - /* U+0054 "T" */ - 0xfc, 0x41, 0x4, 0x10, 0x41, 0x4, 0x10, - - /* U+0055 "U" */ - 0x86, 0x18, 0x61, 0x86, 0x18, 0x73, 0x78, - - /* U+0056 "V" */ - 0xc2, 0x85, 0x1a, 0x26, 0x44, 0x8a, 0x1c, 0x18, - - /* U+0057 "W" */ - 0x86, 0x18, 0x6d, 0xb6, 0xdc, 0xf3, 0xcc, - - /* U+0058 "X" */ - 0x46, 0xc8, 0xb0, 0xc1, 0x85, 0x9, 0x22, 0x42, - - /* U+0059 "Y" */ - 0xc6, 0x89, 0x11, 0x42, 0x82, 0x4, 0x8, 0x10, - - /* U+005A "Z" */ - 0xfc, 0x31, 0x84, 0x30, 0x84, 0x30, 0xfc, - - /* U+005B "[" */ - 0xf2, 0x49, 0x24, 0x92, 0x70, - - /* U+005C "\\" */ - 0x82, 0x10, 0xc2, 0x10, 0xc2, 0x10, 0xc2, 0x10, - - /* U+005D "]" */ - 0xe4, 0x92, 0x49, 0x24, 0xf0, - - /* U+005E "^" */ - 0x31, 0xc5, 0xa2, 0x84, - - /* U+005F "_" */ - 0xfe, - - /* U+0060 "`" */ - 0xcc, - - /* U+0061 "a" */ - 0x78, 0x30, 0x5f, 0x86, 0x17, 0xc0, - - /* U+0062 "b" */ - 0x82, 0x8, 0x3e, 0x8e, 0x18, 0x61, 0x8b, 0xe0, - - /* U+0063 "c" */ - 0x3f, 0x8, 0x20, 0x83, 0x7, 0xc0, - - /* U+0064 "d" */ - 0x4, 0x10, 0x5f, 0xc6, 0x18, 0x61, 0x45, 0xf0, - - /* U+0065 "e" */ - 0x7b, 0x38, 0x7f, 0x83, 0x7, 0xc0, - - /* U+0066 "f" */ - 0x1e, 0x40, 0x87, 0xe2, 0x4, 0x8, 0x10, 0x20, - 0x40, - - /* U+0067 "g" */ - 0x7f, 0x18, 0x61, 0x87, 0x17, 0xc1, 0xf8, - - /* U+0068 "h" */ - 0x84, 0x21, 0xe8, 0xc6, 0x31, 0x8c, 0x40, - - /* U+0069 "i" */ - 0x61, 0x80, 0x38, 0x20, 0x82, 0x8, 0x20, 0x70, - - /* U+006A "j" */ - 0x18, 0xc0, 0xf0, 0x84, 0x21, 0x8, 0x43, 0xe0, - - /* U+006B "k" */ - 0x82, 0x8, 0x22, 0x93, 0x8e, 0x2c, 0x9a, 0x30, - - /* U+006C "l" */ - 0xe0, 0x82, 0x8, 0x20, 0x82, 0x8, 0x20, 0x70, - - /* U+006D "m" */ - 0xed, 0x26, 0x4c, 0x98, 0x30, 0x60, 0x80, - - /* U+006E "n" */ - 0xf4, 0x63, 0x18, 0xc6, 0x20, - - /* U+006F "o" */ - 0x7b, 0x38, 0x61, 0x87, 0x37, 0x80, - - /* U+0070 "p" */ - 0xf2, 0x28, 0x61, 0x86, 0x3f, 0xa0, 0x80, - - /* U+0071 "q" */ - 0x3d, 0x18, 0x61, 0x87, 0x17, 0xc1, 0x4, - - /* U+0072 "r" */ - 0xfc, 0x21, 0x8, 0x42, 0x0, - - /* U+0073 "s" */ - 0x7c, 0x30, 0xe0, 0x87, 0xc0, - - /* U+0074 "t" */ - 0x20, 0x8f, 0xc8, 0x20, 0x82, 0x8, 0x1c, - - /* U+0075 "u" */ - 0x8c, 0x63, 0x18, 0xc5, 0xe0, - - /* U+0076 "v" */ - 0x42, 0x8d, 0x91, 0x22, 0x87, 0x6, 0x0, - - /* U+0077 "w" */ - 0x83, 0x85, 0x6a, 0xd6, 0xac, 0x99, 0x0, - - /* U+0078 "x" */ - 0x46, 0x58, 0xe0, 0xc3, 0x8c, 0x91, 0x80, - - /* U+0079 "y" */ - 0x8e, 0x34, 0x92, 0x78, 0xc3, 0x8, 0xc0, - - /* U+007A "z" */ - 0xf8, 0xcc, 0x44, 0x63, 0xe0, - - /* U+007B "{" */ - 0x39, 0x8, 0x42, 0x60, 0x84, 0x21, 0x8, 0x30, - - /* U+007C "|" */ - 0xff, 0xf0, - - /* U+007D "}" */ - 0xe1, 0x8, 0x42, 0xc, 0x84, 0x21, 0x9, 0x80, - - /* U+007E "~" */ - 0xe6, 0x60 -}; - - -/*--------------------- - * GLYPH DESCRIPTION - *--------------------*/ - -static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { - {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 120, .box_w = 1, .box_h = 1, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1, .adv_w = 120, .box_w = 2, .box_h = 9, .ofs_x = 3, .ofs_y = 0}, - {.bitmap_index = 4, .adv_w = 120, .box_w = 4, .box_h = 3, .ofs_x = 2, .ofs_y = 7}, - {.bitmap_index = 6, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 14, .adv_w = 120, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 22, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 30, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 38, .adv_w = 120, .box_w = 1, .box_h = 4, .ofs_x = 3, .ofs_y = 6}, - {.bitmap_index = 39, .adv_w = 120, .box_w = 4, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 45, .adv_w = 120, .box_w = 4, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 51, .adv_w = 120, .box_w = 5, .box_h = 5, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 55, .adv_w = 120, .box_w = 6, .box_h = 6, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 60, .adv_w = 120, .box_w = 3, .box_h = 4, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 62, .adv_w = 120, .box_w = 3, .box_h = 1, .ofs_x = 2, .ofs_y = 3}, - {.bitmap_index = 63, .adv_w = 120, .box_w = 2, .box_h = 2, .ofs_x = 3, .ofs_y = 0}, - {.bitmap_index = 64, .adv_w = 120, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 72, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 79, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 85, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 92, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 99, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 106, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 113, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 120, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 127, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 134, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 141, .adv_w = 120, .box_w = 2, .box_h = 7, .ofs_x = 3, .ofs_y = 0}, - {.bitmap_index = 143, .adv_w = 120, .box_w = 3, .box_h = 9, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 147, .adv_w = 120, .box_w = 6, .box_h = 6, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 152, .adv_w = 120, .box_w = 6, .box_h = 4, .ofs_x = 1, .ofs_y = 2}, - {.bitmap_index = 155, .adv_w = 120, .box_w = 6, .box_h = 6, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 160, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 166, .adv_w = 120, .box_w = 6, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 175, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 183, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 190, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 197, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 204, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 210, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 216, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 223, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 230, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 236, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 242, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 249, .adv_w = 120, .box_w = 5, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 255, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 262, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 269, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 277, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 284, .adv_w = 120, .box_w = 7, .box_h = 12, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 295, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 302, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 309, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 316, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 323, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 331, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 338, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 346, .adv_w = 120, .box_w = 7, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 354, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 361, .adv_w = 120, .box_w = 3, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 366, .adv_w = 120, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 374, .adv_w = 120, .box_w = 3, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 379, .adv_w = 120, .box_w = 6, .box_h = 5, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 383, .adv_w = 120, .box_w = 7, .box_h = 1, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 384, .adv_w = 120, .box_w = 3, .box_h = 2, .ofs_x = 2, .ofs_y = 8}, - {.bitmap_index = 385, .adv_w = 120, .box_w = 6, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 391, .adv_w = 120, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 399, .adv_w = 120, .box_w = 6, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 405, .adv_w = 120, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 413, .adv_w = 120, .box_w = 6, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 419, .adv_w = 120, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 428, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 435, .adv_w = 120, .box_w = 5, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 442, .adv_w = 120, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 450, .adv_w = 120, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 458, .adv_w = 120, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 466, .adv_w = 120, .box_w = 6, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 474, .adv_w = 120, .box_w = 7, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 481, .adv_w = 120, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 486, .adv_w = 120, .box_w = 6, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 492, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 499, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 506, .adv_w = 120, .box_w = 5, .box_h = 7, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 511, .adv_w = 120, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 516, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 523, .adv_w = 120, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 528, .adv_w = 120, .box_w = 7, .box_h = 7, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 535, .adv_w = 120, .box_w = 7, .box_h = 7, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 542, .adv_w = 120, .box_w = 7, .box_h = 7, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 549, .adv_w = 120, .box_w = 6, .box_h = 9, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 556, .adv_w = 120, .box_w = 5, .box_h = 7, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 561, .adv_w = 120, .box_w = 5, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 569, .adv_w = 120, .box_w = 1, .box_h = 12, .ofs_x = 3, .ofs_y = -2}, - {.bitmap_index = 571, .adv_w = 120, .box_w = 5, .box_h = 12, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 579, .adv_w = 120, .box_w = 6, .box_h = 2, .ofs_x = 1, .ofs_y = 3} -}; - -/*--------------------- - * CHARACTER MAPPING - *--------------------*/ - - - -/*Collect the unicode lists and glyph_id offsets*/ -static const lv_font_fmt_txt_cmap_t cmaps[] = -{ - { - .range_start = 32, .range_length = 95, .glyph_id_start = 1, - .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY - } -}; - - - -/*-------------------- - * ALL CUSTOM DATA - *--------------------*/ - -#if LV_VERSION_CHECK(8, 0, 0) -/*Store all the custom data of the font*/ -static lv_font_fmt_txt_glyph_cache_t cache; -static const lv_font_fmt_txt_dsc_t font_dsc = { -#else -static lv_font_fmt_txt_dsc_t font_dsc = { -#endif - .glyph_bitmap = glyph_bitmap, - .glyph_dsc = glyph_dsc, - .cmaps = cmaps, - .kern_dsc = NULL, - .kern_scale = 0, - .cmap_num = 1, - .bpp = 1, - .kern_classes = 0, - .bitmap_format = 0, -#if LV_VERSION_CHECK(8, 0, 0) - .cache = &cache -#endif -}; - - -/*----------------- - * PUBLIC FONT - *----------------*/ - -/*Initialize a public general font descriptor*/ -#if LV_VERSION_CHECK(8, 0, 0) -const lv_font_t ui_font_mono15 = { -#else -lv_font_t ui_font_mono15 = { -#endif - .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ - .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ - .line_height = 13, /*The maximum line height required by the font*/ - .base_line = 3, /*Baseline measured from the bottom of the line*/ -#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) - .subpx = LV_FONT_SUBPX_NONE, -#endif -#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 - .underline_position = -2, - .underline_thickness = 0, -#endif - .dsc = &font_dsc /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */ -}; - - - -#endif /*#if UI_FONT_MONO15*/ - diff --git a/projects/APPLaunch/main/ui/fonts/ui_font_mono20.c b/projects/APPLaunch/main/ui/fonts/ui_font_mono20.c deleted file mode 100644 index 86b7180d..00000000 --- a/projects/APPLaunch/main/ui/fonts/ui_font_mono20.c +++ /dev/null @@ -1,569 +0,0 @@ -/******************************************************************************* - * Size: 20 px - * Bpp: 1 - * Opts: --bpp 1 --size 20 --font /home/nihao/SquareLine/assets/UbuntuMono-R.ttf -o /home/nihao/SquareLine/assets/ui_font_mono20.c --format lvgl -r 0x20-0x7f --no-compress --no-prefilter - ******************************************************************************/ - -#include "../ui.h" - -#ifndef UI_FONT_MONO20 -#define UI_FONT_MONO20 1 -#endif - -#if UI_FONT_MONO20 - -/*----------------- - * BITMAPS - *----------------*/ - -/*Store the image of the glyphs*/ -static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { - /* U+0020 " " */ - 0x0, - - /* U+0021 "!" */ - 0xff, 0xff, 0xf, 0xc0, - - /* U+0022 "\"" */ - 0xde, 0xf7, 0xbd, 0x80, - - /* U+0023 "#" */ - 0x11, 0x9, 0x84, 0xdf, 0xf3, 0x21, 0x10, 0x88, - 0x4c, 0xff, 0xb2, 0x19, 0x8, 0x84, 0xc0, - - /* U+0024 "$" */ - 0x18, 0x18, 0x3e, 0xe0, 0xc0, 0xc0, 0xf0, 0x7c, - 0x3e, 0x7, 0x3, 0x3, 0x83, 0xfe, 0x18, 0x18, - 0x18, - - /* U+0025 "%" */ - 0x61, 0xc8, 0xa4, 0x92, 0x49, 0x43, 0x60, 0x20, - 0x36, 0x14, 0x92, 0x49, 0x28, 0x9c, 0x30, - - /* U+0026 "&" */ - 0x3c, 0x33, 0x19, 0x8c, 0xc6, 0xc1, 0xc0, 0xe4, - 0xf2, 0xcd, 0x63, 0xb1, 0xdd, 0xe3, 0x98, - - /* U+0027 "'" */ - 0xff, 0xc0, - - /* U+0028 "(" */ - 0x0, 0x73, 0x8c, 0x61, 0x8c, 0x30, 0xc3, 0xc, - 0x30, 0xc1, 0x86, 0xc, 0x18, 0x30, 0x0, - - /* U+0029 ")" */ - 0x3, 0x86, 0xc, 0x18, 0x60, 0xc3, 0xc, 0x30, - 0xc3, 0xc, 0x61, 0x8c, 0x63, 0x0, 0x0, - - /* U+002A "*" */ - 0x18, 0x18, 0xdb, 0xff, 0x18, 0x3c, 0x66, 0x24, - - /* U+002B "+" */ - 0x18, 0x18, 0x18, 0x18, 0xff, 0x18, 0x18, 0x18, - 0x18, - - /* U+002C "," */ - 0x77, 0x73, 0xec, - - /* U+002D "-" */ - 0xf0, - - /* U+002E "." */ - 0xff, 0x80, - - /* U+002F "/" */ - 0x2, 0x6, 0x6, 0x6, 0xc, 0xc, 0xc, 0x18, - 0x18, 0x18, 0x10, 0x30, 0x30, 0x20, 0x60, 0x60, - 0x40, - - /* U+0030 "0" */ - 0x3c, 0x66, 0x42, 0xc3, 0xc3, 0xdb, 0xc3, 0xc3, - 0xc3, 0xc3, 0x42, 0x66, 0x3c, - - /* U+0031 "1" */ - 0x18, 0x38, 0x78, 0x58, 0x18, 0x18, 0x18, 0x18, - 0x18, 0x18, 0x18, 0x18, 0x7f, - - /* U+0032 "2" */ - 0x78, 0xcc, 0x6, 0x6, 0x6, 0xe, 0x1c, 0x38, - 0x30, 0x60, 0xc0, 0xc0, 0xff, - - /* U+0033 "3" */ - 0xfd, 0x1c, 0x18, 0x30, 0xc7, 0x3, 0x3, 0x6, - 0xc, 0x1c, 0x6f, 0x80, - - /* U+0034 "4" */ - 0x6, 0x7, 0x7, 0x86, 0xc2, 0x63, 0x31, 0x19, - 0x8c, 0xff, 0x83, 0x1, 0x80, 0xc0, 0x60, - - /* U+0035 "5" */ - 0x7f, 0x60, 0x60, 0x60, 0x60, 0x7c, 0x1e, 0x7, - 0x3, 0x3, 0x3, 0x86, 0xfc, - - /* U+0036 "6" */ - 0xe, 0x38, 0x60, 0x60, 0xc0, 0xfc, 0xc6, 0xc3, - 0xc3, 0xc3, 0xc3, 0x66, 0x3c, - - /* U+0037 "7" */ - 0xff, 0x3, 0x6, 0x4, 0xc, 0x8, 0x18, 0x18, - 0x10, 0x30, 0x30, 0x30, 0x30, - - /* U+0038 "8" */ - 0x3c, 0x66, 0xc3, 0xc3, 0xc3, 0x76, 0x3c, 0x66, - 0xc3, 0xc3, 0xc3, 0x66, 0x3c, - - /* U+0039 "9" */ - 0x3c, 0x66, 0xc3, 0xc3, 0xc3, 0xc3, 0x63, 0x3f, - 0x3, 0x6, 0x6, 0x1c, 0x70, - - /* U+003A ":" */ - 0xff, 0x80, 0x7, 0xfc, - - /* U+003B ";" */ - 0x77, 0x70, 0x0, 0x27, 0x73, 0x2c, - - /* U+003C "<" */ - 0x0, 0x7, 0x1e, 0x70, 0xc0, 0xf0, 0x3c, 0x7, - 0x1, - - /* U+003D "=" */ - 0xff, 0x0, 0x0, 0x0, 0xff, - - /* U+003E ">" */ - 0x0, 0xe0, 0x78, 0xe, 0x3, 0xf, 0x3c, 0xe0, - 0x80, - - /* U+003F "?" */ - 0xf8, 0x30, 0xc3, 0x18, 0xe7, 0x18, 0x0, 0x6, - 0x18, 0x60, - - /* U+0040 "@" */ - 0x1e, 0x19, 0x98, 0x6c, 0x3c, 0x7e, 0x6f, 0x67, - 0xb3, 0xd9, 0xec, 0xf3, 0x78, 0xf6, 0x3, 0x0, - 0xc0, 0x3e, - - /* U+0041 "A" */ - 0xc, 0x7, 0x81, 0xe0, 0x48, 0x33, 0xc, 0xc2, - 0x10, 0x84, 0x7f, 0x90, 0x64, 0x9, 0x2, 0xc0, - 0xc0, - - /* U+0042 "B" */ - 0xfc, 0xc6, 0xc3, 0xc3, 0xc3, 0xc6, 0xfc, 0xc6, - 0xc3, 0xc3, 0xc3, 0xc6, 0xfc, - - /* U+0043 "C" */ - 0x1f, 0x18, 0x98, 0x1c, 0xc, 0x6, 0x3, 0x1, - 0x80, 0xc0, 0x60, 0x18, 0x6, 0x1, 0xf0, - - /* U+0044 "D" */ - 0xf8, 0xcc, 0xc6, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, - 0xc3, 0xc3, 0xc6, 0xcc, 0xf8, - - /* U+0045 "E" */ - 0xfe, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfe, 0xc0, - 0xc0, 0xc0, 0xc0, 0xc0, 0xff, - - /* U+0046 "F" */ - 0xff, 0x83, 0x6, 0xc, 0x18, 0x3f, 0xe0, 0xc1, - 0x83, 0x6, 0xc, 0x0, - - /* U+0047 "G" */ - 0x1f, 0x31, 0x60, 0xc0, 0xc0, 0xc0, 0xc0, 0xc3, - 0xc3, 0xc3, 0x63, 0x73, 0x1f, - - /* U+0048 "H" */ - 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xff, 0xc3, - 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, - - /* U+0049 "I" */ - 0xfc, 0xc3, 0xc, 0x30, 0xc3, 0xc, 0x30, 0xc3, - 0xc, 0xfc, - - /* U+004A "J" */ - 0x3f, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, - 0x3, 0x3, 0x3, 0x46, 0x7c, - - /* U+004B "K" */ - 0xc3, 0x63, 0x31, 0x99, 0x8d, 0x87, 0x83, 0xc1, - 0xb0, 0xcc, 0x63, 0x30, 0xd8, 0x6c, 0x18, - - /* U+004C "L" */ - 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, - 0xc0, 0xc0, 0xc0, 0xc0, 0xff, - - /* U+004D "M" */ - 0x63, 0x31, 0xb8, 0xfe, 0xfd, 0x5e, 0xaf, 0x77, - 0x93, 0xc1, 0xe0, 0xf0, 0x78, 0x3c, 0x18, - - /* U+004E "N" */ - 0xc3, 0xe3, 0xe3, 0xf3, 0xd3, 0xd3, 0xcb, 0xcb, - 0xcf, 0xc7, 0xc7, 0xc7, 0xc3, - - /* U+004F "O" */ - 0x3c, 0x31, 0x98, 0xd8, 0x3c, 0x1e, 0xf, 0x7, - 0x83, 0xc1, 0xe0, 0xd8, 0xcc, 0x63, 0xe0, - - /* U+0050 "P" */ - 0xfc, 0xc6, 0xc3, 0xc3, 0xc3, 0xc3, 0xc6, 0xfc, - 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, - - /* U+0051 "Q" */ - 0x3c, 0x31, 0x98, 0xd8, 0x3c, 0x1e, 0xf, 0x7, - 0x83, 0xc1, 0xe0, 0xd8, 0xcf, 0xe3, 0xc0, 0x40, - 0x38, 0x6, - - /* U+0052 "R" */ - 0xfc, 0x63, 0x30, 0xd8, 0x6c, 0x36, 0x1b, 0x19, - 0xf8, 0xcc, 0x63, 0x30, 0x98, 0x6c, 0x30, - - /* U+0053 "S" */ - 0x3e, 0x62, 0xc0, 0xc0, 0xe0, 0x78, 0x3e, 0xe, - 0x7, 0x3, 0x3, 0x86, 0xfc, - - /* U+0054 "T" */ - 0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - 0x18, 0x18, 0x18, 0x18, 0x18, - - /* U+0055 "U" */ - 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, - 0xc3, 0xc3, 0xc3, 0x66, 0x3c, - - /* U+0056 "V" */ - 0xc0, 0xd0, 0x26, 0x19, 0x86, 0x61, 0x88, 0x43, - 0x30, 0xcc, 0x12, 0x4, 0x81, 0xe0, 0x78, 0xc, - 0x0, - - /* U+0057 "W" */ - 0xc1, 0xe0, 0xf0, 0x78, 0x3c, 0x1e, 0x4f, 0x77, - 0xab, 0xd5, 0xfb, 0xf8, 0xcc, 0x66, 0x30, - - /* U+0058 "X" */ - 0x61, 0x98, 0x63, 0x30, 0x48, 0x1e, 0x3, 0x0, - 0xc0, 0x78, 0x12, 0xc, 0xc2, 0x11, 0x86, 0x41, - 0x80, - - /* U+0059 "Y" */ - 0xc0, 0xd0, 0x26, 0x19, 0x84, 0x33, 0xc, 0x81, - 0xe0, 0x30, 0xc, 0x3, 0x0, 0xc0, 0x30, 0xc, - 0x0, - - /* U+005A "Z" */ - 0xff, 0x3, 0x6, 0xc, 0xc, 0x18, 0x18, 0x30, - 0x20, 0x60, 0x40, 0xc0, 0xff, - - /* U+005B "[" */ - 0xfe, 0x31, 0x8c, 0x63, 0x18, 0xc6, 0x31, 0x8c, - 0x63, 0x18, 0xf8, - - /* U+005C "\\" */ - 0x40, 0x60, 0x60, 0x20, 0x30, 0x30, 0x10, 0x10, - 0x18, 0x18, 0x8, 0xc, 0xc, 0x4, 0x6, 0x6, - 0x2, - - /* U+005D "]" */ - 0xf8, 0xc6, 0x31, 0x8c, 0x63, 0x18, 0xc6, 0x31, - 0x8c, 0x63, 0xf8, - - /* U+005E "^" */ - 0x18, 0x3c, 0x3c, 0x66, 0x66, 0xc3, 0x81, - - /* U+005F "_" */ - 0xff, 0xc0, - - /* U+0060 "`" */ - 0xc, 0x62, - - /* U+0061 "a" */ - 0x7c, 0x7, 0x3, 0x3, 0x3f, 0xe3, 0xc3, 0xc3, - 0xe3, 0x7f, - - /* U+0062 "b" */ - 0x40, 0xc0, 0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc3, - 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc6, 0xfc, - - /* U+0063 "c" */ - 0x1f, 0x70, 0x60, 0xc0, 0xc0, 0xc0, 0xc0, 0x60, - 0x70, 0x1f, - - /* U+0064 "d" */ - 0x1, 0x3, 0x3, 0x3, 0x3, 0x3f, 0x63, 0xc3, - 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0x63, 0x3f, - - /* U+0065 "e" */ - 0x3c, 0x66, 0xc3, 0xc3, 0xff, 0xc0, 0xc0, 0xc0, - 0x60, 0x3e, - - /* U+0066 "f" */ - 0x1f, 0x30, 0x30, 0x30, 0xfe, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, - - /* U+0067 "g" */ - 0x3f, 0x63, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, - 0x63, 0x3f, 0x3, 0x6, 0x7c, - - /* U+0068 "h" */ - 0x41, 0x83, 0x6, 0xc, 0x1f, 0xb3, 0x63, 0xc7, - 0x8f, 0x1e, 0x3c, 0x78, 0xf1, 0x80, - - /* U+0069 "i" */ - 0x38, 0x1c, 0xe, 0x0, 0xf, 0x80, 0xc0, 0x60, - 0x30, 0x18, 0xc, 0x6, 0x3, 0x1, 0x80, 0x7c, - - /* U+006A "j" */ - 0x1c, 0x71, 0xc0, 0xfc, 0x30, 0xc3, 0xc, 0x30, - 0xc3, 0xc, 0x30, 0xc3, 0xf8, - - /* U+006B "k" */ - 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc3, 0xc6, 0xcc, - 0xd8, 0xf0, 0xf8, 0xcc, 0xc6, 0xc6, 0xc3, - - /* U+006C "l" */ - 0xf8, 0xc, 0x6, 0x3, 0x1, 0x80, 0xc0, 0x60, - 0x30, 0x18, 0xc, 0x6, 0x3, 0x1, 0x80, 0x7c, - - /* U+006D "m" */ - 0xfe, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xc3, 0xc3, - 0xc3, 0xc3, - - /* U+006E "n" */ - 0xf9, 0x9b, 0x1e, 0x3c, 0x78, 0xf1, 0xe3, 0xc7, - 0x8c, - - /* U+006F "o" */ - 0x3e, 0x31, 0x90, 0x58, 0x3c, 0x1e, 0xf, 0x6, - 0x82, 0x63, 0x1f, 0x0, - - /* U+0070 "p" */ - 0xfc, 0xc6, 0xc2, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, - 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, - - /* U+0071 "q" */ - 0x3f, 0x63, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, - 0x63, 0x3f, 0x3, 0x3, 0x3, - - /* U+0072 "r" */ - 0xff, 0x83, 0x6, 0xc, 0x18, 0x30, 0x60, 0xc1, - 0x80, - - /* U+0073 "s" */ - 0x7d, 0x83, 0x7, 0x7, 0x87, 0x83, 0x83, 0x87, - 0xf8, - - /* U+0074 "t" */ - 0x30, 0x30, 0x30, 0xff, 0x30, 0x30, 0x30, 0x30, - 0x30, 0x30, 0x30, 0x38, 0x1f, - - /* U+0075 "u" */ - 0xc7, 0x8f, 0x1e, 0x3c, 0x78, 0xf1, 0xe3, 0x66, - 0x7c, - - /* U+0076 "v" */ - 0xc1, 0xc3, 0xc3, 0x42, 0x66, 0x66, 0x24, 0x3c, - 0x18, 0x18, - - /* U+0077 "w" */ - 0xc0, 0xd0, 0x24, 0xc9, 0x32, 0x4c, 0x9f, 0xa7, - 0x39, 0xce, 0x33, 0x8, 0x40, - - /* U+0078 "x" */ - 0xc3, 0x66, 0x66, 0x3c, 0x18, 0x3c, 0x3c, 0x66, - 0xc3, 0xc3, - - /* U+0079 "y" */ - 0xc3, 0xc3, 0x43, 0x62, 0x66, 0x26, 0x34, 0x14, - 0x1c, 0x18, 0x18, 0x10, 0xe0, - - /* U+007A "z" */ - 0xfe, 0x1c, 0x30, 0xc3, 0x6, 0x18, 0x30, 0xc1, - 0xfc, - - /* U+007B "{" */ - 0x1e, 0x60, 0xc1, 0x83, 0x6, 0xc, 0x60, 0x20, - 0x60, 0xc1, 0x83, 0x6, 0xc, 0x18, 0x1e, - - /* U+007C "|" */ - 0xff, 0xff, 0xff, 0xff, 0xc0, - - /* U+007D "}" */ - 0xf0, 0x30, 0x60, 0xc1, 0x83, 0x6, 0x3, 0x18, - 0x30, 0x60, 0xc1, 0x83, 0x6, 0xc, 0xf0, - - /* U+007E "~" */ - 0x71, 0x99, 0x8e -}; - - -/*--------------------- - * GLYPH DESCRIPTION - *--------------------*/ - -static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { - {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, - {.bitmap_index = 0, .adv_w = 160, .box_w = 1, .box_h = 1, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1, .adv_w = 160, .box_w = 2, .box_h = 13, .ofs_x = 4, .ofs_y = 0}, - {.bitmap_index = 5, .adv_w = 160, .box_w = 5, .box_h = 5, .ofs_x = 3, .ofs_y = 9}, - {.bitmap_index = 9, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 24, .adv_w = 160, .box_w = 8, .box_h = 17, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 41, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 56, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 71, .adv_w = 160, .box_w = 2, .box_h = 5, .ofs_x = 4, .ofs_y = 9}, - {.bitmap_index = 73, .adv_w = 160, .box_w = 6, .box_h = 19, .ofs_x = 2, .ofs_y = -4}, - {.bitmap_index = 88, .adv_w = 160, .box_w = 6, .box_h = 19, .ofs_x = 2, .ofs_y = -4}, - {.bitmap_index = 103, .adv_w = 160, .box_w = 8, .box_h = 8, .ofs_x = 1, .ofs_y = 5}, - {.bitmap_index = 111, .adv_w = 160, .box_w = 8, .box_h = 9, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 120, .adv_w = 160, .box_w = 4, .box_h = 6, .ofs_x = 3, .ofs_y = -3}, - {.bitmap_index = 123, .adv_w = 160, .box_w = 4, .box_h = 1, .ofs_x = 3, .ofs_y = 5}, - {.bitmap_index = 124, .adv_w = 160, .box_w = 3, .box_h = 3, .ofs_x = 4, .ofs_y = 0}, - {.bitmap_index = 126, .adv_w = 160, .box_w = 8, .box_h = 17, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 143, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 156, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 169, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 182, .adv_w = 160, .box_w = 7, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 194, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 209, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 222, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 235, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 248, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 261, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 274, .adv_w = 160, .box_w = 3, .box_h = 10, .ofs_x = 4, .ofs_y = 0}, - {.bitmap_index = 278, .adv_w = 160, .box_w = 4, .box_h = 12, .ofs_x = 3, .ofs_y = -2}, - {.bitmap_index = 284, .adv_w = 160, .box_w = 8, .box_h = 9, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 293, .adv_w = 160, .box_w = 8, .box_h = 5, .ofs_x = 1, .ofs_y = 3}, - {.bitmap_index = 298, .adv_w = 160, .box_w = 8, .box_h = 9, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 307, .adv_w = 160, .box_w = 6, .box_h = 13, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 317, .adv_w = 160, .box_w = 9, .box_h = 16, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 335, .adv_w = 160, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 352, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 365, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 380, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 393, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 406, .adv_w = 160, .box_w = 7, .box_h = 13, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 418, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 431, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 444, .adv_w = 160, .box_w = 6, .box_h = 13, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 454, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 467, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 482, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 495, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 510, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 523, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 538, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 551, .adv_w = 160, .box_w = 9, .box_h = 16, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 569, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 584, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 597, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 610, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 623, .adv_w = 160, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 640, .adv_w = 160, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 655, .adv_w = 160, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 672, .adv_w = 160, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 689, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 702, .adv_w = 160, .box_w = 5, .box_h = 17, .ofs_x = 3, .ofs_y = -3}, - {.bitmap_index = 713, .adv_w = 160, .box_w = 8, .box_h = 17, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 730, .adv_w = 160, .box_w = 5, .box_h = 17, .ofs_x = 3, .ofs_y = -3}, - {.bitmap_index = 741, .adv_w = 160, .box_w = 8, .box_h = 7, .ofs_x = 1, .ofs_y = 6}, - {.bitmap_index = 748, .adv_w = 160, .box_w = 10, .box_h = 1, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 750, .adv_w = 160, .box_w = 4, .box_h = 4, .ofs_x = 3, .ofs_y = 11}, - {.bitmap_index = 752, .adv_w = 160, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 762, .adv_w = 160, .box_w = 8, .box_h = 15, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 777, .adv_w = 160, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 787, .adv_w = 160, .box_w = 8, .box_h = 15, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 802, .adv_w = 160, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 812, .adv_w = 160, .box_w = 8, .box_h = 14, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 826, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 839, .adv_w = 160, .box_w = 7, .box_h = 15, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 853, .adv_w = 160, .box_w = 9, .box_h = 14, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 869, .adv_w = 160, .box_w = 6, .box_h = 17, .ofs_x = 2, .ofs_y = -3}, - {.bitmap_index = 882, .adv_w = 160, .box_w = 8, .box_h = 15, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 897, .adv_w = 160, .box_w = 9, .box_h = 14, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 913, .adv_w = 160, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 923, .adv_w = 160, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 932, .adv_w = 160, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 944, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 957, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 970, .adv_w = 160, .box_w = 7, .box_h = 10, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 979, .adv_w = 160, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 988, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1001, .adv_w = 160, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1010, .adv_w = 160, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1020, .adv_w = 160, .box_w = 10, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1033, .adv_w = 160, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1043, .adv_w = 160, .box_w = 8, .box_h = 13, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 1056, .adv_w = 160, .box_w = 7, .box_h = 10, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1065, .adv_w = 160, .box_w = 7, .box_h = 17, .ofs_x = 2, .ofs_y = -3}, - {.bitmap_index = 1080, .adv_w = 160, .box_w = 2, .box_h = 17, .ofs_x = 4, .ofs_y = -3}, - {.bitmap_index = 1085, .adv_w = 160, .box_w = 7, .box_h = 17, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 1100, .adv_w = 160, .box_w = 8, .box_h = 3, .ofs_x = 1, .ofs_y = 4} -}; - -/*--------------------- - * CHARACTER MAPPING - *--------------------*/ - - - -/*Collect the unicode lists and glyph_id offsets*/ -static const lv_font_fmt_txt_cmap_t cmaps[] = -{ - { - .range_start = 32, .range_length = 95, .glyph_id_start = 1, - .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY - } -}; - - - -/*-------------------- - * ALL CUSTOM DATA - *--------------------*/ - -#if LV_VERSION_CHECK(8, 0, 0) -/*Store all the custom data of the font*/ -static lv_font_fmt_txt_glyph_cache_t cache; -static const lv_font_fmt_txt_dsc_t font_dsc = { -#else -static lv_font_fmt_txt_dsc_t font_dsc = { -#endif - .glyph_bitmap = glyph_bitmap, - .glyph_dsc = glyph_dsc, - .cmaps = cmaps, - .kern_dsc = NULL, - .kern_scale = 0, - .cmap_num = 1, - .bpp = 1, - .kern_classes = 0, - .bitmap_format = 0, -#if LV_VERSION_CHECK(8, 0, 0) - .cache = &cache -#endif -}; - - -/*----------------- - * PUBLIC FONT - *----------------*/ - -/*Initialize a public general font descriptor*/ -#if LV_VERSION_CHECK(8, 0, 0) -const lv_font_t ui_font_mono20 = { -#else -lv_font_t ui_font_mono20 = { -#endif - .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ - .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ - .line_height = 19, /*The maximum line height required by the font*/ - .base_line = 4, /*Baseline measured from the bottom of the line*/ -#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) - .subpx = LV_FONT_SUBPX_NONE, -#endif -#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 - .underline_position = -2, - .underline_thickness = 0, -#endif - .dsc = &font_dsc /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */ -}; - - - -#endif /*#if UI_FONT_MONO20*/ - From 3dc206716c0e8b3d78cbf9e662dc2fb494892591 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 14:45:14 +0800 Subject: [PATCH 24/70] refactor(APPLaunch): centralize home input group management Move the old Screen1group and input_group_init implementation into UILaunchPage. Expose init_input_group, bind_home_input_group, and home_input_group for home-screen input group management. Remove the obsolete ui_input_group C facade. Validation: CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j22 passed. --- projects/APPLaunch/main/ui/Launch.cpp | 4 +-- projects/APPLaunch/main/ui/UILaunchPage.cpp | 32 +++++++++++++++++-- projects/APPLaunch/main/ui/UILaunchPage.h | 3 ++ .../main/ui/components/ui_launch_page.hpp | 2 +- projects/APPLaunch/main/ui/ui.h | 1 - projects/APPLaunch/main/ui/ui_input_group.c | 10 ------ projects/APPLaunch/main/ui/ui_input_group.h | 22 ------------- 7 files changed, 35 insertions(+), 39 deletions(-) delete mode 100644 projects/APPLaunch/main/ui/ui_input_group.c delete mode 100644 projects/APPLaunch/main/ui/ui_input_group.h diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 9376cab2..7b9dfdcb 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -252,7 +252,7 @@ class LaunchImpl auto self = (LaunchImpl *)arg; SLOGI("[HOME] lv_go_back_home executing (page=%p)", self->app_Page.get()); lv_timer_enable(true); - lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); + UILaunchPage::bind_home_input_group(); lv_disp_load_scr(ui_Screen1); lv_refr_now(NULL); if (self->app_Page) @@ -307,7 +307,7 @@ class LaunchImpl SLOGI("App %s exited with code %d", exec.c_str(), ret); lv_timer_enable(true); if (indev) - lv_indev_set_group(indev, Screen1group); + lv_indev_set_group(indev, UILaunchPage::home_input_group()); lv_disp_load_scr(ui_Screen1); /* Child process has returned; we are back on the launcher home. * Hide the overlay so it doesn't linger. */ diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index b6bca039..47b48a5a 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -318,7 +318,7 @@ void switch_left(lv_event_t *e) void go_back_home(lv_event_t *e) { lv_disp_load_scr(ui_Screen1); - lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); + UILaunchPage::bind_home_input_group(); } @@ -453,6 +453,7 @@ char mono_font_path_buf[512]; char gif_path[256]; const char *font_path = nullptr; const char *mono_font_path = nullptr; +lv_group_t *home_input_group = nullptr; } // namespace @@ -521,12 +522,37 @@ void UILaunchPage::init_fonts() if (!g_font_bold_12) g_font_bold_12 = (lv_font_t *)&lv_font_montserrat_12; } +lv_group_t *UILaunchPage::home_input_group() +{ + return ::home_input_group; +} + +void UILaunchPage::bind_home_input_group() +{ + lv_indev_t *indev = lv_indev_get_next(NULL); + if (indev) { + lv_indev_set_group(indev, ::home_input_group); + } +} + +void UILaunchPage::init_input_group() +{ + if (::home_input_group) { + bind_home_input_group(); + return; + } + + ::home_input_group = lv_group_create(); + lv_group_add_obj(::home_input_group, ui_Screen1); + bind_home_input_group(); +} + void UILaunchPage::load_home_screen() { SLOGI("[HOME] home_screen_load() - loading launcher home screen"); ui____initial_actions0 = lv_obj_create(NULL); lv_disp_load_scr(ui_Screen1); - lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); + UILaunchPage::bind_home_input_group(); cp0_signal_audio_api_play_asset("startup.mp3"); } @@ -571,7 +597,7 @@ void UILaunchPage::init_ui() ui_info_bind(); launch_circle_init(); - input_group_init(); + init_input_group(); #ifndef APPLAUNCH_STARTUP_ANIMATION load_home_screen(); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index 6f12b905..e963ed49 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -15,6 +15,9 @@ class UILaunchPage : public home_base static void load_home_screen(); static void start_startup_gif(); static void create_screen(); + static void init_input_group(); + static void bind_home_input_group(); + static lv_group_t *home_input_group(); private: static void init_images(); diff --git a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp index 457d33c5..0d523cd6 100644 --- a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp @@ -579,7 +579,7 @@ class UILaunchPage : public home_base int ret = cp0_process_exec_blocking(it->Exec.c_str(), &LVGL_HOME_KEY_FLAG, 0); SLOGI("App %s exited with code %d", it->Exec.c_str(), ret); lv_timer_enable(true); - if (indev) lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); + if (indev) lv_indev_set_group(lv_indev_get_next(NULL), get_key_group()); lv_disp_load_scr(ui_Screen1); lv_refr_now(disp); } diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 13a1183d..81b6876e 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -18,7 +18,6 @@ extern "C" #include "components/ui_comp.h" #include "components/ui_comp_hook.h" #include "ui_events.h" -#include "ui_input_group.h" #include "keyboard_input.h" #include "cp0_lvgl_app.h" diff --git a/projects/APPLaunch/main/ui/ui_input_group.c b/projects/APPLaunch/main/ui/ui_input_group.c deleted file mode 100644 index b07ff9cd..00000000 --- a/projects/APPLaunch/main/ui/ui_input_group.c +++ /dev/null @@ -1,10 +0,0 @@ -#include "ui.h" - -lv_group_t *Screen1group = NULL; - -void input_group_init(void) -{ - Screen1group = lv_group_create(); - lv_group_add_obj(Screen1group, ui_Screen1); - lv_indev_set_group(lv_indev_get_next(NULL), Screen1group); -} diff --git a/projects/APPLaunch/main/ui/ui_input_group.h b/projects/APPLaunch/main/ui/ui_input_group.h deleted file mode 100644 index 7d1e4373..00000000 --- a/projects/APPLaunch/main/ui/ui_input_group.h +++ /dev/null @@ -1,22 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#ifndef _UI_INPUT_GROUP_H -#define _UI_INPUT_GROUP_H - -#ifdef __cplusplus -extern "C" { -#endif - -extern lv_group_t *Screen1group; - -void input_group_init(void); - - -#ifdef __cplusplus -} /*extern "C"*/ -#endif - -#endif From b4590a3b94d873eee69a0e04d6ed8d5d0a26b6bf Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 14:50:31 +0800 Subject: [PATCH 25/70] chore(APPLaunch): enable LVGL external data support Enable CONFIG_V9_5_LV_USE_EXT_DATA across APPLaunch default build configurations. --- projects/APPLaunch/config_defaults.mk | 2 +- projects/APPLaunch/darwin_config_defaults.mk | 2 +- projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk | 2 +- projects/APPLaunch/linux_x86_sdl2_config_defaults.mk | 2 +- projects/APPLaunch/mac_cross_cp0_config_defaults.mk | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/projects/APPLaunch/config_defaults.mk b/projects/APPLaunch/config_defaults.mk index ce2df891..311de281 100644 --- a/projects/APPLaunch/config_defaults.mk +++ b/projects/APPLaunch/config_defaults.mk @@ -75,7 +75,7 @@ CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 - +CONFIG_V9_5_LV_USE_EXT_DATA=y CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y CONFIG_V9_5_LV_USE_GIF=y diff --git a/projects/APPLaunch/darwin_config_defaults.mk b/projects/APPLaunch/darwin_config_defaults.mk index 9e03de85..05bb85bb 100644 --- a/projects/APPLaunch/darwin_config_defaults.mk +++ b/projects/APPLaunch/darwin_config_defaults.mk @@ -80,7 +80,7 @@ CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 - +CONFIG_V9_5_LV_USE_EXT_DATA=y CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y CONFIG_V9_5_LV_USE_GIF=y diff --git a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk index 66b33e87..09e47e01 100644 --- a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk @@ -81,7 +81,7 @@ CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 - +CONFIG_V9_5_LV_USE_EXT_DATA=y CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y CONFIG_V9_5_LV_USE_GIF=y diff --git a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk index c7cb551d..e0288e9c 100644 --- a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk @@ -80,7 +80,7 @@ CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 - +CONFIG_V9_5_LV_USE_EXT_DATA=y CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y CONFIG_V9_5_LV_USE_GIF=y diff --git a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk index cf52ab2e..3d8d1663 100644 --- a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk @@ -83,7 +83,7 @@ CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 - +CONFIG_V9_5_LV_USE_EXT_DATA=y CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y CONFIG_V9_5_LV_USE_GIF=y From 5b5d95f004b44262159bb275b443a5f55a5d6f3e Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 15:46:17 +0800 Subject: [PATCH 26/70] refactor(APPLaunch): consolidate launcher carousel UI Move launcher carousel objects into a typed UILaunchPage array and remove unused SquareLine object declarations. Use C++ rotation helpers and a structured carousel slot table for card/title animation state. Align app page top bars with the launcher header styling while keeping flex layout spacing. --- projects/APPLaunch/main/ui/Launch.cpp | 40 +- projects/APPLaunch/main/ui/UILaunchPage.cpp | 601 ++++++++---------- projects/APPLaunch/main/ui/UILaunchPage.h | 24 + .../main/ui/components/ui_app_page.hpp | 158 +++-- projects/APPLaunch/main/ui/ui.h | 1 - projects/APPLaunch/main/ui/ui_obj.h | 113 ---- 6 files changed, 414 insertions(+), 523 deletions(-) diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 7b9dfdcb..fa7b94c0 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -156,32 +156,32 @@ class LaunchImpl { auto it = std::next(app_list.begin(), 0); - lv_label_set_text(ui_leftOuterLabel, it->Name.c_str()); - panel_set_icon(ui_leftOuterPanel, it->Icon.c_str()); + lv_label_set_text(UILaunchPage::label(0), it->Name.c_str()); + panel_set_icon(UILaunchPage::panel(0), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 1); - lv_label_set_text(ui_leftLabel, it->Name.c_str()); - panel_set_icon(ui_leftPanel, it->Icon.c_str()); + lv_label_set_text(UILaunchPage::label(1), it->Name.c_str()); + panel_set_icon(UILaunchPage::panel(1), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 2); - lv_label_set_text(ui_switchLabel, it->Name.c_str()); - panel_set_icon(ui_switchPanel, it->Icon.c_str()); + lv_label_set_text(UILaunchPage::label(2), it->Name.c_str()); + panel_set_icon(UILaunchPage::panel(2), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 3); - lv_label_set_text(ui_rightLabel, it->Name.c_str()); - panel_set_icon(ui_rightPanel, it->Icon.c_str()); + lv_label_set_text(UILaunchPage::label(3), it->Name.c_str()); + panel_set_icon(UILaunchPage::panel(3), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 4); - lv_label_set_text(ui_rightOuterLabel, it->Name.c_str()); - panel_set_icon(ui_rightOuterPanel, it->Icon.c_str()); + lv_label_set_text(UILaunchPage::label(4), it->Name.c_str()); + panel_set_icon(UILaunchPage::panel(4), it->Icon.c_str()); } // Dynamic icons filtered by Settings configuration @@ -490,32 +490,32 @@ class LaunchImpl // far left outside (hidden) { auto &a = app_at(current_app - 2); - lv_label_set_text(ui_leftOuterLabel, a.Name.c_str()); - panel_set_icon(ui_leftOuterPanel, a.Icon.c_str()); + lv_label_set_text(UILaunchPage::label(0), a.Name.c_str()); + panel_set_icon(UILaunchPage::panel(0), a.Icon.c_str()); } // left { auto &a = app_at(current_app - 1); - lv_label_set_text(ui_leftLabel, a.Name.c_str()); - panel_set_icon(ui_leftPanel, a.Icon.c_str()); + lv_label_set_text(UILaunchPage::label(1), a.Name.c_str()); + panel_set_icon(UILaunchPage::panel(1), a.Icon.c_str()); } // center { auto &a = app_at(current_app); - lv_label_set_text(ui_switchLabel, a.Name.c_str()); - panel_set_icon(ui_switchPanel, a.Icon.c_str()); + lv_label_set_text(UILaunchPage::label(2), a.Name.c_str()); + panel_set_icon(UILaunchPage::panel(2), a.Icon.c_str()); } // right { auto &a = app_at(current_app + 1); - lv_label_set_text(ui_rightLabel, a.Name.c_str()); - panel_set_icon(ui_rightPanel, a.Icon.c_str()); + lv_label_set_text(UILaunchPage::label(3), a.Name.c_str()); + panel_set_icon(UILaunchPage::panel(3), a.Icon.c_str()); } // far right outside (hidden) { auto &a = app_at(current_app + 2); - lv_label_set_text(ui_rightOuterLabel, a.Name.c_str()); - panel_set_icon(ui_rightOuterPanel, a.Icon.c_str()); + lv_label_set_text(UILaunchPage::label(4), a.Name.c_str()); + panel_set_icon(UILaunchPage::panel(4), a.Icon.c_str()); } } diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 47b48a5a..a6ab59ae 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -8,45 +8,54 @@ #include "Animation/ui_launcher_animation.h" -#include - -extern "C" { - -typedef void (*switch_cb_t)(lv_event_t *); +#include +std::array UILaunchPage::carousel_elements = {}; -#define ROTATE_LEFT(arr, start, end) \ - do \ - { \ - __typeof__((arr)[0]) _tmp = (arr)[(start)]; \ - memmove(&(arr)[(start)], &(arr)[(start) + 1], \ - ((end) - (start)) * sizeof((arr)[0])); \ - (arr)[(end)] = _tmp; \ - } while (0) +static void rotate_carousel_left(size_t start, size_t end) +{ + auto &items = UILaunchPage::carousel_elements; + std::rotate(items.begin() + start, items.begin() + start + 1, items.begin() + end + 1); +} -#define ROTATE_RIGHT(arr, start, end) \ - do \ - { \ - __typeof__((arr)[0]) _tmp = (arr)[(end)]; \ - memmove(&(arr)[(start) + 1], &(arr)[(start)], \ - ((end) - (start)) * sizeof((arr)[0])); \ - (arr)[(start)] = _tmp; \ - } while (0) +static void rotate_carousel_right(size_t start, size_t end) +{ + auto &items = UILaunchPage::carousel_elements; + std::rotate(items.begin() + start, items.begin() + end, items.begin() + end + 1); +} -lv_obj_t *launch_circle[100]; +extern "C" { -// ==================== standard coordinates for 5 slots ==================== +typedef void (*switch_cb_t)(lv_event_t *); -static const lv_coord_t SLOT_X[] = {-177, -99, 0, 99, 177, -177, -99, 0, 99, 177}; -static const lv_coord_t SLOT_Y[] = {4, -6, -16, -6, 4, LABEL_Y_SIDE, LABEL_Y_SIDE, LABEL_Y_CENTER, LABEL_Y_SIDE, LABEL_Y_SIDE}; -static const lv_coord_t SLOT_W[] = {61, 80, 100, 80, 61}; -static const lv_coord_t SLOT_H[] = {61, 80, 100, 80, 61}; +// ==================== standard layout for carousel slots ==================== + +struct CarouselSlot { + lv_coord_t x; + lv_coord_t y; + lv_coord_t width; + lv_coord_t height; + bool hidden; +}; + +static const CarouselSlot CAROUSEL_SLOTS[] = { + {-177, 4, 61, 61, true}, + {-99, -6, 80, 80, false}, + {0, -16, 100, 100, false}, + {99, -6, 80, 80, false}, + {177, 4, 61, 61, true}, + {-177, LABEL_Y_SIDE, 0, 0, true}, + {-99, LABEL_Y_SIDE, 0, 0, false}, + {0, LABEL_Y_CENTER, 0, 0, false}, + {99, LABEL_Y_SIDE, 0, 0, false}, + {177, LABEL_Y_SIDE, 0, 0, true}, +}; static bool is_animating = false; static switch_cb_t pending_switch = NULL; static int Panel_current_pos = 2; -static int switch_current_pos = 11; +static int switch_current_pos = UILaunchPage::kPageDot2; // ============================================================ @@ -68,44 +77,13 @@ static void audio_play_enter(void) audio_play_ui_asset("enter.wav"); } -// ============================================================ -// Initialize -// ============================================================ - -void launch_circle_init() -{ - launch_circle[0] = ui_leftOuterPanel; - launch_circle[1] = ui_leftPanel; - launch_circle[2] = ui_switchPanel; - launch_circle[3] = ui_rightPanel; - launch_circle[4] = ui_rightOuterPanel; - - launch_circle[5] = ui_leftOuterLabel; - launch_circle[6] = ui_leftLabel; - launch_circle[7] = ui_switchLabel; - launch_circle[8] = ui_rightLabel; - launch_circle[9] = ui_rightOuterLabel; - - launch_circle[10] = ui_Panel4; - launch_circle[11] = ui_Panel3; - launch_circle[12] = ui_Panel5; - launch_circle[13] = ui_Panel6; - launch_circle[14] = ui_Panel7; - launch_circle[15] = ui_Panel8; - launch_circle[16] = ui_Panel9; - launch_circle[17] = ui_Panel10; - - -} - - // ============================================================ // switch panel style // ============================================================ static void switchpanleEnable(int obj_index, int enable) { - lv_obj_t *obj = launch_circle[obj_index]; + lv_obj_t *obj = UILaunchPage::carousel_elements[obj_index]; if (enable) { @@ -138,7 +116,7 @@ static void switchpanleEnable(int obj_index, int enable) static void switchpanleEnableClick(int obj_index, int enable) { - lv_obj_t *obj = launch_circle[obj_index]; + lv_obj_t *obj = UILaunchPage::carousel_elements[obj_index]; if (enable) { @@ -157,12 +135,13 @@ static void switchpanleEnableClick(int obj_index, int enable) static void snap_panel_to_slot(lv_obj_t *panel, int slot) { - lv_obj_set_x(panel, SLOT_X[slot]); - lv_obj_set_y(panel, SLOT_Y[slot]); - lv_obj_set_width(panel, SLOT_W[slot]); - lv_obj_set_height(panel, SLOT_H[slot]); + const CarouselSlot &layout = CAROUSEL_SLOTS[slot]; + lv_obj_set_x(panel, layout.x); + lv_obj_set_y(panel, layout.y); + lv_obj_set_width(panel, layout.width); + lv_obj_set_height(panel, layout.height); - if (slot == 0 || slot == 4) + if (layout.hidden) { lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); } @@ -179,10 +158,11 @@ static void snap_panel_to_slot(lv_obj_t *panel, int slot) static void snap_label_to_slot(lv_obj_t *label, int slot) { - lv_obj_set_x(label, SLOT_X[slot]); - lv_obj_set_y(label, SLOT_Y[slot]); + const CarouselSlot &layout = CAROUSEL_SLOTS[slot]; + lv_obj_set_x(label, layout.x); + lv_obj_set_y(label, layout.y); - if (slot == 5 || slot == 9) + if (layout.hidden) { lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); } @@ -201,12 +181,12 @@ static void snap_all_panels() { for (int i = 0; i < 5; i++) { - snap_panel_to_slot(launch_circle[i], i); + snap_panel_to_slot(UILaunchPage::carousel_elements[i], i); } for (int i = 5; i < 10; i++) { - snap_label_to_slot(launch_circle[i], i); + snap_label_to_slot(UILaunchPage::carousel_elements[i], i); } is_animating = false; @@ -214,12 +194,12 @@ static void snap_all_panels() // Reset border colors: center=bright, sides=dark for (int i = 0; i < 5; i++) { uint32_t color = (i == 2) ? BORDER_COLOR_CENTER : BORDER_COLOR_SIDE; - lv_obj_set_style_border_color(launch_circle[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(UILaunchPage::carousel_elements[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); } // Reset all label fonts to bold for (int i = 5; i < 10; i++) { - lv_obj_set_style_text_font(launch_circle[i], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(UILaunchPage::carousel_elements[i], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); } if (pending_switch) { @@ -244,27 +224,27 @@ void switch_right(lv_event_t *e) is_animating = true; - lv_obj_clear_flag(launch_circle[0], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(UILaunchPage::carousel_elements[0], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_right(launch_circle, snap_all_panels); + launcher_home_animation::animate_right(UILaunchPage::carousel_elements.data(), snap_all_panels); - snap_panel_to_slot(launch_circle[4], 0); + snap_panel_to_slot(UILaunchPage::carousel_elements[4], 0); - lv_obj_clear_flag(launch_circle[5], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(UILaunchPage::carousel_elements[5], LV_OBJ_FLAG_HIDDEN); - snap_label_to_slot(launch_circle[9], 5); + snap_label_to_slot(UILaunchPage::carousel_elements[9], 5); - cpp_app_right(launch_circle[4], launch_circle[9]); + cpp_app_right(UILaunchPage::carousel_elements[4], UILaunchPage::carousel_elements[9]); switchpanleEnableClick(2, 0); - ROTATE_RIGHT(launch_circle, 0, 4); + rotate_carousel_right(0, 4); switchpanleEnableClick(2, 1); - ROTATE_RIGHT(launch_circle, 5, 9); + rotate_carousel_right(5, 9); switchpanleEnable(switch_current_pos, 0); - switch_current_pos = switch_current_pos == 10 ? 17 : switch_current_pos - 1; + switch_current_pos = switch_current_pos == UILaunchPage::kPageDot0 ? UILaunchPage::kPageDot4 : switch_current_pos - 1; switchpanleEnable(switch_current_pos, 1); } @@ -284,27 +264,27 @@ void switch_left(lv_event_t *e) is_animating = true; - lv_obj_clear_flag(launch_circle[4], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(UILaunchPage::carousel_elements[4], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_left(launch_circle, snap_all_panels); + launcher_home_animation::animate_left(UILaunchPage::carousel_elements.data(), snap_all_panels); - snap_panel_to_slot(launch_circle[0], 4); + snap_panel_to_slot(UILaunchPage::carousel_elements[0], 4); - lv_obj_clear_flag(launch_circle[9], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(UILaunchPage::carousel_elements[9], LV_OBJ_FLAG_HIDDEN); - snap_label_to_slot(launch_circle[5], 9); + snap_label_to_slot(UILaunchPage::carousel_elements[5], 9); - cpp_app_left(launch_circle[0], launch_circle[5]); + cpp_app_left(UILaunchPage::carousel_elements[0], UILaunchPage::carousel_elements[5]); switchpanleEnableClick(2, 0); - ROTATE_LEFT(launch_circle, 0, 4); + rotate_carousel_left(0, 4); switchpanleEnableClick(2, 1); - ROTATE_LEFT(launch_circle, 5, 9); + rotate_carousel_left(5, 9); switchpanleEnable(switch_current_pos, 0); - switch_current_pos = switch_current_pos == 17 ? 10 : switch_current_pos + 1; + switch_current_pos = switch_current_pos == UILaunchPage::kPageDot4 ? UILaunchPage::kPageDot0 : switch_current_pos + 1; switchpanleEnable(switch_current_pos, 1); } @@ -527,6 +507,16 @@ lv_group_t *UILaunchPage::home_input_group() return ::home_input_group; } +lv_obj_t *UILaunchPage::panel(size_t slot) +{ + return carousel_elements[kCardFarLeft + slot]; +} + +lv_obj_t *UILaunchPage::label(size_t slot) +{ + return carousel_elements[kTitleFarLeft + slot]; +} + void UILaunchPage::bind_home_input_group() { lv_indev_t *indev = lv_indev_get_next(NULL); @@ -595,8 +585,6 @@ void UILaunchPage::init_ui() create_screen(); ui_info_bind(); - launch_circle_init(); - init_input_group(); #ifndef APPLAUNCH_STARTUP_ANIMATION @@ -803,196 +791,157 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_align(::ui_APP_Container, LV_ALIGN_CENTER); lv_obj_clear_flag(::ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - ui_Panel4 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel4, 5); - lv_obj_set_height(ui_Panel4, 5); - lv_obj_set_x(ui_Panel4, -35); - lv_obj_set_y(ui_Panel4, 70); - lv_obj_set_align(ui_Panel4, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel4, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel4, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel4, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel4, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel4, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel4, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel3 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel3, 8); - lv_obj_set_height(ui_Panel3, 8); - lv_obj_set_x(ui_Panel3, -25); - lv_obj_set_y(ui_Panel3, 70); - lv_obj_set_align(ui_Panel3, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel3, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel3, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel3, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel3, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel3, lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel3, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel5 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel5, 5); - lv_obj_set_height(ui_Panel5, 5); - lv_obj_set_x(ui_Panel5, -15); - lv_obj_set_y(ui_Panel5, 70); - lv_obj_set_align(ui_Panel5, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel5, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel5, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel5, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel5, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel5, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel5, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel6 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel6, 5); - lv_obj_set_height(ui_Panel6, 5); - lv_obj_set_x(ui_Panel6, -5); - lv_obj_set_y(ui_Panel6, 70); - lv_obj_set_align(ui_Panel6, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel6, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel6, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel6, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel6, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel6, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel6, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel7 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel7, 5); - lv_obj_set_height(ui_Panel7, 5); - lv_obj_set_x(ui_Panel7, 5); - lv_obj_set_y(ui_Panel7, 70); - lv_obj_set_align(ui_Panel7, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel7, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel7, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel7, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel7, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel7, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel7, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel8 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel8, 5); - lv_obj_set_height(ui_Panel8, 5); - lv_obj_set_x(ui_Panel8, 15); - lv_obj_set_y(ui_Panel8, 70); - lv_obj_set_align(ui_Panel8, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel8, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel8, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel8, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel8, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel8, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel8, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel9 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel9, 5); - lv_obj_set_height(ui_Panel9, 5); - lv_obj_set_x(ui_Panel9, 25); - lv_obj_set_y(ui_Panel9, 70); - lv_obj_set_align(ui_Panel9, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel9, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel9, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel9, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel9, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel9, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel9, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Panel10 = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_Panel10, 5); - lv_obj_set_height(ui_Panel10, 5); - lv_obj_set_x(ui_Panel10, 35); - lv_obj_set_y(ui_Panel10, 70); - lv_obj_set_align(ui_Panel10, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_Panel10, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Panel10, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel10, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(ui_Panel10, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_Panel10, lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_Panel10, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_switchLabel = lv_label_create(::ui_APP_Container); - lv_obj_set_width(ui_switchLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_switchLabel, LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(ui_switchLabel, 0); - lv_obj_set_y(ui_switchLabel, LABEL_Y_CENTER); - lv_obj_set_align(ui_switchLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_switchLabel, "CLI"); - lv_obj_set_style_text_font(ui_switchLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(ui_switchLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_switchLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_rightLabel = lv_label_create(::ui_APP_Container); - lv_obj_set_width(ui_rightLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_rightLabel, LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(ui_rightLabel, 99); - lv_obj_set_y(ui_rightLabel, LABEL_Y_SIDE); - lv_obj_set_align(ui_rightLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_rightLabel, "GAME"); - lv_obj_set_style_text_color(ui_rightLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(ui_rightLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_rightLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_leftLabel = lv_label_create(::ui_APP_Container); - lv_obj_set_width(ui_leftLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_leftLabel, LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(ui_leftLabel, -99); - lv_obj_set_y(ui_leftLabel, LABEL_Y_SIDE); - lv_obj_set_align(ui_leftLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_leftLabel, "STORE"); - lv_obj_set_style_text_color(ui_leftLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_leftLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(ui_leftLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_leftPanel = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_leftPanel, 80); - lv_obj_set_height(ui_leftPanel, 80); - lv_obj_set_x(ui_leftPanel, -99); - lv_obj_set_y(ui_leftPanel, -6); - lv_obj_set_align(ui_leftPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_leftPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(ui_leftPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_leftPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_leftPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_leftPanel, lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_leftPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_switchPanel = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_switchPanel, 100); - lv_obj_set_height(ui_switchPanel, 100); - lv_obj_set_x(ui_switchPanel, 0); - lv_obj_set_y(ui_switchPanel, -16); - lv_obj_set_align(ui_switchPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_switchPanel, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_switchPanel, 22, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_switchPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_switchPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_switchPanel, lv_color_hex(0x444444), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_switchPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_switchPanel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_rightPanel = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_rightPanel, 80); - lv_obj_set_height(ui_rightPanel, 80); - lv_obj_set_x(ui_rightPanel, 99); - lv_obj_set_y(ui_rightPanel, -6); - lv_obj_set_align(ui_rightPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_rightPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(ui_rightPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_rightPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_rightPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_rightPanel, lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_rightPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_rightOuterPanel = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_rightOuterPanel, 61); - lv_obj_set_height(ui_rightOuterPanel, 61); - lv_obj_set_x(ui_rightOuterPanel, 177); - lv_obj_set_y(ui_rightOuterPanel, 4); - lv_obj_set_align(ui_rightOuterPanel, LV_ALIGN_CENTER); - lv_obj_add_flag(ui_rightOuterPanel, LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_clear_flag(ui_rightOuterPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(ui_rightOuterPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_rightOuterPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_rightOuterPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_rightOuterPanel, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_rightOuterPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + carousel_elements[kPageDot0] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kPageDot0], 5); + lv_obj_set_height(carousel_elements[kPageDot0], 5); + lv_obj_set_x(carousel_elements[kPageDot0], -20); + lv_obj_set_y(carousel_elements[kPageDot0], 70); + lv_obj_set_align(carousel_elements[kPageDot0], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kPageDot0], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kPageDot1] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kPageDot1], 5); + lv_obj_set_height(carousel_elements[kPageDot1], 5); + lv_obj_set_x(carousel_elements[kPageDot1], -10); + lv_obj_set_y(carousel_elements[kPageDot1], 70); + lv_obj_set_align(carousel_elements[kPageDot1], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kPageDot1], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kPageDot2] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kPageDot2], 10); + lv_obj_set_height(carousel_elements[kPageDot2], 10); + lv_obj_set_x(carousel_elements[kPageDot2], 0); + lv_obj_set_y(carousel_elements[kPageDot2], 70); + lv_obj_set_align(carousel_elements[kPageDot2], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kPageDot2], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot2], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kPageDot3] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kPageDot3], 5); + lv_obj_set_height(carousel_elements[kPageDot3], 5); + lv_obj_set_x(carousel_elements[kPageDot3], 10); + lv_obj_set_y(carousel_elements[kPageDot3], 70); + lv_obj_set_align(carousel_elements[kPageDot3], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kPageDot3], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kPageDot4] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kPageDot4], 5); + lv_obj_set_height(carousel_elements[kPageDot4], 5); + lv_obj_set_x(carousel_elements[kPageDot4], 20); + lv_obj_set_y(carousel_elements[kPageDot4], 70); + lv_obj_set_align(carousel_elements[kPageDot4], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kPageDot4], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kTitleCenter] = lv_label_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kTitleCenter], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements[kTitleCenter], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements[kTitleCenter], 0); + lv_obj_set_y(carousel_elements[kTitleCenter], LABEL_Y_CENTER); + lv_obj_set_align(carousel_elements[kTitleCenter], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements[kTitleCenter], "CLI"); + lv_obj_set_style_text_font(carousel_elements[kTitleCenter], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(carousel_elements[kTitleCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements[kTitleCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kTitleRight] = lv_label_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kTitleRight], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements[kTitleRight], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements[kTitleRight], 99); + lv_obj_set_y(carousel_elements[kTitleRight], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements[kTitleRight], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements[kTitleRight], "GAME"); + lv_obj_set_style_text_color(carousel_elements[kTitleRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleRight], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements[kTitleRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kTitleLeft] = lv_label_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kTitleLeft], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements[kTitleLeft], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements[kTitleLeft], -99); + lv_obj_set_y(carousel_elements[kTitleLeft], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements[kTitleLeft], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements[kTitleLeft], "STORE"); + lv_obj_set_style_text_color(carousel_elements[kTitleLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements[kTitleLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleLeft], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kCardLeft] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kCardLeft], 80); + lv_obj_set_height(carousel_elements[kCardLeft], 80); + lv_obj_set_x(carousel_elements[kCardLeft], -99); + lv_obj_set_y(carousel_elements[kCardLeft], -6); + lv_obj_set_align(carousel_elements[kCardLeft], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kCardLeft], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements[kCardLeft], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements[kCardLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kCardLeft], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kCardLeft], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kCardLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kCardCenter] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kCardCenter], 100); + lv_obj_set_height(carousel_elements[kCardCenter], 100); + lv_obj_set_x(carousel_elements[kCardCenter], 0); + lv_obj_set_y(carousel_elements[kCardCenter], -16); + lv_obj_set_align(carousel_elements[kCardCenter], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kCardCenter], LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(carousel_elements[kCardCenter], 22, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements[kCardCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kCardCenter], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kCardCenter], lv_color_hex(0x444444), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kCardCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(carousel_elements[kCardCenter], 2, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kCardRight] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kCardRight], 80); + lv_obj_set_height(carousel_elements[kCardRight], 80); + lv_obj_set_x(carousel_elements[kCardRight], 99); + lv_obj_set_y(carousel_elements[kCardRight], -6); + lv_obj_set_align(carousel_elements[kCardRight], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements[kCardRight], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements[kCardRight], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements[kCardRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kCardRight], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kCardRight], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kCardRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kCardFarRight] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kCardFarRight], 61); + lv_obj_set_height(carousel_elements[kCardFarRight], 61); + lv_obj_set_x(carousel_elements[kCardFarRight], 177); + lv_obj_set_y(carousel_elements[kCardFarRight], 4); + lv_obj_set_align(carousel_elements[kCardFarRight], LV_ALIGN_CENTER); + lv_obj_add_flag(carousel_elements[kCardFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_clear_flag(carousel_elements[kCardFarRight], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements[kCardFarRight], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements[kCardFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kCardFarRight], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kCardFarRight], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kCardFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); ui_leftButton = lv_btn_create(::ui_APP_Container); lv_obj_set_width(ui_leftButton, 17); @@ -1024,51 +973,51 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_shadow_color(ui_rightButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftOuterPanel = lv_obj_create(::ui_APP_Container); - lv_obj_set_width(ui_leftOuterPanel, 61); - lv_obj_set_height(ui_leftOuterPanel, 61); - lv_obj_set_x(ui_leftOuterPanel, -177); - lv_obj_set_y(ui_leftOuterPanel, 4); - lv_obj_set_align(ui_leftOuterPanel, LV_ALIGN_CENTER); - lv_obj_add_flag(ui_leftOuterPanel, LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_clear_flag(ui_leftOuterPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(ui_leftOuterPanel, 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_leftOuterPanel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_leftOuterPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(ui_leftOuterPanel, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(ui_leftOuterPanel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_leftOuterLabel = lv_label_create(::ui_APP_Container); - lv_obj_set_width(ui_leftOuterLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_leftOuterLabel, LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(ui_leftOuterLabel, -177); - lv_obj_set_y(ui_leftOuterLabel, LABEL_Y_SIDE); - lv_obj_set_align(ui_leftOuterLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_leftOuterLabel, "one"); - lv_obj_add_flag(ui_leftOuterLabel, LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_set_style_text_font(ui_leftOuterLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(ui_leftOuterLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_leftOuterLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_rightOuterLabel = lv_label_create(::ui_APP_Container); - lv_obj_set_width(ui_rightOuterLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_rightOuterLabel, LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(ui_rightOuterLabel, 177); - lv_obj_set_y(ui_rightOuterLabel, LABEL_Y_SIDE); - lv_obj_set_align(ui_rightOuterLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_rightOuterLabel, "three"); - lv_obj_add_flag(ui_rightOuterLabel, LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_set_style_text_font(ui_rightOuterLabel, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(ui_rightOuterLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_rightOuterLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_add_event_cb(ui_leftPanel, app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_switchPanel, app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_rightPanel, app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_rightOuterPanel, app_launch, LV_EVENT_CLICKED, NULL); + carousel_elements[kCardFarLeft] = lv_obj_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kCardFarLeft], 61); + lv_obj_set_height(carousel_elements[kCardFarLeft], 61); + lv_obj_set_x(carousel_elements[kCardFarLeft], -177); + lv_obj_set_y(carousel_elements[kCardFarLeft], 4); + lv_obj_set_align(carousel_elements[kCardFarLeft], LV_ALIGN_CENTER); + lv_obj_add_flag(carousel_elements[kCardFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_clear_flag(carousel_elements[kCardFarLeft], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements[kCardFarLeft], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements[kCardFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements[kCardFarLeft], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements[kCardFarLeft], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements[kCardFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kTitleFarLeft] = lv_label_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kTitleFarLeft], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements[kTitleFarLeft], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements[kTitleFarLeft], -177); + lv_obj_set_y(carousel_elements[kTitleFarLeft], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements[kTitleFarLeft], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements[kTitleFarLeft], "one"); + lv_obj_add_flag(carousel_elements[kTitleFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_set_style_text_font(carousel_elements[kTitleFarLeft], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(carousel_elements[kTitleFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements[kTitleFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kTitleFarRight] = lv_label_create(::ui_APP_Container); + lv_obj_set_width(carousel_elements[kTitleFarRight], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements[kTitleFarRight], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements[kTitleFarRight], 177); + lv_obj_set_y(carousel_elements[kTitleFarRight], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements[kTitleFarRight], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements[kTitleFarRight], "three"); + lv_obj_add_flag(carousel_elements[kTitleFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_set_style_text_font(carousel_elements[kTitleFarRight], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(carousel_elements[kTitleFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements[kTitleFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_add_event_cb(carousel_elements[kCardLeft], app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(carousel_elements[kCardCenter], app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(carousel_elements[kCardRight], app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(carousel_elements[kCardFarRight], app_launch, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_leftButton, switch_right, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_rightButton, switch_left, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_leftOuterPanel, app_launch, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(carousel_elements[kCardFarLeft], app_launch, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(ui_Screen1, main_key_switch, (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index e963ed49..afdd4fcc 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -1,6 +1,7 @@ #pragma once #include "components/ui_app_page.hpp" +#include #include class Launch; @@ -15,9 +16,32 @@ class UILaunchPage : public home_base static void load_home_screen(); static void start_startup_gif(); static void create_screen(); + enum LauncherCarouselElement : size_t { + kCardFarLeft = 0, + kCardLeft, + kCardCenter, + kCardRight, + kCardFarRight, + kTitleFarLeft, + kTitleLeft, + kTitleCenter, + kTitleRight, + kTitleFarRight, + kPageDot0, + kPageDot1, + kPageDot2, + kPageDot3, + kPageDot4, + kLauncherCarouselElementCount, + }; + static void init_input_group(); static void bind_home_input_group(); static lv_group_t *home_input_group(); + static lv_obj_t *panel(size_t slot); + static lv_obj_t *label(size_t slot); + + static std::array carousel_elements; private: static void init_images(); diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 2afcb8ef..41b97144 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -45,37 +45,20 @@ class UIAppTopBar ui_TOP_Container = lv_obj_create(parent); lv_obj_remove_style_all(ui_TOP_Container); lv_obj_set_size(ui_TOP_Container, 320, 20); - lv_obj_set_pos(ui_TOP_Container, 0, 0); - lv_obj_set_align(ui_TOP_Container, LV_ALIGN_TOP_LEFT); lv_obj_clear_flag(ui_TOP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); lv_obj_set_flex_flow(ui_TOP_Container, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(ui_TOP_Container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_all(ui_TOP_Container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(ui_TOP_Container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - title_label_ = lv_label_create(ui_TOP_Container); - lv_obj_set_width(title_label_, 110); - lv_obj_set_height(title_label_, 20); - lv_label_set_long_mode(title_label_, LV_LABEL_LONG_DOT); - lv_label_set_text(title_label_, title.c_str()); - lv_obj_set_style_text_color(title_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(title_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(title_label_, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(title_label_, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); - - status_bar_ = lv_obj_create(ui_TOP_Container); - lv_obj_remove_style_all(status_bar_); - lv_obj_set_width(status_bar_, 210); - lv_obj_set_height(status_bar_, 20); - lv_obj_clear_flag(status_bar_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW_REVERSE); - lv_obj_set_flex_align(status_bar_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_all(status_bar_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(status_bar_, 4, LV_PART_MAIN | LV_STATE_DEFAULT); - - create_battery(status_bar_); - create_time(status_bar_); - create_wifi(status_bar_); + lv_obj_set_style_pad_left(ui_TOP_Container, 5, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(ui_TOP_Container, 4, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(ui_TOP_Container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(ui_TOP_Container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(ui_TOP_Container, 4, LV_PART_MAIN | LV_STATE_DEFAULT); + + create_title(ui_TOP_Container); + create_spacer(ui_TOP_Container); + create_wifi(ui_TOP_Container); + create_time(ui_TOP_Container); + create_battery(ui_TOP_Container); return ui_TOP_Container; } @@ -99,6 +82,12 @@ class UIAppTopBar { cp0_wifi_status_t ws = cp0_wifi_get_status(); set_wifi_signal(ws.connected ? ws.signal : 0); + if (!wifi_panel_) + return; + if (ws.connected) + lv_obj_clear_flag(wifi_panel_, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(wifi_panel_, LV_OBJ_FLAG_HIDDEN); } void update_battery(const cp0_battery_info_t &bat) @@ -114,6 +103,8 @@ class UIAppTopBar char pwr_buf[16]; snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); lv_label_set_text(power_label_, pwr_buf); + if (battery_bar_) + lv_bar_set_value(battery_bar_, soc, LV_ANIM_OFF); } void update_status() @@ -126,75 +117,116 @@ class UIAppTopBar std::string title_ = "APP"; lv_obj_t *ui_TOP_Container = nullptr; lv_obj_t *title_label_ = nullptr; - lv_obj_t *status_bar_ = nullptr; lv_obj_t *battery_panel_ = nullptr; + lv_obj_t *battery_bar_ = nullptr; lv_obj_t *power_label_ = nullptr; lv_obj_t *time_panel_ = nullptr; lv_obj_t *time_label_ = nullptr; lv_obj_t *wifi_panel_ = nullptr; lv_obj_t *wifi_bars_[4] = {}; - static void clear_panel_style(lv_obj_t *obj) + static lv_font_t *top_title_font() + { + return g_font_bold_14 ? g_font_bold_14 : (lv_font_t *)&lv_font_montserrat_14; + } + + + static void clear_status_panel_style(lv_obj_t *obj) { lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_all(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); } - void create_battery(lv_obj_t *parent) + void create_title(lv_obj_t *parent) { - battery_panel_ = lv_obj_create(parent); - lv_obj_set_size(battery_panel_, 38, 13); - clear_panel_style(battery_panel_); - - power_label_ = lv_label_create(battery_panel_); - lv_obj_set_align(power_label_, LV_ALIGN_CENTER); - lv_label_set_text(power_label_, "100%"); - lv_obj_set_style_text_color(power_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(power_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(power_label_, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + title_label_ = lv_label_create(parent); + lv_label_set_long_mode(title_label_, LV_LABEL_LONG_DOT); + lv_label_set_text(title_label_, title_.c_str()); + lv_obj_set_width(title_label_, 150); + lv_obj_set_height(title_label_, 20); + lv_obj_set_style_text_font(title_label_, top_title_font(), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(title_label_, lv_color_hex(0xCCAA00), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(title_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(title_label_, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } - void create_time(lv_obj_t *parent) + void create_spacer(lv_obj_t *parent) { - time_panel_ = lv_obj_create(parent); - lv_obj_set_size(time_panel_, 40, 13); - clear_panel_style(time_panel_); - - time_label_ = lv_label_create(time_panel_); - lv_obj_set_align(time_label_, LV_ALIGN_CENTER); - lv_label_set_text(time_label_, "19:45"); - lv_obj_set_style_text_color(time_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(time_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(time_label_, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_t *spacer = lv_obj_create(parent); + lv_obj_remove_style_all(spacer); + lv_obj_set_size(spacer, 0, 20); + lv_obj_set_flex_grow(spacer, 1); + lv_obj_clear_flag(spacer, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } void create_wifi(lv_obj_t *parent) { - static const int bar_heights[4] = {3, 6, 7, 9}; + static const int bar_heights[4] = {6, 9, 12, 15}; wifi_panel_ = lv_obj_create(parent); - lv_obj_set_size(wifi_panel_, 30, 13); - clear_panel_style(wifi_panel_); + lv_obj_set_size(wifi_panel_, 22, 15); + clear_status_panel_style(wifi_panel_); lv_obj_set_flex_flow(wifi_panel_, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(wifi_panel_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_END); + lv_obj_set_flex_align(wifi_panel_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_END); lv_obj_set_style_pad_column(wifi_panel_, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(wifi_panel_, LV_OBJ_FLAG_HIDDEN); for (int i = 0; i < 4; ++i) { wifi_bars_[i] = lv_obj_create(wifi_panel_); - lv_obj_remove_style_all(wifi_bars_[i]); - lv_obj_set_size(wifi_bars_[i], 5, bar_heights[i]); - lv_obj_set_style_radius(wifi_bars_[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(wifi_bars_[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_size(wifi_bars_[i], 4, bar_heights[i]); + lv_obj_clear_flag(wifi_bars_[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(wifi_bars_[i], 2, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(wifi_bars_[i], lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(wifi_bars_[i], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_set_style_bg_opa(wifi_bars_[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(wifi_bars_[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT); } } + void create_time(lv_obj_t *parent) + { + time_panel_ = lv_obj_create(parent); + lv_obj_set_size(time_panel_, 40, 16); + clear_status_panel_style(time_panel_); + lv_obj_set_style_bg_img_src(time_panel_, ui_img_time_png, LV_PART_MAIN | LV_STATE_DEFAULT); + + time_label_ = lv_label_create(time_panel_); + lv_obj_set_align(time_label_, LV_ALIGN_CENTER); + lv_label_set_text(time_label_, "15:21"); + lv_obj_set_style_text_color(time_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(time_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } + + void create_battery(lv_obj_t *parent) + { + battery_panel_ = lv_obj_create(parent); + lv_obj_set_size(battery_panel_, 36, 16); + clear_status_panel_style(battery_panel_); + lv_obj_set_style_bg_img_src(battery_panel_, ui_img_battery_bg_png, LV_PART_MAIN | LV_STATE_DEFAULT); + + battery_bar_ = lv_bar_create(battery_panel_); + lv_bar_set_value(battery_bar_, 96, LV_ANIM_OFF); + lv_bar_set_start_value(battery_bar_, 0, LV_ANIM_OFF); + lv_obj_set_size(battery_bar_, 33, 14); + lv_obj_set_align(battery_bar_, LV_ALIGN_CENTER); + lv_obj_set_style_radius(battery_bar_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(battery_bar_, lv_color_hex(0x484847), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(battery_bar_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(battery_bar_, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(battery_bar_, lv_color_hex(0x666633), LV_PART_INDICATOR | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(battery_bar_, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); + + power_label_ = lv_label_create(battery_panel_); + lv_obj_set_align(power_label_, LV_ALIGN_CENTER); + lv_label_set_text(power_label_, "96%"); + lv_obj_set_style_text_color(power_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(power_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } + void set_wifi_signal(int signal) { static const int thresholds[4] = {1, 30, 60, 80}; diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 81b6876e..5ca4e4f3 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -29,7 +29,6 @@ extern "C" void ui_Screen1_screen_init(void); - void launch_circle_init(); void ui_info_bind(); #define LV_EVENT_KEYBOARD_GET_KEY(e) ((struct key_item *)lv_event_get_param(e))->key_code diff --git a/projects/APPLaunch/main/ui/ui_obj.h b/projects/APPLaunch/main/ui/ui_obj.h index 0ebfce23..3b1100f5 100644 --- a/projects/APPLaunch/main/ui/ui_obj.h +++ b/projects/APPLaunch/main/ui/ui_obj.h @@ -11,119 +11,6 @@ UI_DEFINE_OBJECT(ui_batteryPanel) UI_DEFINE_OBJECT(ui_Bar1) UI_DEFINE_OBJECT(ui_powerLabel) UI_DEFINE_OBJECT(ui_APP_Container) -UI_DEFINE_OBJECT(ui_Panel4) -UI_DEFINE_OBJECT(ui_Panel3) -UI_DEFINE_OBJECT(ui_Panel5) -UI_DEFINE_OBJECT(ui_Panel6) -UI_DEFINE_OBJECT(ui_Panel7) -UI_DEFINE_OBJECT(ui_Panel8) -UI_DEFINE_OBJECT(ui_Panel9) -UI_DEFINE_OBJECT(ui_Panel10) -UI_DEFINE_OBJECT(ui_switchLabel) -UI_DEFINE_OBJECT(ui_rightLabel) -UI_DEFINE_OBJECT(ui_leftLabel) -UI_DEFINE_OBJECT(ui_leftPanel) -UI_DEFINE_OBJECT(ui_switchPanel) -UI_DEFINE_OBJECT(ui_rightPanel) -UI_DEFINE_OBJECT(ui_rightOuterPanel) UI_DEFINE_OBJECT(ui_leftButton) UI_DEFINE_OBJECT(ui_rightButton) -UI_DEFINE_OBJECT(ui_leftOuterPanel) -UI_DEFINE_OBJECT(ui_leftOuterLabel) -UI_DEFINE_OBJECT(ui_rightOuterLabel) -UI_DEFINE_OBJECT(ui_Image4) -UI_DEFINE_OBJECT(ui_Panel12) -UI_DEFINE_OBJECT(ui_timeLabel3) -UI_DEFINE_OBJECT(ui_Bar4) -UI_DEFINE_OBJECT(ui_powerLabel3) -UI_DEFINE_OBJECT(ui_Label1) -UI_DEFINE_OBJECT(ui_Label5) -UI_DEFINE_OBJECT(ui_Container4) -UI_DEFINE_OBJECT(ui_Image2) -UI_DEFINE_OBJECT(ui_Panel11) -UI_DEFINE_OBJECT(ui_timeLabel1) -UI_DEFINE_OBJECT(ui_Bar3) -UI_DEFINE_OBJECT(ui_powerLabel1) -UI_DEFINE_OBJECT(ui_APP) -UI_DEFINE_OBJECT(ui_Panel13) -UI_DEFINE_OBJECT(ui_Label4) -UI_DEFINE_OBJECT(ui_Bar5) -UI_DEFINE_OBJECT(ui_Label9) -UI_DEFINE_OBJECT(ui_Button1) -UI_DEFINE_OBJECT(ui_Label10) -UI_DEFINE_OBJECT(ui_Container1) -UI_DEFINE_OBJECT(ui_appinstall) -UI_DEFINE_OBJECT(ui_Label2) -UI_DEFINE_OBJECT(ui_appremove) -UI_DEFINE_OBJECT(ui_Label6) -UI_DEFINE_OBJECT(ui_appupdate) -UI_DEFINE_OBJECT(ui_Label7) -UI_DEFINE_OBJECT(ui_Label3) -UI_DEFINE_OBJECT(ui_Image6) -UI_DEFINE_OBJECT(ui_Image8) -UI_DEFINE_OBJECT(ui_Image10) -UI_DEFINE_OBJECT(ui_Image12) -UI_DEFINE_OBJECT(ui_Image14) -UI_DEFINE_OBJECT(ui_Image16) -UI_DEFINE_OBJECT(ui_Image18) -UI_DEFINE_OBJECT(ui_Image3) -UI_DEFINE_OBJECT(ui_Panel2) -UI_DEFINE_OBJECT(ui_timeLabel2) -UI_DEFINE_OBJECT(ui_Bar2) -UI_DEFINE_OBJECT(ui_powerLabel2) -UI_DEFINE_OBJECT(ui_Label8) -UI_DEFINE_OBJECT(ui_Container2) -UI_DEFINE_OBJECT(ui_claw) -UI_DEFINE_OBJECT(ui_Image5) -UI_DEFINE_OBJECT(ui_Panel14) -UI_DEFINE_OBJECT(ui_timeLabel4) -UI_DEFINE_OBJECT(ui_Bar6) -UI_DEFINE_OBJECT(ui_powerLabel4) -UI_DEFINE_OBJECT(ui_Label11) -UI_DEFINE_OBJECT(ui_Container3) -UI_DEFINE_OBJECT(ui_Image7) -UI_DEFINE_OBJECT(ui_Panel15) -UI_DEFINE_OBJECT(ui_Label13) -UI_DEFINE_OBJECT(ui_Panel16) -UI_DEFINE_OBJECT(ui_Panel20) -UI_DEFINE_OBJECT(ui_Panel21) -UI_DEFINE_OBJECT(ui_Panel23) -UI_DEFINE_OBJECT(ui_Panel22) -UI_DEFINE_OBJECT(ui_Panel17) -UI_DEFINE_OBJECT(ui_Label14) -UI_DEFINE_OBJECT(ui_Container5) -UI_DEFINE_OBJECT(ui_Panel18) -UI_DEFINE_OBJECT(ui_Image9) -UI_DEFINE_OBJECT(ui_Image11) -UI_DEFINE_OBJECT(ui_Label12) -UI_DEFINE_OBJECT(ui_Image13) -UI_DEFINE_OBJECT(ui_Image15) -UI_DEFINE_OBJECT(ui_Image17) -UI_DEFINE_OBJECT(ui_Panel19) -UI_DEFINE_OBJECT(ui_Roller1) -UI_DEFINE_OBJECT(ui_logoLabel) -UI_DEFINE_OBJECT(ui_Panel24) -UI_DEFINE_OBJECT(ui_Label15) -UI_DEFINE_OBJECT(ui_Panel25) -UI_DEFINE_OBJECT(ui_Panel26) -UI_DEFINE_OBJECT(ui_Panel27) -UI_DEFINE_OBJECT(ui_Panel28) -UI_DEFINE_OBJECT(ui_Panel29) -UI_DEFINE_OBJECT(ui_Panel30) -UI_DEFINE_OBJECT(ui_Label16) -UI_DEFINE_OBJECT(ui_Container6) -UI_DEFINE_OBJECT(ui_leftPanel2) -UI_DEFINE_OBJECT(ui_rightButton2) -UI_DEFINE_OBJECT(ui_leftLabel2) -UI_DEFINE_OBJECT(ui_switchLabel2) -UI_DEFINE_OBJECT(ui_rightLabel2) -UI_DEFINE_OBJECT(ui_leftOuterPanel2) -UI_DEFINE_OBJECT(ui_rightOuterPanel2) -UI_DEFINE_OBJECT(ui_leftOuterLabel2) -UI_DEFINE_OBJECT(ui_rightOuterLabel2) -UI_DEFINE_OBJECT(ui_switchPanel2) -UI_DEFINE_OBJECT(ui_rightPanel2) -UI_DEFINE_OBJECT(ui_leftButton2) -UI_DEFINE_OBJECT(ui_Container7) -UI_DEFINE_OBJECT(ui_Panel33) UI_DEFINE_OBJECT(ui____initial_actions0) From 3c2a5d73dbad0435b5314af2cf3e1be7b99f8837 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 16:52:50 +0800 Subject: [PATCH 27/70] refactor(APPLaunch): consolidate UI resources and font management Move launcher image resources to semantic file names and resolve them directly through cp0_file_path_c at LVGL call sites. Replace loose global font pointers with a LauncherFonts manager owned by zero_lvgl_os and keyed by TTF name, size, and style. Clean up launcher page base naming, input accessors, SDL Ctrl-key handling, and move launcher initialization responsibilities under zero_lvgl_os. Verified with: CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j22 --- .../cp0_lvgl/include/cp0_lvgl_app.h | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_file.cpp | 12 +- .../cp0_lvgl/src/sdl/sdl_lvgl_file.cpp | 12 +- .../cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c | 17 +- .../{camera.png => camera_app_icon.png} | Bin ...tail_info.png => carousel_detail_card.png} | Bin .../{down_logo.png => carousel_down_hint.png} | Bin .../{left.png => carousel_left_arrow.png} | Bin ...{left_logo.png => carousel_left_badge.png} | Bin .../{right.png => carousel_right_arrow.png} | Bin ...ight_logo.png => carousel_right_badge.png} | Bin .../{up_logo.png => carousel_up_hint.png} | Bin ..._logo_w.png => carousel_zero_wordmark.png} | Bin .../{zero.png => launcher_brand_logo.png} | Bin ...y_bg.png => status_battery_background.png} | Bin ...time_bg.png => status_time_background.png} | Bin projects/APPLaunch/main/ui/Launch.cpp | 20 +-- projects/APPLaunch/main/ui/UILaunchPage.cpp | 167 ++++++------------ projects/APPLaunch/main/ui/UILaunchPage.h | 3 - .../ui/components/page_app/ui_app_IpPanel.hpp | 8 +- .../ui/components/page_app/ui_app_camera.hpp | 20 +-- .../ui/components/page_app/ui_app_compass.hpp | 30 ++-- .../ui/components/page_app/ui_app_console.hpp | 16 +- .../ui/components/page_app/ui_app_file.hpp | 10 +- .../ui/components/page_app/ui_app_game.hpp | 14 +- .../ui/components/page_app/ui_app_lora.hpp | 8 +- .../ui/components/page_app/ui_app_mesh.hpp | 8 +- .../ui/components/page_app/ui_app_music.hpp | 10 +- .../ui/components/page_app/ui_app_rec.hpp | 16 +- .../ui/components/page_app/ui_app_setup.hpp | 34 ++-- .../ui/components/page_app/ui_app_ssh.hpp | 21 ++- .../page_app/ui_app_tank_battle.hpp | 12 +- .../main/ui/components/ui_app_page.hpp | 152 ++++++++-------- .../main/ui/components/ui_launch_page.hpp | 14 +- projects/APPLaunch/main/ui/ui.c | 41 +---- projects/APPLaunch/main/ui/ui.cpp | 10 +- projects/APPLaunch/main/ui/ui.h | 49 +++-- projects/APPLaunch/main/ui/ui_global_hint.cpp | 3 +- projects/APPLaunch/main/ui/ui_loading.cpp | 3 +- projects/APPLaunch/main/ui/zero_lvgl_os.cpp | 33 +++- projects/APPLaunch/main/ui/zero_lvgl_os.h | 4 + 41 files changed, 354 insertions(+), 394 deletions(-) rename projects/APPLaunch/APPLaunch/share/images/{camera.png => camera_app_icon.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{detail_info.png => carousel_detail_card.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{down_logo.png => carousel_down_hint.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{left.png => carousel_left_arrow.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{left_logo.png => carousel_left_badge.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{right.png => carousel_right_arrow.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{right_logo.png => carousel_right_badge.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{up_logo.png => carousel_up_hint.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{zero_logo_w.png => carousel_zero_wordmark.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{zero.png => launcher_brand_logo.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{battery_bg.png => status_battery_background.png} (100%) rename projects/APPLaunch/APPLaunch/share/images/{time_bg.png => status_time_background.png} (100%) diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index 67ecc3c4..fa06f9bf 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -77,6 +77,7 @@ const char *cp0_config_get_str(const char *key, const char *default_val); void cp0_config_set_str(const char *key, const char *val); void cp0_config_save(void); + const char *cp0_file_path_c(const char *file); int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp index 8efac6c8..f046c96b 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace { constexpr const char *kAppRoot = "/usr/share/APPLaunch"; @@ -36,6 +37,7 @@ bool is_font_ext(const std::string &ext) { return ext == "ttf" || ext == "otf"; } + } // namespace std::string cp0_file_path(std::string file) @@ -76,7 +78,11 @@ std::string cp0_file_path(std::string file) extern "C" const char *cp0_file_path_c(const char *file) { - static thread_local std::string path; - path = cp0_file_path(file ? std::string(file) : std::string()); - return path.c_str(); + static thread_local std::unordered_map paths; + std::string key = file ? std::string(file) : std::string(); + auto it = paths.find(key); + if (it == paths.end()) { + it = paths.emplace(key, cp0_file_path(key)).first; + } + return it->second.c_str(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp index 1d849d92..d56cd2c9 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -215,6 +216,7 @@ std::string app_relative_path(const std::string &root_path, const std::string &f return make_relative_to_cwd(absolute_path); } + } // namespace std::string cp0_file_path(std::string file) @@ -256,7 +258,11 @@ std::string cp0_file_path(std::string file) extern "C" const char *cp0_file_path_c(const char *file) { - static thread_local std::string path; - path = cp0_file_path(file ? std::string(file) : std::string()); - return path.c_str(); + static thread_local std::unordered_map paths; + std::string key = file ? std::string(file) : std::string(); + auto it = paths.find(key); + if (it == paths.end()) { + it = paths.emplace(key, cp0_file_path(key)).first; + } + return it->second.c_str(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c index 32a8a236..4f0d9320 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c @@ -170,6 +170,18 @@ static const char *cp0_sdl_ctrl_utf8(SDL_Keycode key) } } +static bool cp0_sdl_ctrl_letter(const SDL_KeyboardEvent *event, char *out) +{ + SDL_Keymod mods = SDL_GetModState(); + SDL_Keycode sym = event->keysym.sym; + + if ((mods & KMOD_CTRL) == 0 || sym < SDLK_a || sym > SDLK_z) + return false; + + *out = (char)(sym - SDLK_a + 1); + return true; +} + static uint32_t cp0_sdl_scancode_to_linux_key(SDL_Scancode scancode) { switch (scancode) { @@ -551,10 +563,11 @@ void lv_sdl_keyboard_handler(SDL_Event *event) if (event->type == SDL_KEYDOWN) { cp0_sdl_fill_key_meta(kbd, &event->key); uint32_t ctrl_key = cp0_sdl_ctrl_to_lv_key(event->key.keysym.sym); - if (ctrl_key == 0) + char ctrl_char = 0; + if (ctrl_key == 0 && !cp0_sdl_ctrl_letter(&event->key, &ctrl_char)) return; - char ctrl_buf[2] = {(char)ctrl_key, '\0'}; + char ctrl_buf[2] = {ctrl_char != 0 ? ctrl_char : (char)ctrl_key, '\0'}; cp0_sdl_enqueue_text(kbd, ctrl_buf); } else if (event->type == SDL_TEXTINPUT) { diff --git a/projects/APPLaunch/APPLaunch/share/images/camera.png b/projects/APPLaunch/APPLaunch/share/images/camera_app_icon.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/camera.png rename to projects/APPLaunch/APPLaunch/share/images/camera_app_icon.png diff --git a/projects/APPLaunch/APPLaunch/share/images/detail_info.png b/projects/APPLaunch/APPLaunch/share/images/carousel_detail_card.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/detail_info.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_detail_card.png diff --git a/projects/APPLaunch/APPLaunch/share/images/down_logo.png b/projects/APPLaunch/APPLaunch/share/images/carousel_down_hint.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/down_logo.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_down_hint.png diff --git a/projects/APPLaunch/APPLaunch/share/images/left.png b/projects/APPLaunch/APPLaunch/share/images/carousel_left_arrow.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/left.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_left_arrow.png diff --git a/projects/APPLaunch/APPLaunch/share/images/left_logo.png b/projects/APPLaunch/APPLaunch/share/images/carousel_left_badge.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/left_logo.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_left_badge.png diff --git a/projects/APPLaunch/APPLaunch/share/images/right.png b/projects/APPLaunch/APPLaunch/share/images/carousel_right_arrow.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/right.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_right_arrow.png diff --git a/projects/APPLaunch/APPLaunch/share/images/right_logo.png b/projects/APPLaunch/APPLaunch/share/images/carousel_right_badge.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/right_logo.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_right_badge.png diff --git a/projects/APPLaunch/APPLaunch/share/images/up_logo.png b/projects/APPLaunch/APPLaunch/share/images/carousel_up_hint.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/up_logo.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_up_hint.png diff --git a/projects/APPLaunch/APPLaunch/share/images/zero_logo_w.png b/projects/APPLaunch/APPLaunch/share/images/carousel_zero_wordmark.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/zero_logo_w.png rename to projects/APPLaunch/APPLaunch/share/images/carousel_zero_wordmark.png diff --git a/projects/APPLaunch/APPLaunch/share/images/zero.png b/projects/APPLaunch/APPLaunch/share/images/launcher_brand_logo.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/zero.png rename to projects/APPLaunch/APPLaunch/share/images/launcher_brand_logo.png diff --git a/projects/APPLaunch/APPLaunch/share/images/battery_bg.png b/projects/APPLaunch/APPLaunch/share/images/status_battery_background.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/battery_bg.png rename to projects/APPLaunch/APPLaunch/share/images/status_battery_background.png diff --git a/projects/APPLaunch/APPLaunch/share/images/time_bg.png b/projects/APPLaunch/APPLaunch/share/images/status_time_background.png similarity index 100% rename from projects/APPLaunch/APPLaunch/share/images/time_bg.png rename to projects/APPLaunch/APPLaunch/share/images/status_time_background.png diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index fa7b94c0..83f4e13a 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -276,9 +276,9 @@ class LaunchImpl lv_refr_now(NULL); auto p = std::make_shared(); app_Page = p; - lv_disp_load_scr(p->get_ui()); - lv_indev_set_group(lv_indev_get_next(NULL), p->get_key_group()); - p->go_back_home = std::bind(&LaunchImpl::go_back_home, this); + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&LaunchImpl::go_back_home, this); p->terminal_sysplause = sysplause; /* Console page fully covers APP_Container; safe to hide now. * The heavy exec() call below will still run while the terminal @@ -368,7 +368,7 @@ class LaunchImpl } // Parse the INI file - std::string app_name, app_icon, app_exec; + std::string page_title, app_icon, app_exec; bool app_terminal = false; bool app_sysplause = true; bool in_desktop_entry = false; @@ -421,7 +421,7 @@ class LaunchImpl rtrim(value); if (key == "Name") - app_name = value; + page_title = value; else if (key == "Icon") app_icon = value; else if (key == "Exec") @@ -433,7 +433,7 @@ class LaunchImpl } // Name and Exec are required for registration - if (app_name.empty() || app_exec.empty()) + if (page_title.empty() || app_exec.empty()) { fprintf(stderr, "applications_load: skip %s (missing Name or Exec)\n", filepath.c_str()); continue; @@ -453,7 +453,7 @@ class LaunchImpl continue; } - app_list.emplace_back(app_name, app_icon, app_exec, app_terminal, app_sysplause); + app_list.emplace_back(page_title, app_icon, app_exec, app_terminal, app_sysplause); } closedir(dir); @@ -673,10 +673,10 @@ app::app(std::string name, lv_refr_now(NULL); auto p = std::make_shared(); self->app_Page = p; - lv_disp_load_scr(p->get_ui()); + lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), - p->get_key_group()); - p->go_back_home = + p->input_group()); + p->navigate_home = std::bind(&LaunchImpl::go_back_home, self); /* Page is now attached and drawable; hide the overlay. The * next LVGL frame will paint the new page without it. */ diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index a6ab59ae..20995695 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -199,7 +199,7 @@ static void snap_all_panels() // Reset all label fonts to bold for (int i = 5; i < 10; i++) { - lv_obj_set_style_text_font(UILaunchPage::carousel_elements[i], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(UILaunchPage::carousel_elements[i], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); } if (pending_switch) { @@ -427,79 +427,58 @@ void main_key_switch(lv_event_t *e) namespace { -char img_path_buf[16][256]; -char regular_font_path[512]; -char mono_font_path_buf[512]; char gif_path[256]; -const char *font_path = nullptr; -const char *mono_font_path = nullptr; lv_group_t *home_input_group = nullptr; } // namespace lv_obj_t *startup_gif = nullptr; -void UILaunchPage::init_images() +LauncherFonts::~LauncherFonts() { - struct ImagePath { - const char **ptr; - const char *name; - }; - - ImagePath table[] = { - {&ui_img_zero_png, "zero.png"}, - {&ui_img_time_png, "time_bg.png"}, - {&ui_img_battery_bg_png, "battery_bg.png"}, - {&ui_img_left_png, "left.png"}, - {&ui_img_right_png, "right.png"}, - {&ui_img_zero_logo_w_png, "zero_logo_w.png"}, - {&ui_img_left_logo_png, "left_logo.png"}, - {&ui_img_right_logo_png, "right_logo.png"}, - {&ui_img_detail_info_png, "detail_info.png"}, - {&ui_img_down_logo_png, "down_logo.png"}, - {&ui_img_up_logo_png, "up_logo.png"}, - {&ui_img_camera_png, "camera.png"}, - }; - - int count = sizeof(table) / sizeof(table[0]); - for (int i = 0; i < count && i < 16; ++i) { - snprintf(img_path_buf[i], sizeof(img_path_buf[i]), "%s", cp0_file_path(table[i].name).c_str()); - *table[i].ptr = img_path_buf[i]; + release(); +} + +lv_font_t *LauncherFonts::get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style) +{ + const std::string font_key = key(ttf_name, size, style); + auto it = fonts_.find(font_key); + if (it != fonts_.end()) { + return it->second ? it->second : fallback(size); + } + + lv_font_t *font = lv_freetype_font_create(cp0_file_path_c(ttf_name), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, + size, style); + fonts_[font_key] = font; + return font ? font : fallback(size); +} + +void LauncherFonts::release() +{ + for (auto &item : fonts_) { + if (item.second) { + lv_freetype_font_delete(item.second); + item.second = nullptr; + } + } + fonts_.clear(); +} + +lv_font_t *LauncherFonts::fallback(uint16_t size) const +{ + if (size >= 18) { + return (lv_font_t *)&lv_font_montserrat_20; + } + if (size >= 14) { + return (lv_font_t *)&lv_font_montserrat_14; } + return (lv_font_t *)&lv_font_montserrat_12; } -void UILaunchPage::init_fonts() +std::string LauncherFonts::key(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style) { - snprintf(regular_font_path, sizeof(regular_font_path), "%s", cp0_file_path("AlibabaPuHuiTi-3-55-Regular.ttf").c_str()); - snprintf(mono_font_path_buf, sizeof(mono_font_path_buf), "%s", cp0_file_path("LiberationMono-Regular.ttf").c_str()); - font_path = regular_font_path; - mono_font_path = mono_font_path_buf; - - g_font_cn_20 = lv_freetype_font_create(font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 20, - LV_FREETYPE_FONT_STYLE_NORMAL); - g_font_cn_14 = lv_freetype_font_create(font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 14, - LV_FREETYPE_FONT_STYLE_NORMAL); - g_font_cn_12 = lv_freetype_font_create(font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, - LV_FREETYPE_FONT_STYLE_BOLD); - g_font_mono_12 = lv_freetype_font_create(mono_font_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, - LV_FREETYPE_FONT_STYLE_NORMAL); - - char bold_path[512]; - snprintf(bold_path, sizeof(bold_path), "%s", cp0_file_path("Montserrat-Bold.ttf").c_str()); - g_font_bold_20 = lv_freetype_font_create(bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 18, - LV_FREETYPE_FONT_STYLE_BOLD); - g_font_bold_14 = lv_freetype_font_create(bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, - LV_FREETYPE_FONT_STYLE_BOLD); - g_font_bold_12 = lv_freetype_font_create(bold_path, LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 12, - LV_FREETYPE_FONT_STYLE_BOLD); - - if (!g_font_cn_20) g_font_cn_20 = (lv_font_t *)&lv_font_montserrat_20; - if (!g_font_cn_14) g_font_cn_14 = (lv_font_t *)&lv_font_montserrat_14; - if (!g_font_cn_12) g_font_cn_12 = (lv_font_t *)&lv_font_montserrat_12; - if (!g_font_mono_12) g_font_mono_12 = (lv_font_t *)&lv_font_montserrat_12; - if (!g_font_bold_20) g_font_bold_20 = (lv_font_t *)&lv_font_montserrat_18; - if (!g_font_bold_14) g_font_bold_14 = (lv_font_t *)&lv_font_montserrat_14; - if (!g_font_bold_12) g_font_bold_12 = (lv_font_t *)&lv_font_montserrat_12; + return std::string(ttf_name ? ttf_name : "") + "#" + std::to_string(size) + "#" + + std::to_string(static_cast(style)); } lv_group_t *UILaunchPage::home_input_group() @@ -570,42 +549,6 @@ void UILaunchPage::start_startup_gif() lv_disp_load_scr(startup_gif); } -void UILaunchPage::init_ui() -{ - init_images(); - init_fonts(); - - LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); - - lv_disp_t *dispp = lv_disp_get_default(); - lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), - lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT); - lv_disp_set_theme(dispp, theme); - - create_screen(); - - ui_info_bind(); - init_input_group(); - -#ifndef APPLAUNCH_STARTUP_ANIMATION - load_home_screen(); -#else -#ifdef HAL_PLATFORM_SDL - load_home_screen(); -#else - char gif_check[256]; - snprintf(gif_check, sizeof(gif_check), "%s", cp0_file_path("logo_output.gif").c_str()); - FILE *gif_file = fopen(gif_check, "r"); - if (gif_file) { - fclose(gif_file); - start_startup_gif(); - } else { - load_home_screen(); - } -#endif -#endif -} - extern "C" void home_screen_load() { UILaunchPage::load_home_screen(); @@ -616,12 +559,6 @@ extern "C" void start_startup_gif() UILaunchPage::start_startup_gif(); } -extern "C" void ui_inita(void) -{ - UILaunchPage::init_ui(); -} - - UILaunchPage::UILaunchPage(std::shared_ptr launch) : home_base(), launch_(std::move(launch)) { @@ -648,7 +585,7 @@ void UILaunchPage::create_top(lv_obj_t *parent) { #ifdef APPLAUNCH_LOGO_USE_PNG ui_Image1 = lv_img_create(parent); - lv_img_set_src(ui_Image1, ui_img_zero_png); + lv_img_set_src(ui_Image1, cp0_file_path_c("launcher_brand_logo.png")); lv_obj_set_width(ui_Image1, LV_SIZE_CONTENT); lv_obj_set_height(ui_Image1, LV_SIZE_CONTENT); lv_obj_set_x(ui_Image1, 5); @@ -660,7 +597,7 @@ void UILaunchPage::create_top(lv_obj_t *parent) lv_label_set_text(ui_Image1, "ZERO"); lv_obj_set_x(ui_Image1, 5); lv_obj_set_y(ui_Image1, 2); - lv_obj_set_style_text_font(ui_Image1, g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(ui_Image1, launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_color(ui_Image1, lv_color_hex(0xCCAA00), LV_PART_MAIN | LV_STATE_DEFAULT); #endif @@ -731,7 +668,7 @@ void UILaunchPage::create_top(lv_obj_t *parent) lv_obj_set_style_radius(ui_Panel1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_Panel1, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_Panel1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_Panel1, ui_img_time_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_Panel1, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_Panel1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); ui_timeLabel = lv_label_create(ui_Panel1); @@ -752,7 +689,7 @@ void UILaunchPage::create_top(lv_obj_t *parent) lv_obj_set_style_radius(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_batteryPanel, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_batteryPanel, ui_img_battery_bg_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_batteryPanel, cp0_file_path_c("status_battery_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_all(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -863,7 +800,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_y(carousel_elements[kTitleCenter], LABEL_Y_CENTER); lv_obj_set_align(carousel_elements[kTitleCenter], LV_ALIGN_CENTER); lv_label_set_text(carousel_elements[kTitleCenter], "CLI"); - lv_obj_set_style_text_font(carousel_elements[kTitleCenter], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleCenter], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_color(carousel_elements[kTitleCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -875,7 +812,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_align(carousel_elements[kTitleRight], LV_ALIGN_CENTER); lv_label_set_text(carousel_elements[kTitleRight], "GAME"); lv_obj_set_style_text_color(carousel_elements[kTitleRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(carousel_elements[kTitleRight], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); carousel_elements[kTitleLeft] = lv_label_create(::ui_APP_Container); @@ -887,7 +824,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_label_set_text(carousel_elements[kTitleLeft], "STORE"); lv_obj_set_style_text_color(carousel_elements[kTitleLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(carousel_elements[kTitleLeft], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); carousel_elements[kCardLeft] = lv_obj_create(::ui_APP_Container); lv_obj_set_width(carousel_elements[kCardLeft], 80); @@ -954,7 +891,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_radius(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_leftButton, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_leftButton, ui_img_left_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_leftButton, cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_color(ui_leftButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -969,7 +906,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_radius(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_rightButton, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_rightButton, ui_img_right_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_rightButton, cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_color(ui_rightButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -995,7 +932,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_align(carousel_elements[kTitleFarLeft], LV_ALIGN_CENTER); lv_label_set_text(carousel_elements[kTitleFarLeft], "one"); lv_obj_add_flag(carousel_elements[kTitleFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_set_style_text_font(carousel_elements[kTitleFarLeft], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleFarLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_color(carousel_elements[kTitleFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -1007,7 +944,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_align(carousel_elements[kTitleFarRight], LV_ALIGN_CENTER); lv_label_set_text(carousel_elements[kTitleFarRight], "three"); lv_obj_add_flag(carousel_elements[kTitleFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_set_style_text_font(carousel_elements[kTitleFarRight], g_font_bold_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements[kTitleFarRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_color(carousel_elements[kTitleFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index afdd4fcc..4247d6c7 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -12,7 +12,6 @@ class UILaunchPage : public home_base explicit UILaunchPage(std::shared_ptr launch); ~UILaunchPage(); - static void init_ui(); static void load_home_screen(); static void start_startup_gif(); static void create_screen(); @@ -44,8 +43,6 @@ class UILaunchPage : public home_base static std::array carousel_elements; private: - static void init_images(); - static void init_fonts(); static void create_top(lv_obj_t *parent); static void create_app_container(lv_obj_t *parent); diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp index 39d97c55..37b546a9 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp @@ -16,7 +16,7 @@ // VIEW_MAIN — list (auto-refresh every second and show interface information) // ============================================================ -class UIIpPanelPage : public app_base +class UIIpPanelPage : public AppPage { // ==================== Single network interface info ==================== struct NetIfInfo @@ -28,7 +28,7 @@ class UIIpPanelPage : public app_base }; public: - UIIpPanelPage() : app_base() + UIIpPanelPage() : AppPage() { set_page_title("IP INFO"); creat_UI(); @@ -293,7 +293,7 @@ class UIIpPanelPage : public app_base // ==================== Event binding ==================== void event_handler_init() { - lv_obj_add_event_cb(ui_root, UIIpPanelPage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UIIpPanelPage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) { @@ -342,7 +342,7 @@ class UIIpPanelPage : public app_base lv_timer_del(refresh_timer_); refresh_timer_ = nullptr; } - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); break; default: diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp index cb0f2e74..ff57f5b5 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp @@ -270,13 +270,13 @@ class GalleryStore }; } // namespace camera_app -class UICameraPage : public app_base +class UICameraPage : public AppPage { public: - UICameraPage() : app_base() + UICameraPage() : AppPage() { - app_name = "CAMERA"; - set_page_title(app_name); + page_title_ = "CAMERA"; + set_page_title(page_title_); build_ui(); bind_keyboard(); start_camera(); @@ -461,7 +461,7 @@ class UICameraPage : public app_base void build_bottom_bar() { - bottom_bar_ = lv_obj_create(ui_root); + bottom_bar_ = lv_obj_create(root_screen_); camera_app::clear_obj(bottom_bar_); lv_obj_set_size(bottom_bar_, camera_app::kScreenW, camera_app::kBottomH); lv_obj_set_pos(bottom_bar_, 0, 145); @@ -488,7 +488,7 @@ class UICameraPage : public app_base void build_delete_dialog() { - dialog_scrim_ = lv_obj_create(ui_root); + dialog_scrim_ = lv_obj_create(root_screen_); camera_app::clear_obj(dialog_scrim_); lv_obj_set_size(dialog_scrim_, camera_app::kScreenW, 170); lv_obj_set_pos(dialog_scrim_, 0, 0); @@ -541,7 +541,7 @@ class UICameraPage : public app_base void build_info_panel() { - info_scrim_ = lv_obj_create(ui_root); + info_scrim_ = lv_obj_create(root_screen_); camera_app::clear_obj(info_scrim_); lv_obj_set_size(info_scrim_, camera_app::kScreenW, 170); lv_obj_set_pos(info_scrim_, 0, 0); @@ -590,7 +590,7 @@ class UICameraPage : public app_base void bind_keyboard() { - lvgl_add_call(ui_root, [this](lv_event_code_t c, void *event_param, void *) { + lvgl_add_call(root_screen_, [this](lv_event_code_t c, void *event_param, void *) { if (c != static_cast(LV_EVENT_KEYBOARD)) return; struct key_item *key = static_cast(event_param); @@ -733,8 +733,8 @@ class UICameraPage : public app_base show_page(Page::Camera); return; } - if (go_back_home) - go_back_home(); + if (navigate_home) + navigate_home(); } void show_page(Page page) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp index a950ba40..1b3bf43d 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp @@ -34,7 +34,7 @@ * F6/ESC 返回主页 * ============================================================ */ -class UICompassPage : public app_ +class UICompassPage : public AppPageRoot { static constexpr int kScreenW = 320; static constexpr int kScreenH = 170; @@ -59,9 +59,9 @@ class UICompassPage : public app_ static constexpr const char* ICON_LIST = "\uEA04"; // .svgfont-list public: - UICompassPage() : app_() + UICompassPage() : AppPageRoot() { - app_name = "Compass"; + page_title_ = "Compass"; svg_font_ = lv_freetype_font_create( cp0_file_path("svgfont.ttf").c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, LV_FREETYPE_FONT_STYLE_NORMAL); @@ -145,16 +145,16 @@ class UICompassPage : public app_ */ void creat_UI() { - lv_obj_set_size(ui_root, kScreenW, kScreenH); - lv_obj_clear_flag(ui_root, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(ui_root, color(kColorBg), 0); - lv_obj_set_style_bg_opa(ui_root, LV_OPA_COVER, 0); + lv_obj_set_size(root_screen_, kScreenW, kScreenH); + lv_obj_clear_flag(root_screen_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(root_screen_, color(kColorBg), 0); + lv_obj_set_style_bg_opa(root_screen_, LV_OPA_COVER, 0); - create_status_bar(ui_root); - create_compass_panel(ui_root); - create_imu_panel(ui_root); - create_bottom_bar(ui_root); - create_sensor_missing_overlay(ui_root); + create_status_bar(root_screen_); + create_compass_panel(root_screen_); + create_imu_panel(root_screen_); + create_bottom_bar(root_screen_); + create_sensor_missing_overlay(root_screen_); CompassUiState initial_state; IioDevicePaths initial_paths = enumerate_iio_devices(); @@ -635,7 +635,7 @@ class UICompassPage : public app_ */ void event_handler_init() { - lv_obj_add_event_cb(ui_root, UICompassPage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UICompassPage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t* e) @@ -663,8 +663,8 @@ class UICompassPage : public app_ case KEY_F6: case KEY_ESC: - if (go_back_home) { - go_back_home(); + if (navigate_home) { + navigate_home(); } break; diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp index 98e475bd..ad05e8df 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp @@ -34,7 +34,7 @@ // Public API: // exec(std::string cmd) — start a command (supports command strings with arguments) // ============================================================ -class UIConsolePage : public app_base +class UIConsolePage : public AppPage { /* ------------------------------------------------------------------ */ /* Terminal geometry */ @@ -68,7 +68,7 @@ class UIConsolePage : public app_base bool terminal_sysplause = true; public: - UIConsolePage() : app_base() + UIConsolePage() : AppPage() { console_data_init(); creat_console_UI(); @@ -262,7 +262,7 @@ class UIConsolePage : public app_base (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /* --------------------- line-level label ------------------------ */ - const lv_font_t *mono_font = g_font_mono_12 ? g_font_mono_12 : g_font_cn_12; + const lv_font_t *mono_font = launcher_fonts().get("LiberationMono-Regular.ttf", 12, LV_FREETYPE_FONT_STYLE_NORMAL); for (int r = 0; r < ROWS; r++) { lv_obj_t *lbl = lv_label_create(term_canvas); @@ -300,7 +300,7 @@ class UIConsolePage : public app_base /* ================================================================== */ void event_handler_init() { - lv_obj_add_event_cb(ui_root, UIConsolePage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UIConsolePage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) @@ -327,8 +327,8 @@ class UIConsolePage : public app_base else { waiting_key_to_exit = false; - if (go_back_home) - go_back_home(); + if (navigate_home) + navigate_home(); } } else @@ -383,8 +383,8 @@ class UIConsolePage : public app_base SLOGI("[CONSOLE] ESC held 5s -> kill PTY and go back home"); self->stop_pty(); self->terminal_active = false; - if (self->go_back_home) - self->go_back_home(); + if (self->navigate_home) + self->navigate_home(); } } else diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp index 1b39c139..41da5076 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp @@ -21,7 +21,7 @@ // - LEFT or ESC goes to parent (ESC at root goes home) // ============================================================ -class UIFilePage : public app_base +class UIFilePage : public AppPage { struct FileEntry { @@ -31,7 +31,7 @@ class UIFilePage : public app_base }; public: - UIFilePage() : app_base() + UIFilePage() : AppPage() { set_page_title("FILES"); current_path_ = "/"; @@ -350,7 +350,7 @@ class UIFilePage : public app_base // ==================== event binding ==================== void event_handler_init() { - lv_obj_add_event_cb(ui_root, UIFilePage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UIFilePage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) @@ -397,8 +397,8 @@ class UIFilePage : public app_base case KEY_ESC: if (current_path_ != "/") navigate_parent(); - else if (go_back_home) - go_back_home(); + else if (navigate_home) + navigate_home(); break; default: diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp index f1ed6514..462b0876 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp @@ -8,7 +8,7 @@ // ============================================================ // Snake Game UIGamePage -// Screen: 320 x 170 (ui_root 320x170) +// Screen: 320 x 170 (root_screen_ 320x170) // // Layout: // Title bar: 22px with "GAME - Snake" and score @@ -24,7 +24,7 @@ // ENTER - start / restart // ESC - quit to home // ============================================================ -class UIGamePage : public app_ +class UIGamePage : public AppPageRoot { // ---- Screen constants ---- static constexpr int SCREEN_W = 320; // Overall screen width @@ -79,7 +79,7 @@ class UIGamePage : public app_ int score_ = 0; public: - UIGamePage() : app_() + UIGamePage() : AppPageRoot() { creat_UI(); @@ -99,7 +99,7 @@ class UIGamePage : public app_ void creat_UI() { // -- Background panel -- - bg_ = lv_obj_create(ui_root); + bg_ = lv_obj_create(root_screen_); lv_obj_set_size(bg_, SCREEN_W, SCREEN_H); lv_obj_set_pos(bg_, 0, 0); lv_obj_set_style_radius(bg_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -342,7 +342,7 @@ class UIGamePage : public app_ // ==================== Event handling ==================== void event_handler_init() { - lv_obj_add_event_cb(ui_root, UIGamePage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UIGamePage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) @@ -380,7 +380,7 @@ class UIGamePage : public app_ game_start(); break; case KEY_ESC: - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); break; default: break; @@ -430,7 +430,7 @@ class UIGamePage : public app_ game_start(); break; case KEY_ESC: - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); break; default: break; diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp index 51b82c80..ed382dbd 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp @@ -1902,15 +1902,15 @@ void ui_app_lora_destroy(void) -class UILoraPage : public app_base +class UILoraPage : public AppPage { public: - UILoraPage() : app_base() + UILoraPage() : AppPage() { Lora_APP::ui_app_lora_set_go_back([this]() { - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); }); - Lora_APP::ui_app_lora_create(ui_APP_Container, ui_root); + Lora_APP::ui_app_lora_create(ui_APP_Container, root_screen_); } ~UILoraPage() diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp index b6e3eabb..39cb26c5 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp @@ -22,7 +22,7 @@ // Actual LoRa hardware integration requires specific drivers. // ============================================================ -class UIMeshPage : public app_base +class UIMeshPage : public AppPage { enum class ViewState { MAIN, INPUT }; @@ -42,7 +42,7 @@ class UIMeshPage : public app_base }; public: - UIMeshPage() : app_base() + UIMeshPage() : AppPage() { set_page_title("MESH"); srand((unsigned)time(nullptr)); @@ -444,7 +444,7 @@ class UIMeshPage : public app_base // ==================== event binding ==================== void event_handler_init() { - lv_obj_add_event_cb(ui_root, UIMeshPage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UIMeshPage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) { @@ -492,7 +492,7 @@ class UIMeshPage : public app_base lv_timer_delete(heartbeat_timer_); heartbeat_timer_ = nullptr; } - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); break; default: diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp index 239c96a2..3cf1fc5a 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp @@ -47,7 +47,7 @@ // LV_KEY_ESC -> return to main screen / exit app // ============================================================ -class UIMusicPage : public app_base +class UIMusicPage : public AppPage { enum class PlayState { @@ -64,7 +64,7 @@ class UIMusicPage : public app_base }; public: - UIMusicPage() : app_base() + UIMusicPage() : AppPage() { set_page_title("MUSIC"); @@ -389,7 +389,7 @@ class UIMusicPage : public app_base void event_handler_init() { - lv_obj_add_event_cb(ui_root, UIMusicPage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UIMusicPage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) @@ -493,8 +493,8 @@ class UIMusicPage : public app_base break; case LV_KEY_ESC: - SLOGI("[MUSIC] ESC -> go_back_home()"); - go_back_home(); + SLOGI("[MUSIC] ESC -> navigate_home()"); + navigate_home(); break; default: diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp index 5c9e1ba5..0fb5f4e0 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp @@ -30,7 +30,7 @@ #include "compat/input_keys.h" #include "hal_lvgl_bsp.h" -class rec_page : public app_base +class rec_page : public AppPage { public: lv_font_t *svg_font = NULL; @@ -73,10 +73,10 @@ class rec_page : public app_base lv_obj_t *but[5]; public: - rec_page() : app_base() + rec_page() : AppPage() { - app_name = "Recorder"; - set_page_title(app_name); + page_title_ = "Recorder"; + set_page_title(page_title_); svg_font = lv_freetype_font_create( cp0_file_path("svgfont.ttf").c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, 16, LV_FREETYPE_FONT_STYLE_NORMAL); @@ -240,7 +240,7 @@ class rec_page : public app_base void creat_BOTTOM_UI() { - ui_BOTTOM_Container = lv_obj_create(ui_root); + ui_BOTTOM_Container = lv_obj_create(root_screen_); lv_obj_remove_style_all(ui_BOTTOM_Container); lv_obj_set_width(ui_BOTTOM_Container, 320); lv_obj_set_height(ui_BOTTOM_Container, 25); @@ -912,7 +912,7 @@ class UIRecPage : public rec_page void bind_keyboard_shortcuts() { - lvgl_add_call(ui_root, [this](lv_event_code_t c, void *event_param, void *user_data) { + lvgl_add_call(root_screen_, [this](lv_event_code_t c, void *event_param, void *user_data) { (void)user_data; if (c != static_cast(LV_EVENT_KEYBOARD)) return; @@ -1044,8 +1044,8 @@ class UIRecPage : public rec_page stop_recording_to_save(); return; } - if (go_back_home) - go_back_home(); + if (navigate_home) + navigate_home(); } void toggle_record() diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp index e7f30114..dc029979 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp @@ -42,7 +42,7 @@ // Actual HAL integration: WiFi scan/connect, brightness, volume, power, reboot, about // ============================================================ -class UISetupPage : public app_base +class UISetupPage : public AppPage { enum class ViewState { MAIN, SUB, VALUE_SELECT, WIFI_LIST, WIFI_PW }; @@ -61,7 +61,7 @@ class UISetupPage : public app_base }; public: - UISetupPage() : app_base() + UISetupPage() : AppPage() { set_page_title("SETTING"); cache_image_paths(); @@ -417,7 +417,7 @@ class UISetupPage : public app_base } lv_obj_set_pos(title, 8, 2); lv_obj_set_style_text_color(title, lv_color_hex(0x58A6FF), LV_PART_MAIN); - lv_obj_set_style_text_font(title, g_font_bold_12 ? g_font_bold_12 : &lv_font_montserrat_12, LV_PART_MAIN); + lv_obj_set_style_text_font(title, launcher_fonts().get("Montserrat-Bold.ttf", 12, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN); // Column headers lv_obj_t *h1 = lv_label_create(cont); @@ -874,7 +874,7 @@ class UISetupPage : public app_base y += lv_obj_get_height(lbl) + 3; }; - add_line("Help", 0x58A6FF, g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_14); + add_line("Help", 0x58A6FF, launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD)); add_line("Screenshot: Ctrl+Alt+S", 0xCCCCCC, &lv_font_montserrat_12); add_line(" Saved to ~/Screenshots", 0x888888, &lv_font_montserrat_10); add_line("Home: Hold ESC 5s", 0xCCCCCC, &lv_font_montserrat_12); @@ -1253,11 +1253,11 @@ class UISetupPage : public app_base RowStyle style_for_slot(int vi) { int dist = vi > ROW_CENTER ? vi - ROW_CENTER : ROW_CENTER - vi; if (dist == 0) - return {g_font_bold_20 ? g_font_bold_20 : &lv_font_montserrat_20, 0xFFFFFF, MENU_X, 255}; + return {launcher_fonts().get("Montserrat-Bold.ttf", 18, LV_FREETYPE_FONT_STYLE_BOLD), 0xFFFFFF, MENU_X, 255}; if (dist == 1) - return {g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_16, 0xAAAAAA, MENU_X, 220}; + return {launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), 0xAAAAAA, MENU_X, 220}; if (dist == 2) - return {g_font_bold_12 ? g_font_bold_12 : &lv_font_montserrat_14, 0x777777, MENU_X, 170}; + return {launcher_fonts().get("Montserrat-Bold.ttf", 12, LV_FREETYPE_FONT_STYLE_BOLD), 0x777777, MENU_X, 170}; return {&lv_font_montserrat_12, 0x555555, MENU_X, 130}; } @@ -1275,13 +1275,13 @@ class UISetupPage : public app_base int opa; if (!smaller) { if (dist == 0) { - font = g_font_bold_20 ? g_font_bold_20 : &lv_font_montserrat_18; + font = launcher_fonts().get("Montserrat-Bold.ttf", 18, LV_FREETYPE_FONT_STYLE_BOLD); color = 0xFFFFFF; opa = 255; } else if (dist == 1) { - font = g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_16; + font = launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD); color = 0xAAAAAA; opa = 220; } else if (dist == 2) { - font = g_font_bold_12 ? g_font_bold_12 : &lv_font_montserrat_14; + font = launcher_fonts().get("Montserrat-Bold.ttf", 12, LV_FREETYPE_FONT_STYLE_BOLD); color = 0x777777; opa = 170; } else { font = &lv_font_montserrat_12; @@ -1290,10 +1290,10 @@ class UISetupPage : public app_base } else { // Smaller variant for sub-menu / right column if (dist == 0) { - font = g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_16; + font = launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD); color = 0xFFFFFF; opa = 255; } else if (dist == 1) { - font = g_font_bold_12 ? g_font_bold_12 : &lv_font_montserrat_14; + font = launcher_fonts().get("Montserrat-Bold.ttf", 12, LV_FREETYPE_FONT_STYLE_BOLD); color = 0xAAAAAA; opa = 220; } else if (dist == 2) { font = &lv_font_montserrat_12; @@ -1389,7 +1389,7 @@ class UISetupPage : public app_base hint_lbl_ = lv_label_create(cont); lv_label_set_text(hint_lbl_, "ok:enter"); lv_obj_set_style_text_color(hint_lbl_, lv_color_hex(0x00CC66), LV_PART_MAIN); - lv_obj_set_style_text_font(hint_lbl_, g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_14, LV_PART_MAIN); + lv_obj_set_style_text_font(hint_lbl_, launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN); lv_obj_update_layout(hint_lbl_); int hint_w = lv_obj_get_width(hint_lbl_); int hint_h = lv_obj_get_height(hint_lbl_); @@ -1594,7 +1594,7 @@ class UISetupPage : public app_base else lv_label_set_text(hint, "ok:enter"); lv_obj_set_style_text_color(hint, lv_color_hex(0x00CC66), LV_PART_MAIN); - lv_obj_set_style_text_font(hint, g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_14, LV_PART_MAIN); + lv_obj_set_style_text_font(hint, launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN); lv_obj_update_layout(hint); int sub_hint_w = lv_obj_get_width(hint); int sub_hint_h = lv_obj_get_height(hint); @@ -1666,7 +1666,7 @@ class UISetupPage : public app_base lv_obj_t *hint = lv_label_create(cont); lv_label_set_text(hint, "ok:set"); lv_obj_set_style_text_color(hint, lv_color_hex(0x00CC66), LV_PART_MAIN); - lv_obj_set_style_text_font(hint, g_font_bold_14 ? g_font_bold_14 : &lv_font_montserrat_14, LV_PART_MAIN); + lv_obj_set_style_text_font(hint, launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN); lv_obj_update_layout(hint); int val_hint_w = lv_obj_get_width(hint); int val_hint_h = lv_obj_get_height(hint); @@ -1763,7 +1763,7 @@ class UISetupPage : public app_base // ==================== Events ==================== void event_handler_init() { - lv_obj_add_event_cb(ui_root, UISetupPage::static_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UISetupPage::static_handler, LV_EVENT_ALL, this); } static void static_handler(lv_event_t *e) { @@ -1852,7 +1852,7 @@ class UISetupPage : public app_base } case KEY_ESC: play_back(); - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); break; default: break; diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp index 27b63243..da74ab55 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp @@ -18,7 +18,7 @@ // VIEW_TERMINAL -- Embedded UIConsolePage running ssh // ============================================================ -class UISSHPage : public app_base +class UISSHPage : public AppPage { enum class ViewState { INPUT, TERMINAL }; @@ -29,7 +29,7 @@ class UISSHPage : public app_base }; public: - UISSHPage() : app_base() + UISSHPage() : AppPage() { set_page_title("SSH"); fields_.resize(3); @@ -226,20 +226,19 @@ class UISSHPage : public app_base // Create console page console_page_ = std::make_shared(); - // Save our own go_back_home so we can restore the input view - auto self_go_home = this->go_back_home; - console_page_->go_back_home = [this, self_go_home]() { + // Restore the SSH input view when the embedded console exits. + console_page_->navigate_home = [this]() { // Return to the SSH input view console_page_.reset(); // Switch screen back to our root - lv_disp_load_scr(this->get_ui()); - lv_indev_set_group(lv_indev_get_next(NULL), this->get_key_group()); + lv_disp_load_scr(this->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); }; // Switch to console screen view_state_ = ViewState::TERMINAL; - lv_disp_load_scr(console_page_->get_ui()); - lv_indev_set_group(lv_indev_get_next(NULL), console_page_->get_key_group()); + lv_disp_load_scr(console_page_->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); // Launch ssh command console_page_->exec(cmd); @@ -248,7 +247,7 @@ class UISSHPage : public app_base // ==================== event binding ==================== void event_handler_init() { - lv_obj_add_event_cb(ui_root, UISSHPage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UISSHPage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) @@ -289,7 +288,7 @@ class UISSHPage : public app_base break; case KEY_ESC: - if (go_back_home) go_back_home(); + if (navigate_home) navigate_home(); break; case KEY_BACKSPACE: diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp index 8fb6fb94..967fa578 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp @@ -13,7 +13,7 @@ #include #include -class UITankBattlePage : public app_ +class UITankBattlePage : public AppPageRoot { private: enum class Dir @@ -67,7 +67,7 @@ class UITankBattlePage : public app_ static constexpr int GRID_OY = (ARENA_H - GRID_H) / 2; public: - UITankBattlePage() : app_() + UITankBattlePage() : AppPageRoot() { init_game_state(); creat_UI(); @@ -147,7 +147,7 @@ class UITankBattlePage : public app_ private: void creat_UI() { - lv_obj_t *bg = lv_obj_create(ui_root); + lv_obj_t *bg = lv_obj_create(root_screen_); lv_obj_set_size(bg, SCREEN_W, SCREEN_H); lv_obj_set_pos(bg, 0, 0); lv_obj_set_style_radius(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -292,7 +292,7 @@ class UITankBattlePage : public app_ private: void event_handler_init() { - lv_obj_add_event_cb(ui_root, UITankBattlePage::static_lvgl_handler, LV_EVENT_ALL, this); + lv_obj_add_event_cb(root_screen_, UITankBattlePage::static_lvgl_handler, LV_EVENT_ALL, this); } static void static_lvgl_handler(lv_event_t *e) @@ -313,8 +313,8 @@ class UITankBattlePage : public app_ uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); if (key == KEY_ESC) { - if (go_back_home) { - go_back_home(); + if (navigate_home) { + navigate_home(); } return; } diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 41b97144..76102284 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -39,12 +39,21 @@ class UIAppTopBar return create(parent, title_); } + void set_height(int height) + { + height_ = height; + if (ui_TOP_Container) + lv_obj_set_height(ui_TOP_Container, height_); + if (title_label_) + lv_obj_set_height(title_label_, height_); + } + lv_obj_t *create(lv_obj_t *parent, const std::string &title) { title_ = title; ui_TOP_Container = lv_obj_create(parent); lv_obj_remove_style_all(ui_TOP_Container); - lv_obj_set_size(ui_TOP_Container, 320, 20); + lv_obj_set_size(ui_TOP_Container, 320, height_); lv_obj_clear_flag(ui_TOP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); lv_obj_set_flex_flow(ui_TOP_Container, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(ui_TOP_Container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); @@ -124,10 +133,11 @@ class UIAppTopBar lv_obj_t *time_label_ = nullptr; lv_obj_t *wifi_panel_ = nullptr; lv_obj_t *wifi_bars_[4] = {}; + int height_ = 20; static lv_font_t *top_title_font() { - return g_font_bold_14 ? g_font_bold_14 : (lv_font_t *)&lv_font_montserrat_14; + return launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD); } @@ -147,7 +157,7 @@ class UIAppTopBar lv_label_set_long_mode(title_label_, LV_LABEL_LONG_DOT); lv_label_set_text(title_label_, title_.c_str()); lv_obj_set_width(title_label_, 150); - lv_obj_set_height(title_label_, 20); + lv_obj_set_height(title_label_, height_); lv_obj_set_style_text_font(title_label_, top_title_font(), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_color(title_label_, lv_color_hex(0xCCAA00), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(title_label_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -158,7 +168,7 @@ class UIAppTopBar { lv_obj_t *spacer = lv_obj_create(parent); lv_obj_remove_style_all(spacer); - lv_obj_set_size(spacer, 0, 20); + lv_obj_set_size(spacer, 0, height_); lv_obj_set_flex_grow(spacer, 1); lv_obj_clear_flag(spacer, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } @@ -192,7 +202,7 @@ class UIAppTopBar time_panel_ = lv_obj_create(parent); lv_obj_set_size(time_panel_, 40, 16); clear_status_panel_style(time_panel_); - lv_obj_set_style_bg_img_src(time_panel_, ui_img_time_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(time_panel_, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); time_label_ = lv_label_create(time_panel_); lv_obj_set_align(time_label_, LV_ALIGN_CENTER); @@ -206,7 +216,7 @@ class UIAppTopBar battery_panel_ = lv_obj_create(parent); lv_obj_set_size(battery_panel_, 36, 16); clear_status_panel_style(battery_panel_); - lv_obj_set_style_bg_img_src(battery_panel_, ui_img_battery_bg_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(battery_panel_, cp0_file_path_c("status_battery_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); battery_bar_ = lv_bar_create(battery_panel_); lv_bar_set_value(battery_bar_, 96, LV_ANIM_OFF); @@ -277,36 +287,35 @@ class UIAppContainer lv_obj_t *ui_APP_Container = nullptr; }; -using UIAPPCOM = UIAppContainer; -class app_ +class AppPageRoot { public: - std::string app_name = "APP"; - lv_group_t *key_group; - lv_obj_t *ui_root; - lv_obj_t *get_ui() { return ui_root; } - lv_group_t *get_key_group() { return key_group; } - std::function go_back_home; - bool have_bottom = false; - + std::string page_title_ = "APP"; + lv_group_t *input_group_; + lv_obj_t *root_screen_; + lv_obj_t *screen() { return root_screen_; } + lv_group_t *input_group() { return input_group_; } + std::function navigate_home; + bool has_bottom_bar_ = false; + int top_bar_height_px_ = 20; public: - app_() + AppPageRoot() { creat_base_UI(); creat_input_group(); } - virtual ~app_() + virtual ~AppPageRoot() { - lv_obj_del(ui_root); - lv_group_delete(key_group); + lv_obj_del(root_screen_); + lv_group_delete(input_group_); } template lv_obj_t *add_bar(Component &&component) { - return component.create(ui_root); + return component.create(root_screen_); } private: @@ -315,37 +324,38 @@ class app_ /* ================================================================== */ void creat_base_UI() { - ui_root = lv_obj_create(NULL); - lv_obj_clear_flag(ui_root, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_flex_flow(ui_root, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(ui_root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_all(ui_root, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_row(ui_root, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_root, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_root, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + root_screen_ = lv_obj_create(NULL); + lv_obj_clear_flag(root_screen_, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_flex_flow(root_screen_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root_screen_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(root_screen_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(root_screen_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(root_screen_, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(root_screen_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } void creat_input_group() { - key_group = lv_group_create(); - lv_group_add_obj(key_group, ui_root); - // lv_group_focus_obj(ui_root); + input_group_ = lv_group_create(); + lv_group_add_obj(input_group_, root_screen_); + // lv_group_focus_obj(root_screen_); } }; -class UIAppTopBara : virtual public app_ +class AppTopBarRegion : virtual public AppPageRoot { public: - UIAppTopBara() + AppTopBarRegion() { - top_bar_.set_title(app_name); + top_bar_.set_title(page_title_); + top_bar_.set_height(top_bar_height_px_); add_bar(top_bar_); UI_bind_event(); update_status_bar(); status_timer_ = lv_timer_create(app_status_timer_cb, 5000, this); } - virtual ~UIAppTopBara() + virtual ~AppTopBarRegion() { if (status_timer_) lv_timer_delete(status_timer_); @@ -363,7 +373,7 @@ class UIAppTopBara : virtual public app_ void set_page_title(const std::string &title) { - app_name = title; + page_title_ = title; top_bar_.set_title(title); } @@ -373,7 +383,7 @@ class UIAppTopBara : virtual public app_ static void app_battery_event_cb(lv_event_t *e) { - UIAppTopBara *self = static_cast(lv_event_get_user_data(e)); + AppTopBarRegion *self = static_cast(lv_event_get_user_data(e)); if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) return; const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); @@ -383,21 +393,21 @@ class UIAppTopBara : virtual public app_ static void app_status_timer_cb(lv_timer_t *timer) { - UIAppTopBara *self = static_cast(lv_timer_get_user_data(timer)); + AppTopBarRegion *self = static_cast(lv_timer_get_user_data(timer)); if (self) self->update_status_bar(); } void UI_bind_event() { - lv_obj_add_event_cb(ui_root, app_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); + lv_obj_add_event_cb(root_screen_, app_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); } }; -class UIAppAPPBara : virtual public app_ +class AppContentRegion : virtual public AppPageRoot { public: - UIAppAPPBara() + AppContentRegion() { refresh(); ui_APP_Container = add_bar(app_container_); @@ -405,7 +415,7 @@ class UIAppAPPBara : virtual public app_ void refresh() { - app_container_.set_height(have_bottom ? 130 : 150); + app_container_.set_height(has_bottom_bar_ ? 130 : 150); } void refash() @@ -413,7 +423,7 @@ class UIAppAPPBara : virtual public app_ refresh(); } - virtual ~UIAppAPPBara() = default; + virtual ~AppContentRegion() = default; lv_obj_t *ui_APP_Container = nullptr; @@ -421,47 +431,47 @@ class UIAppAPPBara : virtual public app_ UIAppContainer app_container_; }; -class UIAppbottomBara : virtual public app_, virtual public UIAppAPPBara +class AppBottomBarRegion : virtual public AppPageRoot, virtual public AppContentRegion { public: - UIAppbottomBara() + AppBottomBarRegion() { - have_bottom = true; + has_bottom_bar_ = true; refresh(); - ui_BOTTOM_Container = lv_obj_create(ui_root); + ui_BOTTOM_Container = lv_obj_create(root_screen_); lv_obj_remove_style_all(ui_BOTTOM_Container); lv_obj_set_width(ui_BOTTOM_Container, 320); lv_obj_set_height(ui_BOTTOM_Container, 20); lv_obj_clear_flag(ui_BOTTOM_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } - virtual ~UIAppbottomBara() = default; + virtual ~AppBottomBarRegion() = default; lv_obj_t *ui_BOTTOM_Container = nullptr; }; -class app_page_base : virtual public UIAppTopBara, virtual public UIAppAPPBara +class AppPageLayout : virtual public AppTopBarRegion, virtual public AppContentRegion { public: - app_page_base() : app_(), UIAppTopBara(), UIAppAPPBara() + AppPageLayout() : AppPageRoot(), AppTopBarRegion(), AppContentRegion() { } - virtual ~app_page_base() = default; + virtual ~AppPageLayout() = default; }; -class app_with_bottom_bar_base : virtual public UIAppTopBara, virtual public UIAppAPPBara, virtual public UIAppbottomBara +class AppPageWithBottomBarLayout : virtual public AppTopBarRegion, virtual public AppContentRegion, virtual public AppBottomBarRegion { public: - app_with_bottom_bar_base() : app_(), UIAppTopBara(), UIAppAPPBara(), UIAppbottomBara() + AppPageWithBottomBarLayout() : AppPageRoot(), AppTopBarRegion(), AppContentRegion(), AppBottomBarRegion() { } - virtual ~app_with_bottom_bar_base() = default; + virtual ~AppPageWithBottomBarLayout() = default; }; -class home_base : public app_ +class home_base : public AppPageRoot { private: lv_obj_t *ui_TOP_logo; @@ -475,7 +485,7 @@ class home_base : public app_ lv_obj_t *ui_APP_Container; public: - home_base() : app_() + home_base() : AppPageRoot() { creat_Top_UI(); UI_bind_event(); @@ -542,8 +552,8 @@ class home_base : public app_ /* ================================================================== */ void creat_Top_UI() { - ui_TOP_logo = lv_img_create(ui_root); - lv_img_set_src(ui_TOP_logo, ui_img_zero_png); + ui_TOP_logo = lv_img_create(root_screen_); + lv_img_set_src(ui_TOP_logo, cp0_file_path_c("launcher_brand_logo.png")); lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); /// 49 lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); /// 12 lv_obj_set_x(ui_TOP_logo, 5); @@ -551,7 +561,7 @@ class home_base : public app_ lv_obj_add_flag(ui_TOP_logo, LV_OBJ_FLAG_ADV_HITTEST); /// Flags lv_obj_clear_flag(ui_TOP_logo, LV_OBJ_FLAG_SCROLLABLE); /// Flags - ui_TOP_time = lv_obj_create(ui_root); + ui_TOP_time = lv_obj_create(root_screen_); lv_obj_set_width(ui_TOP_time, 45); lv_obj_set_height(ui_TOP_time, 16); lv_obj_set_x(ui_TOP_time, 237); @@ -560,7 +570,7 @@ class home_base : public app_ lv_obj_set_style_radius(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_TOP_time, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_TOP_time, ui_img_time_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_TOP_time, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); ui_TOP_time_Label = lv_label_create(ui_TOP_time); lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 @@ -570,7 +580,7 @@ class home_base : public app_ lv_obj_set_style_text_color(ui_TOP_time_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_TOP_time_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_TOP_Power = lv_bar_create(ui_root); + ui_TOP_Power = lv_bar_create(root_screen_); lv_bar_set_value(ui_TOP_Power, 96, LV_ANIM_OFF); lv_bar_set_start_value(ui_TOP_Power, 0, LV_ANIM_OFF); lv_obj_set_width(ui_TOP_Power, 29); @@ -593,7 +603,7 @@ class home_base : public app_ lv_obj_set_style_text_color(ui_TOP_power_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_TOP_power_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_APP_Container = lv_obj_create(ui_root); + ui_APP_Container = lv_obj_create(root_screen_); lv_obj_remove_style_all(ui_APP_Container); lv_obj_set_width(ui_APP_Container, 320); lv_obj_set_height(ui_APP_Container, 150); @@ -605,24 +615,16 @@ class home_base : public app_ void UI_bind_event() { - lv_obj_add_event_cb(ui_root, home_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); - } -}; - -class tmp_app_bash : public app_page_base -{ -public: - tmp_app_bash() : app_page_base() - { + lv_obj_add_event_cb(root_screen_, home_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); } }; -class app_base : public app_page_base +class AppPage : public AppPageLayout { public: - app_base() : app_page_base() + AppPage() : AppPageLayout() { } - ~app_base() override = default; + ~AppPage() override = default; }; diff --git a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp index 0d523cd6..e6e55630 100644 --- a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp @@ -185,7 +185,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(circle_[1], 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(circle_[1], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(circle_[1], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[1], ui_img_store_logo_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(circle_[1], cp0_file_path_c("store_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(circle_[1], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(circle_[1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -200,7 +200,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(circle_[2], 22, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(circle_[2], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(circle_[2], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[2], ui_img_cli_logo_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(circle_[2], cp0_file_path_c("cli_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(circle_[2], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(circle_[2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -215,7 +215,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(circle_[3], 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(circle_[3], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(circle_[3], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[3], ui_img_claw_logo_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(circle_[3], cp0_file_path_c("claw_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(circle_[3], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(circle_[3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -231,7 +231,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(circle_[0], 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(circle_[0], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(circle_[0], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[0], ui_img_python_logo_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(circle_[0], cp0_file_path_c("python_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(circle_[0], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(circle_[0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -247,7 +247,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(circle_[4], 17, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(circle_[4], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(circle_[4], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[4], ui_img_setting_logo_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(circle_[4], cp0_file_path_c("setting_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(circle_[4], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(circle_[4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); @@ -320,7 +320,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(ui_obj_["ui_rightButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_obj_["ui_rightButton"], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_obj_["ui_rightButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_obj_["ui_rightButton"], ui_img_right_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_obj_["ui_rightButton"], cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_color(ui_obj_["ui_rightButton"], lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_obj_["ui_rightButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_add_event_cb(ui_obj_["ui_rightButton"], [](lv_event_t *e){ @@ -339,7 +339,7 @@ class UILaunchPage : public home_base lv_obj_set_style_radius(ui_obj_["ui_leftButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_obj_["ui_leftButton"], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(ui_obj_["ui_leftButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_obj_["ui_leftButton"], ui_img_left_png, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(ui_obj_["ui_leftButton"], cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_color(ui_obj_["ui_leftButton"], lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(ui_obj_["ui_leftButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_add_event_cb(ui_obj_["ui_leftButton"], [](lv_event_t *e){ diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c index 29540460..c0d917ed 100644 --- a/projects/APPLaunch/main/ui/ui.c +++ b/projects/APPLaunch/main/ui/ui.c @@ -11,13 +11,7 @@ #include "sample_log.h" ///////////////////// VARIABLES //////////////////// - - -// SCREEN: ui_Screen1 - - - - +// LVGL objects exported through ui_obj.h. #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN #define UI_DEFINE_OBJECT( x ) lv_obj_t * x ; @@ -26,39 +20,8 @@ #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN -// CUSTOM VARIABLES +// Launcher animation timing. int Animation_time = 200; -const char *ui_img_zero_png; -const char *ui_img_time_png; -const char *ui_img_battery_bg_png; -const char *ui_img_left_png; -const char *ui_img_right_png; - -const char *ui_img_store_logo_png; -const char *ui_img_cli_logo_png; -const char *ui_img_claw_logo_png; -const char *ui_img_setting_logo_png; -const char *ui_img_python_logo_png; - -const char *ui_img_zero_logo_w_png; -const char *ui_img_left_logo_png; -const char *ui_img_right_logo_png; -const char *ui_img_detail_info_png; -const char *ui_img_down_logo_png; -const char *ui_img_up_logo_png; -const char *ui_img_camera_png; - -static uint32_t EVT_TERM_KEY; - -lv_font_t *g_font_cn_20 = NULL; -lv_font_t *g_font_cn_14 = NULL; -lv_font_t *g_font_cn_12 = NULL; -lv_font_t *g_font_mono_12 = NULL; /* terminal-only monospaced font */ -lv_font_t *g_font_bold_20 = NULL; /* bold font for selected settings item */ -lv_font_t *g_font_bold_14 = NULL; /* bold app-name font - center */ -lv_font_t *g_font_bold_12 = NULL; /* bold app-name font - side */ - -// // EVENTS ///////////////////// TEST LVGL SETTINGS //////////////////// diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp index d80b29ac..bdaa519a 100644 --- a/projects/APPLaunch/main/ui/ui.cpp +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -6,6 +6,7 @@ #include "ui.h" #include #include +#include #include "lvgl/src/widgets/gif/lv_gif.h" #include "cp0_lvgl_app.h" #include "sample_log.h" @@ -17,6 +18,13 @@ std::unique_ptr home; void ui_init(void) { - UILaunchPage::init_ui(); home = std::make_unique(); } + +LauncherFonts &launcher_fonts() +{ + if (!home) { + std::abort(); + } + return *home->fonts_; +} diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 5ca4e4f3..52211aa2 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -53,33 +53,6 @@ extern "C" void ui_console_exit(lv_event_t *e); void ui_console_key(lv_event_t *e); - // CUSTOM VARIABLES - extern const char *ui_img_zero_png; - extern const char *ui_img_time_png; - extern const char *ui_img_battery_bg_png; - extern const char *ui_img_left_png; - extern const char *ui_img_right_png; - extern const char *ui_img_store_logo_png; - extern const char *ui_img_cli_logo_png; - extern const char *ui_img_claw_logo_png; - extern const char *ui_img_setting_logo_png; - extern const char *ui_img_python_logo_png; - - extern const char *ui_img_zero_logo_w_png; - extern const char *ui_img_left_logo_png; - extern const char *ui_img_right_logo_png; - extern const char *ui_img_detail_info_png; - extern const char *ui_img_down_logo_png; - extern const char *ui_img_up_logo_png; - extern const char *ui_img_camera_png; - - extern lv_font_t *g_font_cn_20; - extern lv_font_t *g_font_cn_14; - extern lv_font_t *g_font_cn_12; - extern lv_font_t *g_font_mono_12; - extern lv_font_t *g_font_bold_20; - extern lv_font_t *g_font_bold_14; - extern lv_font_t *g_font_bold_12; // Launcher layout constants #define BORDER_COLOR_CENTER 0x444444 #define BORDER_COLOR_SIDE 0x222222 @@ -103,6 +76,28 @@ extern "C" #ifdef __cplusplus } /*extern "C"*/ + +#include +#include +#include + +class LauncherFonts +{ +public: + LauncherFonts() = default; + ~LauncherFonts(); + + lv_font_t *get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style); + +private: + void release(); + lv_font_t *fallback(uint16_t size) const; + static std::string key(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style); + + std::unordered_map fonts_; +}; + +LauncherFonts &launcher_fonts(); #include "Launch.h" #include "UILaunchPage.h" #include "zero_lvgl_os.h" diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index 6a8ad845..bb94738f 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -151,8 +151,7 @@ static void ensure_hint_created(void) lv_obj_set_style_text_color(s_hint_label, lv_color_hex(HINT_TEXT_COLOR), 0); /* Prefer the project's Chinese-capable 12pt font; it already falls * back to lv_font_montserrat_12 inside ui.c if freetype init failed. */ - lv_font_t *font = g_font_cn_12 ? g_font_cn_12 - : (lv_font_t *)&lv_font_montserrat_12; + lv_font_t *font = launcher_fonts().get("AlibabaPuHuiTi-3-55-Regular.ttf", 12, LV_FREETYPE_FONT_STYLE_BOLD); lv_obj_set_style_text_font(s_hint_label, font, 0); lv_label_set_text(s_hint_label, ""); lv_obj_center(s_hint_label); diff --git a/projects/APPLaunch/main/ui/ui_loading.cpp b/projects/APPLaunch/main/ui/ui_loading.cpp index ef8e24dd..401f6e4d 100644 --- a/projects/APPLaunch/main/ui/ui_loading.cpp +++ b/projects/APPLaunch/main/ui/ui_loading.cpp @@ -86,8 +86,7 @@ static void ensure_loading_created(void) lv_color_hex(LOADING_TEXT_COLOR), 0); /* Use the project's 14pt font with montserrat fallback — matches * the sizing of the existing hint overlay's copy. */ - lv_font_t *font = g_font_cn_14 ? g_font_cn_14 - : (lv_font_t *)&lv_font_montserrat_14; + lv_font_t *font = launcher_fonts().get("AlibabaPuHuiTi-3-55-Regular.ttf", 14, LV_FREETYPE_FONT_STYLE_NORMAL); lv_obj_set_style_text_font(s_loading_label, font, 0); lv_label_set_text(s_loading_label, ""); diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp index 81ac4098..06e8aa47 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp @@ -3,21 +3,52 @@ #include "Launch.h" #include "UILaunchPage.h" +#include + void zero_lvgl_os::creat_display() { + fonts_ = std::make_shared(); + dispp_ = lv_disp_get_default(); theme_ = lv_theme_default_init(dispp_, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT); lv_disp_set_theme(dispp_, theme_); } +void zero_lvgl_os::create_launcher_home() +{ + LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); + + UILaunchPage::create_screen(); + ui_info_bind(); + UILaunchPage::init_input_group(); + +#ifndef APPLAUNCH_STARTUP_ANIMATION + UILaunchPage::load_home_screen(); +#else +#ifdef HAL_PLATFORM_SDL + UILaunchPage::load_home_screen(); +#else + const char *gif_path = cp0_file_path_c("logo_output.gif"); + FILE *gif_file = fopen(gif_path, "r"); + if (gif_file) { + fclose(gif_file); + UILaunchPage::start_startup_gif(); + } else { + UILaunchPage::load_home_screen(); + } +#endif +#endif +} + zero_lvgl_os::zero_lvgl_os() { creat_display(); + create_launcher_home(); + launch_ = std::make_shared(); launch_page_ = std::make_shared(launch_); launch_->set_launch_page(launch_page_); - } zero_lvgl_os::~zero_lvgl_os() = default; diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.h b/projects/APPLaunch/main/ui/zero_lvgl_os.h index 1f145cf6..8e379db5 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.h +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.h @@ -5,6 +5,7 @@ class Launch; class UILaunchPage; +class LauncherFonts; class zero_lvgl_os { @@ -13,10 +14,13 @@ class zero_lvgl_os ~zero_lvgl_os(); private: + friend LauncherFonts &launcher_fonts(); void creat_display(); + void create_launcher_home(); lv_disp_t *dispp_ = nullptr; lv_theme_t *theme_ = nullptr; std::shared_ptr launch_page_; + std::shared_ptr fonts_; std::shared_ptr launch_; }; From 25e0ae3a1f92ce59b3abdbf5adca9e91bfa12850 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:04:01 +0800 Subject: [PATCH 28/70] refactor(APPLaunch): remove launcher C callback bridge Route launcher carousel and app launch actions through the bound Launch and UILaunchPage C++ objects instead of ui_info_bind/cpp_app_* C callbacks. Move launcher home startup to zero_lvgl_os::start so font access occurs after the global home owner is assigned, fixing the SDL black-screen abort. Verified with: CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j22 --- projects/APPLaunch/main/ui/Launch.cpp | 25 -------- projects/APPLaunch/main/ui/UILaunchPage.cpp | 67 +++++++++++++-------- projects/APPLaunch/main/ui/UILaunchPage.h | 12 ++-- projects/APPLaunch/main/ui/ui.cpp | 1 + projects/APPLaunch/main/ui/ui.h | 4 -- projects/APPLaunch/main/ui/ui_events.h | 22 ------- projects/APPLaunch/main/ui/zero_lvgl_os.cpp | 20 +++--- projects/APPLaunch/main/ui/zero_lvgl_os.h | 2 + 8 files changed, 66 insertions(+), 87 deletions(-) diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 83f4e13a..a79c5bbe 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -737,28 +737,3 @@ void Launch::launch_app() if (impl_) impl_->launch_app(); } - -// ============================================================ -std::unique_ptr app_launch_Ser; - -extern "C" -{ - - void ui_info_bind() - { - app_launch_Ser = std::make_unique(); - app_launch_Ser->bind_ui(); - } - void cpp_app_left(lv_obj_t *panel, lv_obj_t *label) - { - app_launch_Ser->update_left_slot(panel, label); - } - void cpp_app_right(lv_obj_t *panel, lv_obj_t *label) - { - app_launch_Ser->update_right_slot(panel, label); - } - void cpp_app_launch() - { - app_launch_Ser->launch_app(); - } -} diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 20995695..3d231845 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -24,10 +24,17 @@ static void rotate_carousel_right(size_t start, size_t end) std::rotate(items.begin() + start, items.begin() + end, items.begin() + end + 1); } -extern "C" { +namespace { typedef void (*switch_cb_t)(lv_event_t *); +UILaunchPage *active_launch_page = nullptr; + +static void switch_left(lv_event_t *e); +static void switch_right(lv_event_t *e); +static void app_launch(lv_event_t *e); +static void main_key_switch(lv_event_t *e); + // ==================== standard layout for carousel slots ==================== struct CarouselSlot { @@ -214,7 +221,7 @@ static void snap_all_panels() // Switch right; called when the right arrow is clicked // ============================================================ -void switch_right(lv_event_t *e) +static void switch_right(lv_event_t *e) { if (is_animating) { @@ -234,7 +241,8 @@ void switch_right(lv_event_t *e) snap_label_to_slot(UILaunchPage::carousel_elements[9], 5); - cpp_app_right(UILaunchPage::carousel_elements[4], UILaunchPage::carousel_elements[9]); + if (active_launch_page) + active_launch_page->update_right_slot(UILaunchPage::carousel_elements[4], UILaunchPage::carousel_elements[9]); switchpanleEnableClick(2, 0); rotate_carousel_right(0, 4); @@ -254,7 +262,7 @@ void switch_right(lv_event_t *e) // Switch left; called when the left arrow is clicked // ============================================================ -void switch_left(lv_event_t *e) +static void switch_left(lv_event_t *e) { if (is_animating) { @@ -274,7 +282,8 @@ void switch_left(lv_event_t *e) snap_label_to_slot(UILaunchPage::carousel_elements[5], 9); - cpp_app_left(UILaunchPage::carousel_elements[0], UILaunchPage::carousel_elements[5]); + if (active_launch_page) + active_launch_page->update_left_slot(UILaunchPage::carousel_elements[0], UILaunchPage::carousel_elements[5]); switchpanleEnableClick(2, 0); rotate_carousel_left(0, 4); @@ -295,14 +304,14 @@ void switch_left(lv_event_t *e) // screen / app // ============================================================ -void go_back_home(lv_event_t *e) +static void go_back_home(lv_event_t *e) { lv_disp_load_scr(ui_Screen1); UILaunchPage::bind_home_input_group(); } -void ui_event_Screen1(lv_event_t *e) +static void ui_event_Screen1(lv_event_t *e) { if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) { @@ -311,9 +320,10 @@ void ui_event_Screen1(lv_event_t *e) } -void app_launch(lv_event_t *e) +static void app_launch(lv_event_t *e) { - cpp_app_launch(); + if (active_launch_page) + active_launch_page->launch_selected_app(); } @@ -345,7 +355,7 @@ static uint32_t fzxc_to_arrow(uint32_t key) static int lvping_lock = 0; -void main_key_switch(lv_event_t *e) +static void main_key_switch(lv_event_t *e) { struct key_item *elm = (struct key_item *)lv_event_get_param(e); uint32_t code = fzxc_to_arrow(elm->key_code); @@ -423,7 +433,7 @@ void main_key_switch(lv_event_t *e) } -} // extern "C" +} // namespace namespace { @@ -535,7 +545,8 @@ static void ui_event_logo_over(lv_event_t *e) SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); if (startup_gif) lv_gif_pause(startup_gif); - UILaunchPage::load_home_screen(); + if (active_launch_page) + active_launch_page->load_home_screen(); } } @@ -549,22 +560,35 @@ void UILaunchPage::start_startup_gif() lv_disp_load_scr(startup_gif); } -extern "C" void home_screen_load() +UILaunchPage::UILaunchPage(std::shared_ptr launch) + : home_base(), launch_(std::move(launch)) { - UILaunchPage::load_home_screen(); + active_launch_page = this; } -extern "C" void start_startup_gif() +UILaunchPage::~UILaunchPage() { - UILaunchPage::start_startup_gif(); + if (active_launch_page == this) + active_launch_page = nullptr; } -UILaunchPage::UILaunchPage(std::shared_ptr launch) - : home_base(), launch_(std::move(launch)) +void UILaunchPage::update_left_slot(lv_obj_t *panel, lv_obj_t *label) { + if (launch_) + launch_->update_left_slot(panel, label); } -UILaunchPage::~UILaunchPage() = default; +void UILaunchPage::update_right_slot(lv_obj_t *panel, lv_obj_t *label) +{ + if (launch_) + launch_->update_right_slot(panel, label); +} + +void UILaunchPage::launch_selected_app() +{ + if (launch_) + launch_->launch_app(); +} void UILaunchPage::create_screen() { @@ -959,8 +983,3 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) } - -extern "C" void ui_Screen1_screen_init(void) -{ - UILaunchPage::create_screen(); -} diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index 4247d6c7..c26af1e9 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -12,9 +12,9 @@ class UILaunchPage : public home_base explicit UILaunchPage(std::shared_ptr launch); ~UILaunchPage(); - static void load_home_screen(); - static void start_startup_gif(); - static void create_screen(); + void load_home_screen(); + void start_startup_gif(); + void create_screen(); enum LauncherCarouselElement : size_t { kCardFarLeft = 0, kCardLeft, @@ -34,12 +34,16 @@ class UILaunchPage : public home_base kLauncherCarouselElementCount, }; - static void init_input_group(); + void init_input_group(); static void bind_home_input_group(); static lv_group_t *home_input_group(); static lv_obj_t *panel(size_t slot); static lv_obj_t *label(size_t slot); + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_selected_app(); + static std::array carousel_elements; private: diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp index bdaa519a..4b95e685 100644 --- a/projects/APPLaunch/main/ui/ui.cpp +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -19,6 +19,7 @@ std::unique_ptr home; void ui_init(void) { home = std::make_unique(); + home->start(); } LauncherFonts &launcher_fonts() diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 52211aa2..997727d9 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -27,10 +27,6 @@ extern "C" // SCREEN: ui_Screen1 - void ui_Screen1_screen_init(void); - - void ui_info_bind(); - #define LV_EVENT_KEYBOARD_GET_KEY(e) ((struct key_item *)lv_event_get_param(e))->key_code #define LV_EVENT_KEYBOARD_GET_KEY_STATE(e) ((struct key_item *)lv_event_get_param(e))->key_state #define IS_KEY_PRESSED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) diff --git a/projects/APPLaunch/main/ui/ui_events.h b/projects/APPLaunch/main/ui/ui_events.h index 19e37652..8e98f497 100644 --- a/projects/APPLaunch/main/ui/ui_events.h +++ b/projects/APPLaunch/main/ui/ui_events.h @@ -6,26 +6,4 @@ #ifndef _UI_EVENTS_H #define _UI_EVENTS_H -#ifdef __cplusplus -extern "C" { -#endif - - - -void switch_left(lv_event_t *e); -void switch_right(lv_event_t *e); -void app_launch(lv_event_t *e); -void go_back_home(lv_event_t *e); -void main_key_switch(lv_event_t *e); - -void app_card_click(lv_event_t * e); - - -void cpp_app_left(lv_obj_t *panel, lv_obj_t *label); -void cpp_app_right(lv_obj_t *panel, lv_obj_t *label); -void cpp_app_launch(); -#ifdef __cplusplus -} /*extern "C"*/ -#endif - #endif diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp index 06e8aa47..63815172 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp @@ -19,23 +19,23 @@ void zero_lvgl_os::create_launcher_home() { LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); - UILaunchPage::create_screen(); - ui_info_bind(); - UILaunchPage::init_input_group(); + launch_page_->create_screen(); + launch_->bind_ui(); + launch_page_->init_input_group(); #ifndef APPLAUNCH_STARTUP_ANIMATION - UILaunchPage::load_home_screen(); + launch_page_->load_home_screen(); #else #ifdef HAL_PLATFORM_SDL - UILaunchPage::load_home_screen(); + launch_page_->load_home_screen(); #else const char *gif_path = cp0_file_path_c("logo_output.gif"); FILE *gif_file = fopen(gif_path, "r"); if (gif_file) { fclose(gif_file); - UILaunchPage::start_startup_gif(); + launch_page_->start_startup_gif(); } else { - UILaunchPage::load_home_screen(); + launch_page_->load_home_screen(); } #endif #endif @@ -44,7 +44,6 @@ void zero_lvgl_os::create_launcher_home() zero_lvgl_os::zero_lvgl_os() { creat_display(); - create_launcher_home(); launch_ = std::make_shared(); launch_page_ = std::make_shared(launch_); @@ -52,3 +51,8 @@ zero_lvgl_os::zero_lvgl_os() } zero_lvgl_os::~zero_lvgl_os() = default; + +void zero_lvgl_os::start() +{ + create_launcher_home(); +} diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.h b/projects/APPLaunch/main/ui/zero_lvgl_os.h index 8e379db5..837e31ef 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.h +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.h @@ -13,6 +13,8 @@ class zero_lvgl_os zero_lvgl_os(); ~zero_lvgl_os(); + void start(); + private: friend LauncherFonts &launcher_fonts(); void creat_display(); From 39376c9295181876c6ddce34444bf54ea01c2379 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:19:48 +0800 Subject: [PATCH 29/70] refactor(APPLaunch): migrate UI C interfaces to C++ Move launcher UI initialization to launcher_ui::init and replace C-style loading overlay functions with ui_loading namespace calls. Replace keyboard and battery event macros with C++ inline helpers, remove the obsolete ui.c/Animation_time globals, and keep the global hint C hook as a compatibility shim over ui_global_hint::on_key. Remove stale generated UI component headers/sources that are no longer referenced. --- projects/APPLaunch/main/src/main.cpp | 2 +- .../ui/Animation/ui_launcher_animation.cpp | 6 +- projects/APPLaunch/main/ui/Launch.cpp | 12 +-- projects/APPLaunch/main/ui/UILaunchPage.cpp | 1 - .../ui/components/page_app/ui_app_compass.hpp | 4 +- .../ui/components/page_app/ui_app_file.hpp | 4 +- .../ui/components/page_app/ui_app_game.hpp | 4 +- .../ui/components/page_app/ui_app_lora.hpp | 6 -- .../ui/components/page_app/ui_app_mesh.hpp | 4 +- .../ui/components/page_app/ui_app_setup.hpp | 4 +- .../ui/components/page_app/ui_app_ssh.hpp | 4 +- .../page_app/ui_app_tank_battle.hpp | 6 +- .../main/ui/components/ui_app_page.hpp | 12 +-- .../APPLaunch/main/ui/components/ui_comp.c | 37 -------- .../APPLaunch/main/ui/components/ui_comp.h | 63 ------------- .../main/ui/components/ui_comp_button1.c | 30 ------ .../main/ui/components/ui_comp_button1.h | 24 ----- .../main/ui/components/ui_comp_hook.c | 10 -- .../main/ui/components/ui_comp_hook.h | 19 ---- projects/APPLaunch/main/ui/ui.c | 59 ------------ projects/APPLaunch/main/ui/ui.cpp | 22 ++++- projects/APPLaunch/main/ui/ui.h | 91 ++++++++++--------- projects/APPLaunch/main/ui/ui_events.h | 9 -- projects/APPLaunch/main/ui/ui_global_hint.cpp | 11 ++- projects/APPLaunch/main/ui/ui_global_hint.h | 8 +- projects/APPLaunch/main/ui/ui_loading.cpp | 12 ++- projects/APPLaunch/main/ui/ui_loading.h | 16 ++-- projects/APPLaunch/main/ui/ui_obj.h | 1 - projects/APPLaunch/main/ui/zero_lvgl_os.cpp | 2 - 29 files changed, 127 insertions(+), 356 deletions(-) delete mode 100644 projects/APPLaunch/main/ui/components/ui_comp.c delete mode 100644 projects/APPLaunch/main/ui/components/ui_comp.h delete mode 100644 projects/APPLaunch/main/ui/components/ui_comp_button1.c delete mode 100644 projects/APPLaunch/main/ui/components/ui_comp_button1.h delete mode 100644 projects/APPLaunch/main/ui/components/ui_comp_hook.c delete mode 100644 projects/APPLaunch/main/ui/components/ui_comp_hook.h delete mode 100644 projects/APPLaunch/main/ui/ui.c delete mode 100644 projects/APPLaunch/main/ui/ui_events.h diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index c6f46838..345b4891 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -68,7 +68,7 @@ int main(void) if (LV_EVENT_KEYBOARD == 0) LV_EVENT_KEYBOARD = lv_event_register_id(); - ui_init(); + launcher_ui::init(); // Force full-screen refresh immediately after init SLOGI("[BOOT] ui_init done, forcing full refresh..."); diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp index d7d64f76..5f7b7830 100644 --- a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp @@ -4,10 +4,10 @@ #include -extern "C" int Animation_time; - namespace { +constexpr int kLauncherAnimationTimeMs = 200; + struct LauncherSlot { lv_coord_t x; lv_coord_t y; @@ -128,7 +128,7 @@ void launcher_home_animate(lv_obj_t **items, bool to_right, launcher_home_animat } LvglAnimation::start_raw( - Animation_time, + kLauncherAnimationTimeMs, [ctx](LvglAnimation *anim) { animate_home(ctx, anim); }, diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index a79c5bbe..d01f8029 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -272,7 +272,7 @@ class LaunchImpl SLOGI("Launching terminal app: %s", exec.c_str()); /* Instant visual feedback; paint before the (potentially slow) * Console page construction so the user sees it right away. */ - ui_loading_show("Loading..."); + ui_loading::show("Loading..."); lv_refr_now(NULL); auto p = std::make_shared(); app_Page = p; @@ -283,7 +283,7 @@ class LaunchImpl /* Console page fully covers APP_Container; safe to hide now. * The heavy exec() call below will still run while the terminal * page is on-screen — no overlay needed at that point. */ - ui_loading_hide(); + ui_loading::hide(); p->exec(exec); } @@ -294,7 +294,7 @@ class LaunchImpl * gets immediate feedback when ENTER was pressed. The overlay * stays drawn on the framebuffer right up until the child takes * it over via cp0_process_exec_blocking(). */ - ui_loading_show("Loading..."); + ui_loading::show("Loading..."); lv_disp_t *disp = lv_disp_get_default(); lv_indev_t *indev = lv_indev_get_next(NULL); LVGL_RUN_FLAGE = 0; @@ -311,7 +311,7 @@ class LaunchImpl lv_disp_load_scr(ui_Screen1); /* Child process has returned; we are back on the launcher home. * Hide the overlay so it doesn't linger. */ - ui_loading_hide(); + ui_loading::hide(); lv_obj_invalidate(lv_screen_active()); lv_refr_now(disp); LVGL_RUN_FLAGE = 1; @@ -669,7 +669,7 @@ app::app(std::string name, * construction starts. Without lv_refr_now() the overlay would * only hit the framebuffer after the constructor returns, which * defeats the whole point. */ - ui_loading_show("Loading..."); + ui_loading::show("Loading..."); lv_refr_now(NULL); auto p = std::make_shared(); self->app_Page = p; @@ -680,7 +680,7 @@ app::app(std::string name, std::bind(&LaunchImpl::go_back_home, self); /* Page is now attached and drawable; hide the overlay. The * next LVGL frame will paint the new page without it. */ - ui_loading_hide(); + ui_loading::hide(); }; } diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 3d231845..e090ac7d 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -529,7 +529,6 @@ void UILaunchPage::init_input_group() void UILaunchPage::load_home_screen() { SLOGI("[HOME] home_screen_load() - loading launcher home screen"); - ui____initial_actions0 = lv_obj_create(NULL); lv_disp_load_scr(ui_Screen1); UILaunchPage::bind_home_input_group(); diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp index 1b3bf43d..033dd1be 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp @@ -648,8 +648,8 @@ class UICompassPage : public AppPageRoot void event_handler(lv_event_t* e) { - if (IS_KEY_RELEASED(e)) { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + if (launcher_ui::events::is_key_released(e)) { + uint32_t key = launcher_ui::events::keyboard_key(e); handle_key(key); } } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp index 41da5076..351cfe5e 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp @@ -361,9 +361,9 @@ class UIFilePage : public AppPage void event_handler(lv_event_t *e) { - if (IS_KEY_RELEASED(e)) + if (launcher_ui::events::is_key_released(e)) { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + uint32_t key = launcher_ui::events::keyboard_key(e); int count = (int)entries_.size(); switch (key) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp index 462b0876..3ef9fe22 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp @@ -353,9 +353,9 @@ class UIGamePage : public AppPageRoot void event_handler(lv_event_t *e) { - if (IS_KEY_RELEASED(e)) + if (launcher_ui::events::is_key_released(e)) { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + uint32_t key = launcher_ui::events::keyboard_key(e); switch (state_) { diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp index ed382dbd..4c4a0015 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp @@ -180,7 +180,6 @@ static bool g_hat_5vout_last_cdev_ok = false; // Back callback static std::function g_go_back_home_fn; -static void (*g_go_back_home_c_fn)(void) = NULL; void ui_app_lora_set_go_back(std::function go_back) { @@ -1732,7 +1731,6 @@ static bool handle_app_key(uint32_t key) if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE || key == LV_KEY_DEL) { if (g_go_back_home_fn) g_go_back_home_fn(); - if (g_go_back_home_c_fn) g_go_back_home_c_fn(); return true; } @@ -1871,10 +1869,6 @@ void lora_app_task() lora_poll_irq_and_update_ui(); } -extern "C" void lora_set_go_back_home(void (*cb)(void)) -{ - g_go_back_home_c_fn = cb; -} void ui_app_lora_destroy(void) { diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp index 39cb26c5..731f24bc 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp @@ -453,9 +453,9 @@ class UIMeshPage : public AppPage } void event_handler(lv_event_t *e) { - if (IS_KEY_RELEASED(e)) + if (launcher_ui::events::is_key_released(e)) { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + uint32_t key = launcher_ui::events::keyboard_key(e); switch (view_state_) { case ViewState::MAIN: handle_main_key(key); break; diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp index dc029979..534ae0e3 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp @@ -1776,8 +1776,8 @@ class UISetupPage : public AppPage void on_event(lv_event_t *e) { - bool released = IS_KEY_RELEASED(e); - bool pressed = IS_KEY_PRESSED(e); + bool released = launcher_ui::events::is_key_released(e); + bool pressed = launcher_ui::events::is_key_pressed(e); if (!released && !pressed) return; struct key_item *elm = (struct key_item *)lv_event_get_param(e); diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp index da74ab55..1f9dc705 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp @@ -261,9 +261,9 @@ class UISSHPage : public AppPage // Only handle input view events; terminal view is handled by UIConsolePage if (view_state_ != ViewState::INPUT) return; - if (IS_KEY_RELEASED(e)) + if (launcher_ui::events::is_key_released(e)) { - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + uint32_t key = launcher_ui::events::keyboard_key(e); switch (key) { diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp index 967fa578..82c3b4ce 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp @@ -306,11 +306,11 @@ class UITankBattlePage : public AppPageRoot void event_handler(lv_event_t *e) { - if (!IS_KEY_PRESSED(e) && !IS_KEY_RELEASED(e)) { + if (!launcher_ui::events::is_key_pressed(e) && !launcher_ui::events::is_key_released(e)) { return; } - uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + uint32_t key = launcher_ui::events::keyboard_key(e); if (key == KEY_ESC) { if (navigate_home) { @@ -319,7 +319,7 @@ class UITankBattlePage : public AppPageRoot return; } - if (!IS_KEY_PRESSED(e)) { + if (!launcher_ui::events::is_key_pressed(e)) { return; } diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 76102284..e8f59f1d 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -384,9 +384,9 @@ class AppTopBarRegion : virtual public AppPageRoot static void app_battery_event_cb(lv_event_t *e) { AppTopBarRegion *self = static_cast(lv_event_get_user_data(e)); - if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) + if (!self || lv_event_get_code(e) != launcher_ui::events::battery_event()) return; - const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); + const cp0_battery_info_t *bat = launcher_ui::events::battery_info(e); if (bat) self->update_battery_status(*bat); } @@ -400,7 +400,7 @@ class AppTopBarRegion : virtual public AppPageRoot void UI_bind_event() { - lv_obj_add_event_cb(root_screen_, app_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); + lv_obj_add_event_cb(root_screen_, app_battery_event_cb, launcher_ui::events::battery_event(), this); } }; @@ -501,9 +501,9 @@ class home_base : public AppPageRoot static void home_battery_event_cb(lv_event_t *e) { home_base *self = static_cast(lv_event_get_user_data(e)); - if (!self || lv_event_get_code(e) != LV_EVENT_BATTERY) + if (!self || lv_event_get_code(e) != launcher_ui::events::battery_event()) return; - const cp0_battery_info_t *bat = LV_EVENT_BATTERY_GET_INFO(e); + const cp0_battery_info_t *bat = launcher_ui::events::battery_info(e); if (bat) self->update_battery_status(*bat); } @@ -615,7 +615,7 @@ class home_base : public AppPageRoot void UI_bind_event() { - lv_obj_add_event_cb(root_screen_, home_battery_event_cb, (lv_event_code_t)LV_EVENT_BATTERY, this); + lv_obj_add_event_cb(root_screen_, home_battery_event_cb, launcher_ui::events::battery_event(), this); } }; diff --git a/projects/APPLaunch/main/ui/components/ui_comp.c b/projects/APPLaunch/main/ui/components/ui_comp.c deleted file mode 100644 index 03c09177..00000000 --- a/projects/APPLaunch/main/ui/components/ui_comp.c +++ /dev/null @@ -1,37 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#include "../ui.h" -#include "ui_comp.h" - -uint32_t LV_EVENT_GET_COMP_CHILD; - -typedef struct { - uint32_t child_idx; - lv_obj_t * child; -} ui_comp_get_child_t; - -lv_obj_t * ui_comp_get_child(lv_obj_t * comp, uint32_t child_idx) -{ - ui_comp_get_child_t info; - info.child = NULL; - info.child_idx = child_idx; - lv_obj_send_event(comp, LV_EVENT_GET_COMP_CHILD, &info); - return info.child; -} - -void get_component_child_event_cb(lv_event_t * e) -{ - lv_obj_t ** c = lv_event_get_user_data(e); - ui_comp_get_child_t * info = lv_event_get_param(e); - info->child = c[info->child_idx]; -} - -void del_component_child_event_cb(lv_event_t * e) -{ - lv_obj_t ** c = lv_event_get_user_data(e); - lv_free(c); -} - diff --git a/projects/APPLaunch/main/ui/components/ui_comp.h b/projects/APPLaunch/main/ui/components/ui_comp.h deleted file mode 100644 index 7469625a..00000000 --- a/projects/APPLaunch/main/ui/components/ui_comp.h +++ /dev/null @@ -1,63 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#ifndef _UI_COMP__H -#define _UI_COMP__H - -#include "../ui.h" - -#ifdef __cplusplus -extern "C" { -#endif - - - -#if LVGL_VERSION_MAJOR >= 9 - #define lv_tabview_create_compat(parent, dir, size) \ - ({ \ - lv_obj_t *_tv = lv_tabview_create(parent); \ - lv_tabview_set_tab_bar_position(_tv, dir); \ - lv_tabview_set_tab_bar_size(_tv, size); \ - _tv; \ - }) -#else - #define lv_tabview_create_compat(parent, dir, size) \ - lv_tabview_create(parent, dir, size) -#endif -// #define lv_tabview_create lv_tabview_create_compat - - - -typedef struct { - uint32_t keysym; // X11/custom key code, or 0 - uint16_t mods; // Ctrl/Alt/Shift... - uint8_t utf8[8]; // complete UTF-8 byte sequence -} term_key_evt_t; - -void get_component_child_event_cb(lv_event_t * e); -void del_component_child_event_cb(lv_event_t * e); - -lv_obj_t * ui_comp_get_child(lv_obj_t * comp, uint32_t child_idx); -extern uint32_t LV_EVENT_GET_COMP_CHILD; -#include "ui_comp_button1.h" - - - - - - - - - - - - - - -#ifdef __cplusplus -} /*extern "C"*/ -#endif - -#endif diff --git a/projects/APPLaunch/main/ui/components/ui_comp_button1.c b/projects/APPLaunch/main/ui/components/ui_comp_button1.c deleted file mode 100644 index 0b8c9a51..00000000 --- a/projects/APPLaunch/main/ui/components/ui_comp_button1.c +++ /dev/null @@ -1,30 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#include "../ui.h" - - -// COMPONENT Button1 - -lv_obj_t * ui_Button1_create(lv_obj_t * comp_parent) -{ - - lv_obj_t * cui_Button1; - cui_Button1 = lv_btn_create(comp_parent); - lv_obj_set_width(cui_Button1, 100); - lv_obj_set_height(cui_Button1, 50); - lv_obj_set_x(cui_Button1, 29); - lv_obj_set_y(cui_Button1, 39); - lv_obj_add_flag(cui_Button1, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags - lv_obj_clear_flag(cui_Button1, LV_OBJ_FLAG_SCROLLABLE); /// Flags - - lv_obj_t ** children = lv_mem_alloc(sizeof(lv_obj_t *) * _UI_COMP_BUTTON1_NUM); - children[UI_COMP_BUTTON1_BUTTON1] = cui_Button1; - lv_obj_add_event_cb(cui_Button1, get_component_child_event_cb, LV_EVENT_GET_COMP_CHILD, children); - lv_obj_add_event_cb(cui_Button1, del_component_child_event_cb, LV_EVENT_DELETE, children); - ui_comp_Button1_create_hook(cui_Button1); - return cui_Button1; -} - diff --git a/projects/APPLaunch/main/ui/components/ui_comp_button1.h b/projects/APPLaunch/main/ui/components/ui_comp_button1.h deleted file mode 100644 index 0b4f5c8f..00000000 --- a/projects/APPLaunch/main/ui/components/ui_comp_button1.h +++ /dev/null @@ -1,24 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#ifndef _UI_COMP_BUTTON1_H -#define _UI_COMP_BUTTON1_H - -#include "../ui.h" - -#ifdef __cplusplus -extern "C" { -#endif - -// COMPONENT Button1 -#define UI_COMP_BUTTON1_BUTTON1 0 -#define _UI_COMP_BUTTON1_NUM 1 -lv_obj_t * ui_Button1_create(lv_obj_t * comp_parent); - -#ifdef __cplusplus -} /*extern "C"*/ -#endif - -#endif diff --git a/projects/APPLaunch/main/ui/components/ui_comp_hook.c b/projects/APPLaunch/main/ui/components/ui_comp_hook.c deleted file mode 100644 index f2a227da..00000000 --- a/projects/APPLaunch/main/ui/components/ui_comp_hook.c +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#include "../ui.h" - -void ui_comp_Button1_create_hook(lv_obj_t * comp) -{ -} diff --git a/projects/APPLaunch/main/ui/components/ui_comp_hook.h b/projects/APPLaunch/main/ui/components/ui_comp_hook.h deleted file mode 100644 index 44f1a3b0..00000000 --- a/projects/APPLaunch/main/ui/components/ui_comp_hook.h +++ /dev/null @@ -1,19 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#ifndef _ZERO_UI_COMP_HOOK_H -#define _ZERO_UI_COMP_HOOK_H - -#ifdef __cplusplus -extern "C" { -#endif - -void ui_comp_Button1_create_hook(lv_obj_t * comp); - -#ifdef __cplusplus -} /*extern "C"*/ -#endif - -#endif diff --git a/projects/APPLaunch/main/ui/ui.c b/projects/APPLaunch/main/ui/ui.c deleted file mode 100644 index c0d917ed..00000000 --- a/projects/APPLaunch/main/ui/ui.c +++ /dev/null @@ -1,59 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#include "ui.h" -#include -#include -#include "lvgl/src/widgets/gif/lv_gif.h" -#include "cp0_lvgl_app.h" -#include "sample_log.h" -///////////////////// VARIABLES //////////////////// - -// LVGL objects exported through ui_obj.h. -#undef UI_DEFINE_OBJECT -#undef UI_DEFINE_EVENT_FUN -#define UI_DEFINE_OBJECT( x ) lv_obj_t * x ; -#define UI_DEFINE_EVENT_FUN(x) ; -#include "ui_obj.h" -#undef UI_DEFINE_OBJECT -#undef UI_DEFINE_EVENT_FUN - -// Launcher animation timing. -int Animation_time = 200; - - -///////////////////// TEST LVGL SETTINGS //////////////////// -#if LV_COLOR_DEPTH != 16 - #error "LV_COLOR_DEPTH should be 16bit to match SquareLine Studio's settings" -#endif -#if LV_COLOR_16_SWAP !=0 - #error "LV_COLOR_16_SWAP should be 0 to match SquareLine Studio's settings" -#endif - - -///////////////////// FUNCTIONS //////////////////// - - - -char* cimg_path(const char *name) -{ - static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s", cp0_file_path(name)); - return path_buf; -} - -char* caudio_path(const char *name) -{ - static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s", cp0_file_path(name)); - return path_buf; -} - -char* cfont_path(const char *name) -{ - static char path_buf[512]; - snprintf(path_buf, sizeof(path_buf), "%s", cp0_file_path(name)); - return path_buf; -} diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp index 4b95e685..39c15f7c 100644 --- a/projects/APPLaunch/main/ui/ui.cpp +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -11,17 +11,35 @@ #include "cp0_lvgl_app.h" #include "sample_log.h" +///////////////////// TEST LVGL SETTINGS //////////////////// +#if LV_COLOR_DEPTH != 16 + #error "LV_COLOR_DEPTH should be 16bit to match SquareLine Studio's settings" +#endif +#if LV_COLOR_16_SWAP != 0 + #error "LV_COLOR_16_SWAP should be 0 to match SquareLine Studio's settings" +#endif +// LVGL objects exported through ui_obj.h. +#undef UI_DEFINE_OBJECT +#undef UI_DEFINE_EVENT_FUN +#define UI_DEFINE_OBJECT(x) lv_obj_t *x; +#define UI_DEFINE_EVENT_FUN(x) +#include "ui_obj.h" +#undef UI_DEFINE_OBJECT +#undef UI_DEFINE_EVENT_FUN +std::unique_ptr home; +namespace launcher_ui { -std::unique_ptr home; -void ui_init(void) +void init() { home = std::make_unique(); home->start(); } +} // namespace launcher_ui + LauncherFonts &launcher_fonts() { if (!home) { diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 997727d9..d2a0a510 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -8,35 +8,11 @@ #include "hal_lvgl_bsp.h" -#ifdef __cplusplus -extern "C" -{ -#endif - #include "lvgl/lvgl.h" -#include "components/ui_comp.h" -#include "components/ui_comp_hook.h" -#include "ui_events.h" #include "keyboard_input.h" #include "cp0_lvgl_app.h" -#define lv_mem_alloc lv_malloc -#define lv_mem_free lv_free -#define lv_event_send(obj, evt, param) lv_obj_send_event(obj, evt, param) - - // SCREEN: ui_Screen1 - -#define LV_EVENT_KEYBOARD_GET_KEY(e) ((struct key_item *)lv_event_get_param(e))->key_code -#define LV_EVENT_KEYBOARD_GET_KEY_STATE(e) ((struct key_item *)lv_event_get_param(e))->key_state -#define IS_KEY_PRESSED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) -#define IS_KEY_RELEASED(e) ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) - -#define LV_EVENT_BATTERY lv_c_event[CP0_C_EVENT_BATTERY] -#define LV_EVENT_DELL_CPP_DATA lv_c_event[CP0_C_EVENT_DELL_CPP_DATA] - -#define LV_EVENT_BATTERY_GET_INFO(e) ((cp0_battery_info_t *)lv_event_get_param(e)) - #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN #define UI_DEFINE_OBJECT(x) extern lv_obj_t *x; @@ -45,38 +21,63 @@ extern "C" #undef UI_DEFINE_OBJECT #undef UI_DEFINE_EVENT_FUN - lv_obj_t *ui_console_creat(lv_event_t *e); - void ui_console_exit(lv_event_t *e); - void ui_console_key(lv_event_t *e); - // Launcher layout constants #define BORDER_COLOR_CENTER 0x444444 #define BORDER_COLOR_SIDE 0x222222 #define LABEL_Y_CENTER 50 #define LABEL_Y_SIDE 50 - // EVENTS - extern lv_obj_t *ui____initial_actions0; - -#ifdef _WIN32 -#define PATH_SEP "\\" -#else -#define PATH_SEP "/" -#endif - char *cimg_path(const char *name); - char *caudio_path(const char *name); - char *cfont_path(const char *name); - - // UI INIT - void ui_init(void); - #ifdef __cplusplus -} /*extern "C"*/ - #include #include #include +namespace launcher_ui { + +namespace events { +inline const struct key_item *keyboard_item(lv_event_t *event) +{ + return static_cast(lv_event_get_param(event)); +} + +inline uint32_t keyboard_key(lv_event_t *event) +{ + const struct key_item *item = keyboard_item(event); + return item ? item->key_code : 0; +} + +inline int keyboard_state(lv_event_t *event) +{ + const struct key_item *item = keyboard_item(event); + return item ? item->key_state : 0; +} + +inline bool is_key_pressed(lv_event_t *event) +{ + return lv_event_get_code(event) == static_cast(LV_EVENT_KEYBOARD) && + keyboard_state(event) > 0; +} + +inline bool is_key_released(lv_event_t *event) +{ + return lv_event_get_code(event) == static_cast(LV_EVENT_KEYBOARD) && + keyboard_state(event) == 0; +} + +inline lv_event_code_t battery_event() +{ + return static_cast(lv_c_event[CP0_C_EVENT_BATTERY]); +} + +inline const cp0_battery_info_t *battery_info(lv_event_t *event) +{ + return static_cast(lv_event_get_param(event)); +} +} // namespace events + +void init(); +} + class LauncherFonts { public: diff --git a/projects/APPLaunch/main/ui/ui_events.h b/projects/APPLaunch/main/ui/ui_events.h deleted file mode 100644 index 8e98f497..00000000 --- a/projects/APPLaunch/main/ui/ui_events.h +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by SquareLine Studio -// SquareLine Studio version: SquareLine Studio 1.5.0 -// LVGL version: 8.3.11 -// Project name: zero - -#ifndef _UI_EVENTS_H -#define _UI_EVENTS_H - -#endif diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index bb94738f..624de7de 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -188,7 +188,9 @@ static void show_hint(const char *text) } } -extern "C" void ui_global_hint_on_key(const struct key_item *elm) +namespace ui_global_hint { + +void on_key(const struct key_item *elm) { if (elm == NULL) return; @@ -283,3 +285,10 @@ extern "C" void ui_global_hint_on_key(const struct key_item *elm) } } } + +} // namespace ui_global_hint + +extern "C" void ui_global_hint_on_key(const struct key_item *elm) +{ + ui_global_hint::on_key(elm); +} diff --git a/projects/APPLaunch/main/ui/ui_global_hint.h b/projects/APPLaunch/main/ui/ui_global_hint.h index 1ab8e3a6..ded790a0 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.h +++ b/projects/APPLaunch/main/ui/ui_global_hint.h @@ -12,12 +12,16 @@ #ifndef UI_GLOBAL_HINT_H #define UI_GLOBAL_HINT_H +struct key_item; + #ifdef __cplusplus +namespace ui_global_hint { +void on_key(const struct key_item *elm); +} + extern "C" { #endif -struct key_item; - /* Call on every key_item dequeued from the keyboard queue. * Decides whether to show a transient toast hint; a no-op for * keys that don't match the rules. diff --git a/projects/APPLaunch/main/ui/ui_loading.cpp b/projects/APPLaunch/main/ui/ui_loading.cpp index 401f6e4d..5bdcd44b 100644 --- a/projects/APPLaunch/main/ui/ui_loading.cpp +++ b/projects/APPLaunch/main/ui/ui_loading.cpp @@ -3,7 +3,7 @@ * * Transient "Loading..." overlay for app launches. * - * Created lazily on first ui_loading_show() call as a child of + * Created lazily on first ui_loading::show() call as a child of * lv_layer_top() so it floats above any screen. Never deleted; * visibility is toggled via LV_OBJ_FLAG_HIDDEN. An lv_spinner sits * to the left of a short text label, both centered on-screen inside @@ -12,7 +12,7 @@ * * Design notes: * - Because internal page construction happens synchronously on the - * LVGL thread, callers should follow ui_loading_show() with + * LVGL thread, callers should follow ui_loading::show() with * lv_refr_now(NULL) to force the overlay to actually paint BEFORE * the slow work begins. Otherwise LVGL would only render on the * next frame, which is after the freeze. @@ -93,7 +93,9 @@ static void ensure_loading_created(void) lv_obj_add_flag(s_loading_obj, LV_OBJ_FLAG_HIDDEN); } -extern "C" void ui_loading_show(const char *label) +namespace ui_loading { + +void show(const char *label) { ensure_loading_created(); if (s_loading_obj == NULL || s_loading_label == NULL) return; @@ -106,8 +108,10 @@ extern "C" void ui_loading_show(const char *label) lv_obj_clear_flag(s_loading_obj, LV_OBJ_FLAG_HIDDEN); } -extern "C" void ui_loading_hide(void) +void hide() { if (s_loading_obj == NULL) return; lv_obj_add_flag(s_loading_obj, LV_OBJ_FLAG_HIDDEN); } + +} // namespace ui_loading diff --git a/projects/APPLaunch/main/ui/ui_loading.h b/projects/APPLaunch/main/ui/ui_loading.h index ac86ac9e..8f6335ce 100644 --- a/projects/APPLaunch/main/ui/ui_loading.h +++ b/projects/APPLaunch/main/ui/ui_loading.h @@ -11,31 +11,27 @@ * * Typical usage from the launcher: * - * ui_loading_show("Loading..."); + * ui_loading::show("Loading..."); * lv_refr_now(NULL); // force the overlay to paint *before* the * // (possibly slow) page construction below * auto p = std::make_shared(); * lv_disp_load_scr(p->get_ui()); - * ui_loading_hide(); + * ui_loading::hide(); */ #ifndef UI_LOADING_H #define UI_LOADING_H -#ifdef __cplusplus -extern "C" { -#endif +namespace ui_loading { /* Show the loading overlay with the given label. Idempotent: calling * repeatedly just updates the text. Safe to call before LVGL is fully * initialised (no-op if lv_layer_top() is not yet available). */ -void ui_loading_show(const char *label); +void show(const char *label); /* Hide the loading overlay. Safe to call when not shown (no-op). */ -void ui_loading_hide(void); +void hide(); -#ifdef __cplusplus -} -#endif +} // namespace ui_loading #endif /* UI_LOADING_H */ diff --git a/projects/APPLaunch/main/ui/ui_obj.h b/projects/APPLaunch/main/ui/ui_obj.h index 3b1100f5..bcd7013e 100644 --- a/projects/APPLaunch/main/ui/ui_obj.h +++ b/projects/APPLaunch/main/ui/ui_obj.h @@ -13,4 +13,3 @@ UI_DEFINE_OBJECT(ui_powerLabel) UI_DEFINE_OBJECT(ui_APP_Container) UI_DEFINE_OBJECT(ui_leftButton) UI_DEFINE_OBJECT(ui_rightButton) -UI_DEFINE_OBJECT(ui____initial_actions0) diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp index 63815172..7f4551ad 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp @@ -17,8 +17,6 @@ void zero_lvgl_os::creat_display() void zero_lvgl_os::create_launcher_home() { - LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); - launch_page_->create_screen(); launch_->bind_ui(); launch_page_->init_input_group(); From 6ced547d344852a047d7df2e5902e99bf78cc8a7 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:24:36 +0800 Subject: [PATCH 30/70] test(cp0_lvgl): simulate SDL battery charge cycle Drive the SDL battery percentage from a sine wave so the status bar can exercise 0 to 100 and back to 0 without real hardware. --- .../cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp index 739f4196..b756efb1 100644 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp @@ -1,4 +1,5 @@ #include "hal/hal_settings.h" +#include #include #include #include @@ -7,12 +8,23 @@ hal_battery_info_t hal_battery_read(void) { hal_battery_info_t info; memset(&info, 0, sizeof(info)); - info.voltage_mv = 3800; - info.current_ma = -200; + const time_t now = time(NULL); + constexpr double kPi = 3.14159265358979323846; + constexpr double kBatteryPeriodSeconds = 20.0; + const double phase = (static_cast(now % static_cast(kBatteryPeriodSeconds)) / + kBatteryPeriodSeconds) * + (2.0 * kPi) - + (kPi / 2.0); + const int soc = static_cast((std::sin(phase) + 1.0) * 50.0 + 0.5); + + info.voltage_mv = 3300 + soc * 9; + info.current_ma = soc < 50 ? 200 : -200; info.temperature_c10 = 350; - info.soc = 85; - info.remain_mah = 2500; + info.soc = soc; + info.remain_mah = soc * 30; info.full_mah = 3000; + info.flags = soc < 50 ? 1 : 0; + info.avg_current_ma = info.current_ma; info.valid = 1; return info; } From e73ffe972dd114dda126391b5bce4bda2ae898d3 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:26:11 +0800 Subject: [PATCH 31/70] docs(APPLaunch): add launcher project guide Add detailed English and Chinese documentation covering the launcher architecture, UI framework, build flow, deployment, extension points, and troubleshooting notes. --- docs/launcher-project-guide.md | 29 + .../00-overview-and-reading-path.md | 87 ++ ...ject-layout-and-module-responsibilities.md | 250 +++++ .../02-runtime-framework-and-boot-flow.md | 358 +++++++ .../03-ui-framework-and-home-carousel.md | 464 +++++++++ ...-application-model-and-launch-mechanism.md | 497 ++++++++++ .../05-built-in-page-framework.md | 333 +++++++ .../06-resources-and-configuration.md | 361 +++++++ .../07-input-system-and-key-mapping.md | 420 ++++++++ .../08-build-and-compilation-guide.md | 907 ++++++++++++++++++ .../09-packaging-deployment-and-systemd.md | 901 +++++++++++++++++ .../10-extension-development-guide.md | 420 ++++++++ .../11-debugging-and-troubleshooting.md | 513 ++++++++++ .../12-common-modification-entry-points.md | 215 +++++ ...46\347\273\206\350\257\264\346\230\216.md" | 27 + ...05\350\257\273\350\267\257\347\272\277.md" | 87 ++ ...41\345\235\227\350\201\214\350\264\243.md" | 250 +++++ ...57\345\212\250\346\265\201\347\250\213.md" | 358 +++++++ ...26\351\241\265\350\275\256\346\222\255.md" | 464 +++++++++ ...57\345\212\250\346\234\272\345\210\266.md" | 497 ++++++++++ ...65\351\235\242\346\241\206\346\236\266.md" | 333 +++++++ ...15\347\275\256\347\263\273\347\273\237.md" | 361 +++++++ ...11\351\224\256\346\230\240\345\260\204.md" | 420 ++++++++ ...26\350\257\221\346\214\207\345\215\227.md" | 907 ++++++++++++++++++ ...203\250\347\275\262\344\270\216systemd.md" | 901 +++++++++++++++++ ...00\345\217\221\346\214\207\345\215\227.md" | 420 ++++++++ ...05\351\232\234\346\216\222\346\237\245.md" | 513 ++++++++++ ...45\345\217\243\351\200\237\346\237\245.md" | 215 +++++ 28 files changed, 11508 insertions(+) create mode 100644 docs/launcher-project-guide.md create mode 100644 docs/launcher-project-guide/00-overview-and-reading-path.md create mode 100644 docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md create mode 100644 docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md create mode 100644 docs/launcher-project-guide/03-ui-framework-and-home-carousel.md create mode 100644 docs/launcher-project-guide/04-application-model-and-launch-mechanism.md create mode 100644 docs/launcher-project-guide/05-built-in-page-framework.md create mode 100644 docs/launcher-project-guide/06-resources-and-configuration.md create mode 100644 docs/launcher-project-guide/07-input-system-and-key-mapping.md create mode 100644 docs/launcher-project-guide/08-build-and-compilation-guide.md create mode 100644 docs/launcher-project-guide/09-packaging-deployment-and-systemd.md create mode 100644 docs/launcher-project-guide/10-extension-development-guide.md create mode 100644 docs/launcher-project-guide/11-debugging-and-troubleshooting.md create mode 100644 docs/launcher-project-guide/12-common-modification-entry-points.md create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" create mode 100644 "docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" diff --git a/docs/launcher-project-guide.md b/docs/launcher-project-guide.md new file mode 100644 index 00000000..a16cdf0b --- /dev/null +++ b/docs/launcher-project-guide.md @@ -0,0 +1,29 @@ +# Launcher Project Guide + +This documentation set describes the `launcher` repository, with a focus on the `projects/APPLaunch` launcher project: architecture, source framework, build flow, runtime behavior, packaging, deployment, extension points, and troubleshooting. + +Recommended reading order: + +1. [00 - Overview and Reading Path](launcher-project-guide/00-overview-and-reading-path.md) +2. [01 - Project Layout and Module Responsibilities](launcher-project-guide/01-project-layout-and-module-responsibilities.md) +3. [02 - Runtime Framework and Boot Flow](launcher-project-guide/02-runtime-framework-and-boot-flow.md) +4. [03 - UI Framework and Home Carousel](launcher-project-guide/03-ui-framework-and-home-carousel.md) +5. [04 - Application Model and Launch Mechanism](launcher-project-guide/04-application-model-and-launch-mechanism.md) +6. [05 - Built-in Page Framework](launcher-project-guide/05-built-in-page-framework.md) +7. [06 - Resources and Configuration](launcher-project-guide/06-resources-and-configuration.md) +8. [07 - Input System and Key Mapping](launcher-project-guide/07-input-system-and-key-mapping.md) +9. [08 - Build and Compilation Guide](launcher-project-guide/08-build-and-compilation-guide.md) +10. [09 - Packaging, Deployment, and systemd](launcher-project-guide/09-packaging-deployment-and-systemd.md) +11. [10 - Extension Development Guide](launcher-project-guide/10-extension-development-guide.md) +12. [11 - Debugging and Troubleshooting](launcher-project-guide/11-debugging-and-troubleshooting.md) +13. [12 - Common Modification Entry Points](launcher-project-guide/12-common-modification-entry-points.md) + +## Quick Links + +- To build and run: see [08 - Build and Compilation Guide](launcher-project-guide/08-build-and-compilation-guide.md). +- To understand how apps are launched: see [04 - Application Model and Launch Mechanism](launcher-project-guide/04-application-model-and-launch-mechanism.md). +- To modify the home UI: see [03 - UI Framework and Home Carousel](launcher-project-guide/03-ui-framework-and-home-carousel.md). +- To add a built-in page: see [10 - Extension Development Guide](launcher-project-guide/10-extension-development-guide.md). +- To debug black screens or startup failures: see [11 - Debugging and Troubleshooting](launcher-project-guide/11-debugging-and-troubleshooting.md). + +Chinese version: [Launcher 工程详细说明](launcher工程详细说明.md) diff --git a/docs/launcher-project-guide/00-overview-and-reading-path.md b/docs/launcher-project-guide/00-overview-and-reading-path.md new file mode 100644 index 00000000..2ab3433c --- /dev/null +++ b/docs/launcher-project-guide/00-overview-and-reading-path.md @@ -0,0 +1,87 @@ +# 00 - Overview and Reading Path + +`launcher` is the application project collection for M5CardputerZero. Its core project is `projects/APPLaunch`. APPLaunch is the main on-device launcher: after boot, it initializes LVGL, displays the home carousel, shows the status bar, launches built-in pages or external applications, and provides features such as settings, terminal, music, recording, camera, and LoRa. + +## 1. Documentation Goals + +This documentation set answers the following questions: + +- What does each directory in this repository do? +- How does the APPLaunch process start, and where is the main loop? +- How is the home carousel UI organized and updated? +- How are built-in pages and external applications registered uniformly with the launcher? +- How are dynamic `.desktop` applications scanned and launched? +- How are resource, font, image, and audio paths resolved? +- How do SDL2 simulation, native on-device builds, and cross compilation work? +- How is the project packaged as a `.deb` and started automatically through systemd? +- How do you add a page, an external application, or a resource? +- How do you troubleshoot black screens, missing resources, or external applications that cannot return? + +## 2. Project in One Sentence + +APPLaunch can be understood as a small LVGL-based desktop environment: + +```text +Linux device / SDL2 simulation + | + v +cp0_lvgl platform adaptation layer + | + v +LVGL 9.5 UI framework + | + v +APPLaunch home, status bar, carousel, application manager + | + +--> Built-in page AppPage + +--> PTY terminal application UIConsolePage + +--> External independent process cp0_process_exec_blocking() +``` + +## 3. Recommended Reading Order + +If you are new to the project, read these documents in the following order: + +1. `01-project-layout-and-module-responsibilities.md`: build an initial understanding of the directory structure. +2. `02-runtime-framework-and-boot-flow.md`: understand the path from `main()` to the home screen. +3. `03-ui-framework-and-home-carousel.md`: understand the home UI and carousel cards. +4. `04-application-model-and-launch-mechanism.md`: understand the application list and launch modes. +5. `08-build-and-compilation-guide.md`: build and run the project. +6. `10-extension-development-guide.md`: add a page or application. + +If you only want to complete a specific task: + +| Task | Read | +| --- | --- | +| Build and run the SDL2 version locally | `08-build-and-compilation-guide.md` | +| Cross-compile for the device | `08-build-and-compilation-guide.md` | +| Package a `.deb` | `09-packaging-deployment-and-systemd.md` | +| Modify the home card layout | `03-ui-framework-and-home-carousel.md` | +| Add a built-in page | `10-extension-development-guide.md` | +| Add a `.desktop` external application | `04-application-model-and-launch-mechanism.md`, `10-extension-development-guide.md` | +| Troubleshoot a black screen | `11-debugging-and-troubleshooting.md` | +| Find the entry file for a feature | `12-common-modification-entry-points.md` | + +## 4. Key Project Paths + +| Path | Description | +| --- | --- | +| `projects/APPLaunch` | Main launcher project | +| `projects/APPLaunch/main/src/main.cpp` | APPLaunch entry point and LVGL main loop | +| `projects/APPLaunch/main/ui/Launch.cpp` | Application list, launch logic, status bar refresh | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home UI, carousel, home key handling | +| `projects/APPLaunch/main/ui/components/page_app` | Built-in page implementations | +| `projects/APPLaunch/APPLaunch` | Resource tree packaged into the runtime environment | +| `ext_components/cp0_lvgl` | Platform adaptation layer that wraps file, process, input, and system interfaces | +| `projects/APPLaunch/tools/llm_pack.py` | Debian package build script | + +## 5. Terminology + +- **APPLaunch**: the launcher project or launcher process. +- **Home screen**: the main screen of APPLaunch, with the status bar and application carousel. +- **Built-in page**: a page class compiled into the APPLaunch process, such as `UISetupPage`. +- **Terminal application**: a command run inside APPLaunch through `UIConsolePage` + PTY, such as `bash`. +- **External application**: an independent executable program. When launched, APPLaunch pauses its own LVGL rendering and waits for the external program to exit. +- **Resource tree**: runtime files such as `APPLaunch/share/images`, `APPLaunch/share/audio`, and `APPLaunch/share/font`. +- **On-device**: the AArch64 Linux environment on M5CardputerZero. +- **SDL2 mode**: running in an SDL2 window on the development machine for simulation. diff --git a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md new file mode 100644 index 00000000..a8b90996 --- /dev/null +++ b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md @@ -0,0 +1,250 @@ +# 01 - Project Layout and Module Responsibilities + +This chapter explains the overall repository structure and the internal structure of the APPLaunch project. + +## 1. Overall Repository Structure + +```text +launcher/ +├── SDK/ +├── ext_components/ +├── projects/ +├── doc/ +├── docs/ +├── README.md +└── README_ZH.md +``` + +### 1.1 `SDK/` + +`SDK` is `M5Stack_Linux_Libs`, which provides the project with: + +- The SCons/Kconfig build framework. +- LVGL components. +- Device drivers, utility functions, and example code. +- Build scripts and the component registration mechanism. + +APPLaunch's `SConstruct` sets: + +```python +os.environ["SDK_PATH"] = str(sdk_path) +``` + +Then it calls: + +```python +env = SConscript( + str(sdk_path / "tools" / "scons" / "project.py"), + variant_dir=os.getcwd(), + duplicate=0, +) +``` + +### 1.2 `ext_components/` + +`ext_components` is the repository's extension component directory. APPLaunch includes it through `EXT_COMPONENTS_PATH`. + +```text +ext_components/ +├── cp0_lvgl/ +├── Miniaudio/ +└── Sigslot/ +``` + +| Component | Role | +| --- | --- | +| `cp0_lvgl` | CardputerZero platform adaptation; wraps LVGL initialization, file paths, input, processes, PTY, and system capabilities | +| `Miniaudio` | Dependency for audio playback and recording | +| `Sigslot` | Signal-slot mechanism | + +### 1.3 `projects/` + +```text +projects/ +├── APPLaunch/ +├── AppStore/ +├── Calculator/ +├── CardputerZero-Emulator/ +├── HelloWorld/ +└── UserDemo/ +``` + +| Project | Description | +| --- | --- | +| `APPLaunch` | Main launcher; the focus of this documentation | +| `AppStore` | Application store; can be launched by APPLaunch as an external application | +| `Calculator` | Calculator application; can be launched by APPLaunch | +| `CardputerZero-Emulator` | Device emulator | +| `HelloWorld` | Minimal example project for learning the build flow | +| `UserDemo` | User demo project | + +### 1.4 `doc/` and `docs/` + +- `doc/`: historical documentation, packaging guides, and helper scripts, such as `APPLaunch-App-打包指南.md` and `store_cache_sync.py`. +- `docs/`: developer-facing documentation. This documentation set is placed here. + +## 2. APPLaunch Top-Level Structure + +```text +projects/APPLaunch/ +├── APPLaunch/ +├── main/ +├── tools/ +├── docs/ +├── SConstruct +├── config_defaults.mk +├── linux_x86_sdl2_config_defaults.mk +├── linux_x86_cross_cp0_config_defaults.mk +├── mac_cross_cp0_config_defaults.mk +├── darwin_config_defaults.mk +└── setup.ini +``` + +### 2.1 Top-Level Build Files + +| File | Description | +| --- | --- | +| `SConstruct` | Project entry point; selects the default configuration, SDK path, cross-compilation sysroot, and invokes the SDK build system | +| `config_defaults.mk` | Default on-device configuration; enables Linux framebuffer / evdev | +| `linux_x86_sdl2_config_defaults.mk` | Linux x86 SDL2 simulation configuration | +| `linux_x86_cross_cp0_config_defaults.mk` | Linux x86 cross-compilation configuration for AArch64 | +| `mac_cross_cp0_config_defaults.mk` | macOS cross-compilation configuration for AArch64 | +| `darwin_config_defaults.mk` | macOS SDL / Darwin-related configuration | + +### 2.2 `APPLaunch/` Runtime Resource Tree + +```text +projects/APPLaunch/APPLaunch/ +├── applications/ +│ └── vim.desktop.temple +├── lib/ +│ └── nihao.so +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +This directory is copied into the runtime directory during build/package creation. After installation on the device, it maps to: + +```text +/usr/share/APPLaunch/ +``` + +Responsibilities of the resource tree: + +- `applications/`: stores `.desktop` description files for external applications. +- `share/images/`: application icons, home carousel images, status bar images, and page images. +- `share/audio/`: startup sound, key sound, and switch sound. +- `share/font/`: TTF fonts. +- `lib/`: library files shipped with the package. + +### 2.3 `main/` Main Source Directory + +```text +projects/APPLaunch/main/ +├── Kconfig +├── SConstruct +├── include/ +├── src/ +└── ui/ +``` + +| Path | Description | +| --- | --- | +| `Kconfig` | Component configuration entry point | +| `SConstruct` | Registers the APPLaunch build target and dependencies | +| `include/` | APPLaunch private headers and compatibility headers | +| `src/main.cpp` | Process entry point, LVGL initialization, and main loop | +| `ui/` | Implementations for all UI pages, the home screen, animations, Loading, and more | + +### 2.4 `main/ui/` UI Directory + +```text +main/ui/ +├── ui.c +├── ui.cpp +├── ui.h +├── ui_obj.h +├── Launch.cpp +├── Launch.h +├── UILaunchPage.cpp +├── UILaunchPage.h +├── ui_loading.cpp +├── ui_loading.h +├── ui_global_hint.cpp +├── ui_global_hint.h +├── zero_lvgl_os.cpp +├── zero_lvgl_os.h +├── Animation/ +└── components/ +``` + +| File/Directory | Role | +| --- | --- | +| `ui.c` / `ui.cpp` / `ui.h` | UI initialization, global objects, and the C/C++ bridge | +| `Launch.cpp` | Application manager; implements application list, launch, status bar refresh, and directory watching | +| `UILaunchPage.cpp` | Home UI creation, carousel slots, key handling, and startup animation | +| `ui_loading.cpp` | Loading overlay | +| `ui_global_hint.cpp` | Global hints | +| `zero_lvgl_os.cpp` | LVGL OS/thread-related helpers | +| `Animation/` | Home carousel animation implementation | +| `components/` | Page base classes, components, and custom pages | + +### 2.5 `components/page_app/` Built-In Page Directory + +```text +main/ui/components/page_app/ +├── ui_app_camera.hpp +├── ui_app_compass.hpp +├── ui_app_console.hpp +├── ui_app_file.hpp +├── ui_app_game.hpp +├── ui_app_lora.hpp +├── ui_app_mesh.hpp +├── ui_app_music.hpp +├── ui_app_rec.hpp +├── ui_app_setup.hpp +├── ui_app_ssh.hpp +├── ui_app_tank_battle.hpp +└── ui_app_IpPanel.hpp +``` + +These pages are usually implemented header-only so they can be automatically included by `generate_page_app_includes.py`. + +## 3. Module Dependencies + +Simplified dependency graph: + +```text +main.cpp + ├── ui/ui.h + ├── cp0_lvgl_app.h + ├── cp0_lvgl_file.hpp + └── hal_lvgl_bsp.h + +ui_init() + ├── UILaunchPage + ├── Launch + ├── ui_loading + └── page_app/* + +LaunchImpl + ├── UILaunchPage::panel()/label() + ├── page_v + ├── cp0_file_path() + ├── cp0_process_* + ├── cp0_dir_watch_* + ├── cp0_wifi_* + └── cp0_battery_* +``` + +## 4. Code Style Characteristics + +APPLaunch currently has several clear code style characteristics: + +- Mixed C and C++: LVGL-generated/compatibility code is often C, while most business pages are C++. +- C/C++ bridge functions are exposed through `extern "C"`, such as `cpp_app_launch()`. +- Page classes usually construct LVGL objects directly without using an additional UI framework. +- Hardware capabilities are preferably accessed through the unified interfaces wrapped by `cp0_lvgl`. +- Resource access should preferably use `cp0_file_path()` to avoid path differences between the device and SDL environments. diff --git a/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md b/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md new file mode 100644 index 00000000..3e9ef5d9 --- /dev/null +++ b/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md @@ -0,0 +1,358 @@ +# 02 - Runtime Framework and Boot Flow + +This chapter explains the full path from the APPLaunch process entry point to the first frame of the home screen. Key references are `projects/APPLaunch/main/src/main.cpp`, `projects/APPLaunch/main/ui/ui.cpp`, `projects/APPLaunch/main/ui/zero_lvgl_os.cpp`, and `projects/APPLaunch/main/ui/UILaunchPage.cpp`. + +## 1. Runtime Framework Overview + +APPLaunch is a single-process LVGL application. The main thread performs platform initialization, creates UI objects, refreshes the first frame, and then enters a loop driven by `lv_timer_handler()`. + +```text +APPLaunch process +├── main.cpp +│ ├── lv_init() +│ ├── cp0_lvgl_init() +│ ├── lv_event_register_id() +│ ├── ui_init() +│ └── while (1) +│ ├── APPLaunch_lock() +│ ├── lv_timer_handler() +│ └── usleep(5000) +└── ui_init() + └── zero_lvgl_os() + ├── creat_display() + ├── Create Launch / UILaunchPage bound objects + └── create_launcher_home() +``` + +Core characteristics: + +- LVGL initialization and platform adaptation initialization are executed only once in `main()`. +- The home UI is created under the control of `zero_lvgl_os`; the actual objects are created in `UILaunchPage::create_screen()`. +- `Launch` / `LaunchImpl` is responsible for the application list, launch modes, status bar refresh, and dynamic application directory watching. +- Immediately after `ui_init()`, the first home frame is forced to refresh through `lv_obj_invalidate()` + `lv_refr_now(NULL)`, avoiding a black screen while waiting for the next natural refresh after startup. + +## 2. Entry Files and Key Source Paths + +| Path | Role | +| --- | --- | +| `projects/APPLaunch/main/src/main.cpp` | Process entry point, LVGL main loop, and external-application runtime lock detection | +| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`, creates the global `zero_lvgl_os home` | +| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | Sets the LVGL theme, creates the home screen, and creates Launch bound objects | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home screen, startup GIF, home loading, and input group | +| `projects/APPLaunch/main/ui/Launch.cpp` | Application manager; launches external/terminal/built-in pages and owns the status bar timer | +| `ext_components/cp0_lvgl` | Wrappers for `cp0_lvgl_init()`, file paths, input, processes, and system capabilities | + +## 3. `main()` Boot Flow + +The framework code for `main()` is as follows: + +```cpp +int main(void) +{ + static const std::string default_lock_file = cp0_file_path("lock_file"); + lock_file = default_lock_file.c_str(); + + lv_init(); + cp0_lvgl_init(); + + if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + + ui_init(); + + lv_obj_invalidate(lv_scr_act()); + lv_refr_now(NULL); + + while (1) { + APPLaunch_lock(); + lv_timer_handler(); + usleep(5000); + } +} +``` + +### 3.1 Initialization Phase + +1. `cp0_file_path("lock_file")` resolves the runtime lock file path. +2. `lv_init()` initializes LVGL core objects, memory, timers, and display/indev abstractions. +3. `cp0_lvgl_init()` initializes the platform layer: display, input, framebuffer/SDL, system signals, and other capabilities. +4. `lv_event_register_id()` registers the custom keyboard event `LV_EVENT_KEYBOARD`. +5. `ui_init()` enters APPLaunch's own UI construction flow. + +### 3.2 First-Frame Refresh + +After `ui_init()` returns, the code immediately executes: + +```cpp +lv_obj_invalidate(lv_scr_act()); +lv_refr_now(NULL); +``` + +The purpose of this step is not an ordinary refresh, but forcing the current active screen content to be flushed to the framebuffer/SDL window. When the home objects have just been created, relying only on later `lv_timer_handler()` calls may briefly show a black screen; forcing the first frame makes startup behavior more deterministic. + +### 3.3 Main Loop + +The main loop runs at a 5 ms cadence: + +```text +Each loop iteration + -> APPLaunch_lock() + -> lv_timer_handler() + -> sleep 5ms +``` + +- `APPLaunch_lock()` checks whether an external application has occupied the foreground. +- `lv_timer_handler()` drives LVGL timers, animations, input events, and redraws. +- `usleep(5000)` controls CPU usage and refresh cadence. + +## 4. From `ui_init()` to Home Object Creation + +`ui_init()` is located in `projects/APPLaunch/main/ui/ui.cpp`: + +```cpp +std::unique_ptr home; + +void ui_init(void) +{ + home = std::make_unique(); +} +``` + +The `zero_lvgl_os` constructor continues with: + +```cpp +zero_lvgl_os::zero_lvgl_os() +{ + creat_display(); + + launch_ = std::make_shared(); + launch_page_ = std::make_shared(launch_); + launch_->set_launch_page(launch_page_); + + create_launcher_home(); +} +``` + +Pay attention to the order here: + +1. `creat_display()` first creates the font manager and sets the LVGL theme. +2. It constructs `Launch` and `UILaunchPage`, then establishes the two-way collaboration relationship through `Launch::set_launch_page()`. +3. `create_launcher_home()` creates the home screen, calls `Launch::bind_ui()` to build the application list, initializes the input group, and displays either the home screen or the startup GIF. + +## 5. Display / Theme Initialization + +The core code of `zero_lvgl_os::creat_display()`: + +```cpp +void zero_lvgl_os::creat_display() +{ + fonts_ = std::make_shared(); + + dispp_ = lv_disp_get_default(); + theme_ = lv_theme_default_init( + dispp_, + lv_palette_main(LV_PALETTE_BLUE), + lv_palette_main(LV_PALETTE_RED), + false, + LV_FONT_DEFAULT); + lv_disp_set_theme(dispp_, theme_); +} +``` + +Notes: + +- `LauncherFonts` is the FreeType font cache shared by the home screen and pages. Its entry function is `launcher_fonts()`. +- `lv_disp_get_default()` depends on `cp0_lvgl_init()` having already registered the display device. +- The theme is only the base theme. Most home controls still have their sizes, colors, background images, and fonts set manually in `UILaunchPage.cpp`. + +## 6. Home Creation and Display Flow + +`zero_lvgl_os::create_launcher_home()` is the main entry point for displaying the home screen: + +```cpp +void zero_lvgl_os::create_launcher_home() +{ + LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); + + launch_page_->create_screen(); + launch_->bind_ui(); + launch_page_->init_input_group(); + +#ifndef APPLAUNCH_STARTUP_ANIMATION + launch_page_->load_home_screen(); +#else +#ifdef HAL_PLATFORM_SDL + launch_page_->load_home_screen(); +#else + const char *gif_path = cp0_file_path_c("logo_output.gif"); + FILE *gif_file = fopen(gif_path, "r"); + if (gif_file) { + fclose(gif_file); + launch_page_->start_startup_gif(); + } else { + launch_page_->load_home_screen(); + } +#endif +#endif +} +``` + +### 6.1 Home Screen Creation + +`UILaunchPage::create_screen()` creates the screen only once: + +```cpp +void UILaunchPage::create_screen() +{ + if (ui_Screen1) + return; + + ui_Screen1 = lv_obj_create(NULL); + lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); + + create_top(ui_Screen1); + create_app_container(ui_Screen1); +} +``` + +It creates two areas: + +- `create_top()`: top-left logo plus WiFi, time, and battery status bar. +- `create_app_container()`: home carousel container, 5 cards, 5 titles, 5 page dots, and left/right arrows. + +### 6.2 Input Group Binding + +The home input group is created in `UILaunchPage::init_input_group()`: + +```cpp +home_input_group = lv_group_create(); +lv_group_add_obj(home_input_group, ui_Screen1); +lv_indev_set_group(indev, home_input_group); +``` + +This allows keyboard events to be delivered to `ui_Screen1`, where `main_key_switch()` handles left/right switching and Enter launch. + +### 6.3 Startup GIF and Home Display + +When `APPLAUNCH_STARTUP_ANIMATION` is enabled and the platform is not SDL: + +```text +Check cp0_file_path_c("logo_output.gif") + -> file exists: UILaunchPage::start_startup_gif() + -> file does not exist: UILaunchPage::load_home_screen() +``` + +`start_startup_gif()` creates an independent GIF screen: + +```cpp +startup_gif = lv_gif_create(NULL); +lv_gif_set_src(startup_gif, gif_path); +lv_obj_center(startup_gif); +lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); +lv_disp_load_scr(startup_gif); +``` + +When GIF playback finishes, it receives `LV_EVENT_READY`. The callback `ui_event_logo_over()` pauses the GIF and loads the home screen: + +```cpp +if (event_code == LV_EVENT_READY && !done) { + if (startup_gif) lv_gif_pause(startup_gif); + UILaunchPage::load_home_screen(); +} +``` + +Responsibilities of `load_home_screen()`: + +```cpp +ui____initial_actions0 = lv_obj_create(NULL); +lv_disp_load_scr(ui_Screen1); +UILaunchPage::bind_home_input_group(); +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +## 7. Boot Sequence Text + +```text +main() + -> cp0_file_path("lock_file") + -> lv_init() + -> cp0_lvgl_init() + -> register LV_EVENT_KEYBOARD + -> ui_init() + -> new zero_lvgl_os + -> creat_display() + -> new LauncherFonts + -> lv_disp_get_default() + -> lv_theme_default_init() + -> new Launch + -> new UILaunchPage(Launch) + -> Launch::set_launch_page() + -> create_launcher_home() + -> register LV_EVENT_GET_COMP_CHILD + -> launch_page_->create_screen() + -> create_top() + -> create_app_container() + -> launch_->bind_ui() + -> new LaunchImpl + -> Register fixed/dynamic applications and write them into home slots + -> Create status bar and application directory watch timers + -> launch_page_->init_input_group() + -> load_home_screen() or start_startup_gif() + -> lv_obj_invalidate(lv_scr_act()) + -> lv_refr_now(NULL) + -> while forever + -> APPLaunch_lock() + -> lv_timer_handler() + -> usleep(5000) +``` + +## 8. External Application Runtime Lock `APPLaunch_lock()` + +`APPLaunch_lock()` coordinates the foreground rendering relationship between APPLaunch and external independent processes. + +```cpp +void APPLaunch_lock() +{ + int holder_pid = 0; + cp0_process_check_lock(lock_file, &holder_pid); + + if (holder_pid == 0) { + LVGL_RUN_FLAGE = 1; + lv_obj_invalidate(lv_scr_act()); + } else { + if (LVGL_HOME_KEY_FLAG) { + // Kill the external application after HOME is held for 5 seconds. + cp0_process_kill(holder_pid, 3000); + } + LVGL_RUN_FLAGE = 0; + } +} +``` + +The actual code has several state variables: + +- `lvgl_lock`: avoids repeatedly restoring LVGL refresh in every loop; it performs `invalidate` once after the lock is released. +- `home_back_status` / `start_time`: track how long the HOME key has been held. +- `holder_pid`: the PID of the external process currently holding the lock file. + +Logic: + +```text +No external application holds the lock + -> APPLaunch restores LVGL_RUN_FLAGE=1 + -> If just recovered from the locked state, redraw the current screen + +An external application holds the lock + -> APPLaunch sets LVGL_RUN_FLAGE=0 and pauses its own rendering + -> If the HOME key has been held for >= 5 seconds, try to kill the external application +``` + +## 9. Notes + +- `ui_init()` already creates and may load the home screen internally. The later `lv_refr_now(NULL)` in `main()` is a first-frame safeguard and should not be removed casually. +- `cp0_lvgl_init()` must run before `ui_init()`, otherwise `lv_disp_get_default()`, input devices, paths, and system interfaces may not be ready. +- The SDL platform skips the startup GIF by default; only the device checks and plays `logo_output.gif`. +- Home input must be rebound through `UILaunchPage::bind_home_input_group()`; returning to the home screen from a built-in page or terminal page must also restore this group. +- While an external independent application is running, it sets `LVGL_RUN_FLAGE=0`; do not assume APPLaunch will continue refreshing the UI during this period. +- `APPLaunch_lock()` depends on cooperation between `cp0_process_exec_blocking()` and the lock file. If an external application exits abnormally but the lock is not released, the home screen may appear not to refresh; investigate the lock file and holder PID. diff --git a/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md b/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md new file mode 100644 index 00000000..80d2ca8b --- /dev/null +++ b/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md @@ -0,0 +1,464 @@ +# 03 - UI Framework and Home Carousel + +This chapter explains how the APPLaunch home UI is organized, how data flows through the carousel cards, and how key events are handled. Key references are `projects/APPLaunch/main/ui/UILaunchPage.cpp`, `projects/APPLaunch/main/ui/UILaunchPage.h`, `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp`, and `projects/APPLaunch/main/ui/Launch.cpp`. + +## 1. UI Framework Overview + +APPLaunch does not use a traditional desktop framework. Instead, it builds the UI directly from an LVGL object tree: + +```text +ui_Screen1 +├── Top status bar create_top() +│ ├── ZERO / logo +│ ├── WiFi signal bars +│ ├── Time panel +│ └── Battery panel +└── ui_APP_Container create_app_container() + ├── 5 carousel card panels + ├── 5 title labels + ├── Left/right arrow buttons + └── 5 page dots +``` + +The global entry points for home objects come from declarations in `ui_obj.h`, such as `ui_Screen1`, `ui_APP_Container`, `ui_timeLabel`, and `ui_Bar1`. Actual creation and styling are concentrated in `UILaunchPage.cpp`. + +## 2. Key Source Paths + +| Path | Description | +| --- | --- | +| `projects/APPLaunch/main/ui/UILaunchPage.h` | Home class definition, carousel element enum, and `carousel_elements` array | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home screen creation, carousel switching, keyboard events, startup GIF, and font cache | +| `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | Carousel left/right switch animation | +| `projects/APPLaunch/main/ui/Launch.cpp` | Fills new card content after switching, launches the current application, and refreshes the status bar | +| `projects/APPLaunch/main/ui/ui.h` | Home layout constants such as `LABEL_Y_CENTER` and `BORDER_COLOR_CENTER` | +| `projects/APPLaunch/main/ui/ui_obj.h` | Global LVGL object declarations | + +## 3. Responsibilities of `UILaunchPage` + +`UILaunchPage` is the facade class for the home UI: + +```cpp +class UILaunchPage : public home_base +{ +public: + static void load_home_screen(); + static void start_startup_gif(); + static void create_screen(); + + static void init_input_group(); + static void bind_home_input_group(); + static lv_group_t *home_input_group(); + static lv_obj_t *panel(size_t slot); + static lv_obj_t *label(size_t slot); + + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_selected_app(); + + static std::array carousel_elements; +}; +``` + +It has two categories of responsibilities: + +- Static responsibilities: create the screen, maintain the home input group, and provide `panel()` / `label()` accessors. +- Instance responsibilities: hold the `Launch` pointer and forward carousel updates and application launch operations to `LaunchImpl`. + +The current code stores the active home page instance in `active_launch_page` so static event callbacks can call it: + +```cpp +namespace { +UILaunchPage *active_launch_page = nullptr; +} + +UILaunchPage::UILaunchPage(std::shared_ptr launch) + : home_base(), launch_(std::move(launch)) +{ + active_launch_page = this; +} +``` + +## 4. Carousel Element Array + +All core objects of the home carousel are stored in a fixed array: + +```cpp +std::array + UILaunchPage::carousel_elements = {}; +``` + +The enum is defined in `UILaunchPage.h`: + +```cpp +enum LauncherCarouselElement : size_t { + kCardFarLeft = 0, + kCardLeft, + kCardCenter, + kCardRight, + kCardFarRight, + kTitleFarLeft, + kTitleLeft, + kTitleCenter, + kTitleRight, + kTitleFarRight, + kPageDot0, + kPageDot1, + kPageDot2, + kPageDot3, + kPageDot4, + kLauncherCarouselElementCount, +}; +``` + +The array is divided into three segments: + +| Index Range | Object | Description | +| --- | --- | --- | +| `0..4` | Card panel | far-left, left, center, right, far-right | +| `5..9` | Title label | Corresponds to the card slots | +| `10..14` | Page dot | 5 bottom status dots | + +Helper accessors: + +```cpp +lv_obj_t *UILaunchPage::panel(size_t slot) +{ + return carousel_elements[kCardFarLeft + slot]; +} + +lv_obj_t *UILaunchPage::label(size_t slot) +{ + return carousel_elements[kTitleFarLeft + slot]; +} +``` + +Therefore, `panel(2)` is the center card, and `label(2)` is the center title. + +## 5. Standard Slot Layout + +`UILaunchPage.cpp` uses `CarouselSlot` to describe the static carousel layout: + +```cpp +struct CarouselSlot { + lv_coord_t x; + lv_coord_t y; + lv_coord_t width; + lv_coord_t height; + bool hidden; +}; + +static const CarouselSlot CAROUSEL_SLOTS[] = { + {-177, 4, 61, 61, true}, + {-99, -6, 80, 80, false}, + {0, -16, 100, 100, false}, + {99, -6, 80, 80, false}, + {177, 4, 61, 61, true}, + {-177, LABEL_Y_SIDE, 0, 0, true}, + {-99, LABEL_Y_SIDE, 0, 0, false}, + {0, LABEL_Y_CENTER, 0, 0, false}, + {99, LABEL_Y_SIDE, 0, 0, false}, + {177, LABEL_Y_SIDE, 0, 0, true}, +}; +``` + +Slot semantics: + +```text +Cards: far-left(hidden) left center right far-right(hidden) +Titles: far-left(hidden) left center right far-right(hidden) +``` + +The hidden far-side slots are animation buffers: before switching, the card that is about to enter is placed on the far side; after the animation ends, the array order is rotated. + +## 6. Home Creation Flow + +`create_screen()` creates the root screen: + +```cpp +ui_Screen1 = lv_obj_create(NULL); +lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); +lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); + +create_top(ui_Screen1); +create_app_container(ui_Screen1); +``` + +### 6.1 Top Status Bar + +`create_top()` contains: + +- The top-left `ZERO` text or `launcher_brand_logo.png`. +- `ui_wifiPanel` and `ui_wifiBar1..4`, hidden by default and shown by signal strength during status refresh. +- `ui_Panel1`, the time background image `status_time_background.png`, and `ui_timeLabel`. +- `ui_batteryPanel`, the battery background image `status_battery_background.png`, `ui_Bar1`, and `ui_powerLabel`. + +Status bar data is not refreshed in `UILaunchPage`, but in `LaunchImpl::update_home_status_bar()`: + +```cpp +cp0_wifi_status_t wifi = cp0_wifi_get_status(); +cp0_time_str(time_buf, sizeof(time_buf)); +cp0_battery_info_t bat = cp0_battery_read(); +``` + +`LaunchImpl` creates a 5-second timer during construction: + +```cpp +status_timer = lv_timer_create(home_status_timer_cb, 5000, this); +``` + +### 6.2 Carousel Container + +`create_app_container()` creates `ui_APP_Container`: + +```cpp +ui_APP_Container = lv_obj_create(parent); +lv_obj_remove_style_all(ui_APP_Container); +lv_obj_set_width(ui_APP_Container, 320); +lv_obj_set_height(ui_APP_Container, 150); +lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); +``` + +It then creates, in order: + +- 5 page dots: `kPageDot0..kPageDot4`; the center page dot defaults to 10x10 and yellow. +- 5 titles: the center defaults to `CLI`, left/right default to `STORE` / `GAME`, and far-side titles are hidden. +- 5 cards: the center is 100x100, left/right are 80x80, and far-side cards are 61x61 and hidden. +- Left/right buttons: background images `carousel_left_arrow.png` / `carousel_right_arrow.png`. + +The default titles are only UI placeholders. Real content is written by `LaunchImpl` after it initializes the application list. + +## 7. Carousel Switch Flow + +Carousel switching is split into two parts: UI animation and application data update. + +### 7.1 Switching Right with `switch_right()` + +`switch_right()` means the cards move right as a group, and the current selection becomes the previous application in the list: + +```cpp +void switch_right(lv_event_t *e) +{ + if (is_animating) { + pending_switch = &switch_right; + return; + } + + is_animating = true; + lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); + launcher_home_animation::animate_right(carousel_elements.data(), snap_all_panels); + + snap_panel_to_slot(carousel_elements[4], 0); + snap_label_to_slot(carousel_elements[9], 5); + + active_launch_page->update_right_slot(carousel_elements[4], carousel_elements[9]); + rotate_carousel_right(0, 4); + rotate_carousel_right(5, 9); +} +``` + +Key steps: + +1. If an animation is already running, store this request in `pending_switch` and execute it after the current animation finishes. +2. Show the far-left hidden card as the side entering the viewport during the animation. +3. Call `launcher_home_animation::animate_right()` to start the animation. +4. Pre-snap the far-right object to the far-left slot and fill it with the new application content that will enter. +5. Rotate `carousel_elements[0..4]` and `[5..9]` so the array order matches the new visual order. +6. Update page dot highlighting. + +### 7.2 Switching Left with `switch_left()` + +`switch_left()` means the cards move left as a group, and the current selection becomes the next application in the list: + +```cpp +void switch_left(lv_event_t *e) +{ + if (is_animating) { + pending_switch = &switch_left; + return; + } + + is_animating = true; + lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); + launcher_home_animation::animate_left(carousel_elements.data(), snap_all_panels); + + snap_panel_to_slot(carousel_elements[0], 4); + snap_label_to_slot(carousel_elements[5], 9); + + active_launch_page->update_left_slot(carousel_elements[0], carousel_elements[5]); + rotate_carousel_left(0, 4); + rotate_carousel_left(5, 9); +} +``` + +It is symmetric with `switch_right()`: the far-right side enters the viewport, while the far-left object is moved to the far-right slot and filled with new content. + +## 8. Snapping Back After Animation + +The animation completion callback is `snap_all_panels()`: + +```cpp +static void snap_all_panels() +{ + for (int i = 0; i < 5; i++) + snap_panel_to_slot(carousel_elements[i], i); + + for (int i = 5; i < 10; i++) + snap_label_to_slot(carousel_elements[i], i); + + is_animating = false; + + if (pending_switch) { + switch_cb_t cb = pending_switch; + pending_switch = NULL; + cb(NULL); + } +} +``` + +It solves two problems: + +- Animation interpolation may introduce small errors, so objects are force-snapped to the standard slots after the animation ends. +- If the user repeatedly presses direction keys during the animation, only one pending switch is kept and executed after the animation completes. + +## 9. How Application Data Is Written into the Carousel + +`LaunchImpl` maintains `current_app` and `app_list`. During a switch, `UILaunchPage` only passes in the panel/label to be reused; `LaunchImpl` calculates which application should be displayed. + +Fill the new right end after switching left: + +```cpp +void update_left_slot(lv_obj_t *panel, lv_obj_t *label) +{ + current_app = current_app == app_list.size() - 1 ? 0 : current_app + 1; + int next_app = current_app; + next_app = next_app == app_list.size() - 1 ? 0 : next_app + 1; + next_app = next_app == app_list.size() - 1 ? 0 : next_app + 1; + auto it = std::next(app_list.begin(), next_app); + lv_label_set_text(label, it->Name.c_str()); + panel_set_icon(panel, it->Icon.c_str()); +} +``` + +Fill the new left end after switching right: + +```cpp +void update_right_slot(lv_obj_t *panel, lv_obj_t *label) +{ + current_app = current_app == 0 ? app_list.size() - 1 : current_app - 1; + int next_app = current_app; + next_app = next_app == 0 ? app_list.size() - 1 : next_app - 1; + next_app = next_app == 0 ? app_list.size() - 1 : next_app - 1; + auto it = std::next(app_list.begin(), next_app); + lv_label_set_text(label, it->Name.c_str()); + panel_set_icon(panel, it->Icon.c_str()); +} +``` + +Diagram: + +```text +Visual slots: [far-left] [left] [center] [right] [far-right] +Application index: current-2 current-1 current current+1 current+2 + +Press RIGHT: + current -> current-1 + New far-left needs to display current-2 + +Press LEFT: + current -> current+1 + New far-right needs to display current+2 +``` + +## 10. Input Events and Sound Effects + +The home keyboard event is bound at the end of `create_app_container()`: + +```cpp +lv_obj_add_event_cb(ui_Screen1, main_key_switch, + (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); +``` + +`main_key_switch()` logic: + +```text +Press LEFT/Z + -> audio_play_switch() + -> switch_right() + +Press RIGHT/C + -> audio_play_switch() + -> switch_left() + +Release ENTER + -> audio_play_enter() + -> app_launch() + +Release F12 + -> Toggle green test background lvping_lock +``` + +The code first maps `F/X/Z/C` to arrow keys through `fzxc_to_arrow()`: + +```cpp +KEY_F -> KEY_UP +KEY_X -> KEY_DOWN +KEY_Z -> KEY_LEFT +KEY_C -> KEY_RIGHT +``` + +Sound effect entry points: + +```cpp +cp0_signal_system_play_asset("switch.wav"); +cp0_signal_system_play_asset("enter.wav"); +``` + +The startup sound is played in `load_home_screen()`: + +```cpp +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +## 11. Home Sequence Text + +```text +UILaunchPage::create_screen() + -> create_top() + -> Create logo / WiFi / time / battery objects + -> create_app_container() + -> Create page dots + -> Create labels + -> Create cards + -> Create arrows + -> Bind click and keyboard callbacks + +User presses RIGHT + -> main_key_switch() + -> audio_play_switch() + -> switch_left() + -> is_animating=true + -> animate_left() + -> update_left_slot() + -> rotate cards / labels + -> Update page dot + -> snap_all_panels() + -> Snap objects to standard slots + -> is_animating=false + -> If pending_switch exists, continue executing it + +User presses ENTER + -> main_key_switch() + -> audio_play_enter() + -> app_launch() + -> UILaunchPage::launch_selected_app() + -> Launch::launch_app() +``` + +## 12. Notes + +- `carousel_elements` stores LVGL object pointers; carousel switching rotates the pointer array instead of destroying and recreating objects. +- The names `switch_left()` / `switch_right()` describe animation direction and are not necessarily identical to user key direction. Currently, `KEY_LEFT` calls `switch_right()`, and `KEY_RIGHT` calls `switch_left()`. +- During animation, only one `pending_switch` is recorded, so rapid repeated key presses do not create an unbounded queue. +- Home card click events are all bound to `app_launch()`, but normal interaction mainly uses center selection + Enter launch. If mouse/touch interaction is enabled, confirm whether clicking a non-center card matches expectations. +- Status bar objects are created by `UILaunchPage`, but the refresh timer is created during `LaunchImpl` construction. If the home screen is created without executing `Launch::bind_ui()`, the application list and status bar refresh will not start. +- When adding or adjusting carousel slots, update `CAROUSEL_SLOTS`, the initial positions in `create_app_container()`, and the slot definitions in the animation file together to avoid jumps after animation completion. diff --git a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md new file mode 100644 index 00000000..f5ed9fa5 --- /dev/null +++ b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md @@ -0,0 +1,497 @@ +# 04 - Application Model and Launch Mechanism + +This chapter explains how APPLaunch unifies built-in pages, terminal commands, and external standalone programs into one application list, and how an application is launched after the user presses Enter. Key references are `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/Launch.h`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/components/page_app/*`. + +## 1. Application Model Overview + +APPLaunch abstracts every home-screen entry as an `app`: + +```text +app +├── Name display title +├── Icon icon path +├── Exec external command; can be empty for built-in pages +└── launch(LaunchImpl*) launch action +``` + +After this unification, the home carousel does not need to care about application type. It only displays `Name` and `Icon`; when Enter is pressed, it simply calls the current `app.launch()`. + +```text +Home center card + -> Launch::launch_app() + -> LaunchImpl::launch_app() + -> app.launch(this) + ├── Built-in page: new PageT + lv_disp_load_scr() + ├── Terminal app: UIConsolePage + PTY exec() + └── External app: cp0_process_exec_blocking() +``` + +## 2. Key Source Paths + +| Path | Description | +| --- | --- | +| `projects/APPLaunch/main/ui/Launch.h` | Public facade for `Launch`, hiding `LaunchImpl` | +| `projects/APPLaunch/main/ui/Launch.cpp` | `app`, `LaunchImpl`, application list, launch logic, `.desktop` scanning | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Forwards Enter / click events to `Launch::launch_app()` | +| `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | Terminal page `UIConsolePage` | +| `projects/APPLaunch/main/ui/components/page_app/*.hpp` | Built-in pages such as settings, music, file, camera, and LoRa | +| `projects/APPLaunch/APPLaunch/applications/` | Runtime `.desktop` application descriptor directory | +| `ext_components/cp0_lvgl` | Lower-level capabilities such as process launch, PTY, directory watching, and path resolution | + +## 3. `Launch` and `LaunchImpl` Layers + +`Launch.h` exposes only a small public surface: + +```cpp +class Launch +{ +public: + void bind_ui(); + void set_launch_page(std::shared_ptr launch_page); + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_app(); + +private: + std::unique_ptr impl_; + std::shared_ptr launch_page_; +}; +``` + +The real logic lives in `LaunchImpl`: + +```cpp +class LaunchImpl +{ +private: + int current_app = 2; + cp0_watcher_t dir_watcher = NULL; + lv_timer_t *watch_timer = nullptr; + lv_timer_t *status_timer = nullptr; + int fixed_count; + +public: + std::list app_list; + std::shared_ptr app_Page; + std::shared_ptr home_Page; +}; +``` + +Field meanings: + +| Field | Description | +| --- | --- | +| `current_app` | Application index corresponding to the current center card. Defaults to `2`, so the initial center card is CLI | +| `app_list` | All fixed applications and dynamic `.desktop` applications | +| `fixed_count` | Number of fixed applications. Dynamic reload keeps the elements before this point | +| `app_Page` | Lifetime holder for the current built-in page or terminal page | +| `dir_watcher` / `watch_timer` | Watches the `applications/` directory for changes and reloads dynamic apps | +| `status_timer` | Timer that refreshes the home status bar | + +## 4. `app` Structure and Three Launch Modes + +`app` is defined in `Launch.cpp`: + +```cpp +struct app +{ + std::string Name; + std::string Icon; + std::string Exec; + + std::function launch; + + app(std::string name, std::string icon, std::string exec, bool terminal); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause, bool run_as_root); + + template + app(std::string name, std::string icon, page_t tag); +}; +``` + +Three application categories: + +| Type | Construction | Launch function | Examples | +| --- | --- | --- | --- | +| Built-in page | `page_v` | Constructs a page and calls `lv_disp_load_scr()` | `GAME`, `SETTING`, `MUSIC` | +| Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`, `CLI` | +| External process | `exec, terminal=false` | `launch_Exec()` | AppStore, Calculator | + +## 5. Fixed Application Registration + +The `LaunchImpl` constructor first registers fixed entries: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", + false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +Then it writes the first 5 applications into the 5 home-screen slots: + +```cpp +lv_label_set_text(UILaunchPage::label(0), it->Name.c_str()); +panel_set_icon(UILaunchPage::panel(0), it->Icon.c_str()); +``` + +Initial state: + +```text +slot 0 far-left : Python +slot 1 left : STORE +slot 2 center : CLI +slot 3 right : GAME +slot 4 far-right: SETTING +current_app : 2 +``` + +After that, optional applications are appended according to settings and platform conditions: + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("Music")) + app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); + +if (APP_ENABLED("Math")) + app_list.emplace_back("MATH", img_path("math_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); + +app_list.emplace_back("Compass", img_path("compass_needle_80.png"), page_v); +``` + +On Linux device builds, pages such as `IP_PANEL`, `FILE`, `SSH`, `MESH`, `REC`, `CAMERA`, `LORA`, and `TANK` are also appended according to configuration. + +## 6. Built-in Page Launch Mechanism + +Built-in pages are constructed through the template constructor: + +```cpp +template +app::app(std::string name, std::string icon, page_t) + : Name(std::move(name)), Icon(std::move(icon)) +{ + launch = [](LaunchImpl *self) + { + ui_loading_show("Loading..."); + lv_refr_now(NULL); + + auto p = std::make_shared(); + self->app_Page = p; + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&LaunchImpl::go_back_home, self); + + ui_loading_hide(); + }; +} +``` + +Built-in pages must follow these conventions: + +- The page class can be constructed without arguments. +- It provides `screen()` to return the page's root screen. +- It provides `input_group()` to return the page's own input group. +- It provides or inherits the `navigate_home` callback for returning to the home screen. + +Launch sequence: + +```text +Enter + -> app.launch(LaunchImpl*) + -> ui_loading_show("Loading...") + -> lv_refr_now(NULL) + -> make_shared() + -> app_Page = p keeps the lifetime + -> lv_disp_load_scr(p->screen()) + -> Input device switches to p->input_group() + -> p->navigate_home = LaunchImpl::go_back_home + -> ui_loading_hide() +``` + +## 7. Terminal Application Launch Mechanism + +Terminal applications use `UIConsolePage`, and the external command runs inside a terminal page in the APPLaunch process: + +```cpp +void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) +{ + ui_loading_show("Loading..."); + lv_refr_now(NULL); + + auto p = std::make_shared(); + app_Page = p; + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&LaunchImpl::go_back_home, this); + p->terminal_sysplause = sysplause; + + ui_loading_hide(); + p->exec(exec); +} +``` + +Typical entries: + +```text +Python -> exec = "python3", terminal = true +CLI -> exec = "bash", terminal = true +``` + +Compared with built-in pages, terminal applications add one extra step: `p->exec(exec)`. They usually interact with the command through a PTY. What the user sees is `UIConsolePage`, not a separate UI outside APPLaunch. + +## 8. External Standalone Application Launch Mechanism + +External applications use `cp0_process_exec_blocking()`: + +```cpp +void launch_Exec(const std::string &exec, bool keep_root = false) +{ + ui_loading_show("Loading..."); + + lv_disp_t *disp = lv_disp_get_default(); + lv_indev_t *indev = lv_indev_get_next(NULL); + + LVGL_RUN_FLAGE = 0; + if (indev) + lv_indev_set_group(indev, NULL); + lv_timer_enable(false); + lv_refr_now(disp); + + int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, + keep_root ? 1 : 0); + + lv_timer_enable(true); + if (indev) + lv_indev_set_group(indev, UILaunchPage::home_input_group()); + lv_disp_load_scr(ui_Screen1); + ui_loading_hide(); + lv_obj_invalidate(lv_screen_active()); + lv_refr_now(disp); + LVGL_RUN_FLAGE = 1; +} +``` + +Key points: + +- Shows Loading and forces a refresh before launch, so the user gets immediate feedback. +- Clears the APPLaunch input group so the home screen does not keep processing keys while the external process is running. +- `lv_timer_enable(false)` pauses LVGL timers while the external program takes the foreground. +- `cp0_process_exec_blocking()` blocks until the external program exits. +- After the external program exits, it restores the timer, input group, home screen, and `LVGL_RUN_FLAGE`. + +Sequence text: + +```text +Enter external app + -> ui_loading_show() + -> LVGL_RUN_FLAGE=0 + -> lv_indev_set_group(NULL) + -> lv_timer_enable(false) + -> lv_refr_now() + -> cp0_process_exec_blocking() + -> External program runs + -> APPLaunch main rendering is paused + -> Wait for the external program to exit + -> lv_timer_enable(true) + -> Bind home input group + -> lv_disp_load_scr(ui_Screen1) + -> ui_loading_hide() + -> lv_refr_now() + -> LVGL_RUN_FLAGE=1 +``` + +`STORE` is an example external application: + +```cpp +app_list.emplace_back("STORE", + img_path("store_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", + false, + true, + true); +``` + +Here `run_as_root=true` is passed to `launch_Exec(exec, run_as_root)`, and then converted to `keep_root ? 1 : 0`. + +## 9. Return-to-Home Mechanism + +Built-in pages and terminal pages return to the home screen through the `navigate_home` callback: + +```cpp +void go_back_home() +{ + lv_async_call(lv_go_back_home, this); +} + +static void lv_go_back_home(void *arg) +{ + auto self = (LaunchImpl *)arg; + lv_timer_enable(true); + UILaunchPage::bind_home_input_group(); + lv_disp_load_scr(ui_Screen1); + lv_refr_now(NULL); + if (self->app_Page) + self->app_Page.reset(); +} +``` + +Why `lv_async_call()` is used: + +- Returning home may be triggered by a page event or input callback. +- Running asynchronously avoids destroying the page object directly inside the current LVGL event stack. +- `app_Page.reset()` releases the current page, so the code must ensure that page object is no longer used. + +External applications do not use `navigate_home`; instead, the home screen is restored after `cp0_process_exec_blocking()` returns. + +## 10. `.desktop` Dynamic Application Scanning + +Dynamic application directory: + +```cpp +const std::string app_dir_path = cp0_file_path("applications"); +``` + +After installation on a device, this usually maps to: + +```text +/usr/share/APPLaunch/applications/ +``` + +Example `.desktop` file: + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/e-Mail_80.png +``` + +`applications_load()` only processes files with the `.desktop` extension, and reads fields from the `[Desktop Entry]` section: + +| Field | Required | Description | +| --- | --- | --- | +| `Name` | Yes | Home-screen display title | +| `Exec` | Yes | Launch command | +| `Icon` | No | Icon path | +| `Terminal` | No | `true/True/1` means launch through `UIConsolePage` | +| `Sysplause` | No | Pause policy passed to the terminal page; defaults to true | + +Registration logic: + +```cpp +if (page_title.empty() || app_exec.empty()) + continue; + +for (auto it : app_list) { + if (it.Exec == app_exec) { + in_list = true; + break; + } +} + +if (!in_list) + app_list.emplace_back(page_title, app_icon, app_exec, + app_terminal, app_sysplause); +``` + +Note: dynamic applications are deduplicated by `Exec`; if `Exec` matches a fixed app or another `.desktop` app, it is skipped. + +## 11. Dynamic Application Directory Watching and Reloading + +At the end of the `LaunchImpl` constructor: + +```cpp +fixed_count = app_list.size(); +applications_load(); +inotify_init_watch(); +watch_timer = lv_timer_create(app_dir_watch_cb, 3000, this); +``` + +Watch flow: + +```text +Every 3 seconds via LVGL timer + -> cp0_dir_watch_poll(dir_watcher) + -> If applications/ changed + -> applications_reload() + -> Delete dynamic apps after fixed_count + -> applications_load() + -> refresh_ui_panels() +``` + +`refresh_ui_panels()` rewrites the 5 visible/hidden slots according to the current `current_app`: + +```cpp +app_at(current_app - 2) -> far-left +app_at(current_app - 1) -> left +app_at(current_app) -> center +app_at(current_app + 1) -> right +app_at(current_app + 2) -> far-right +``` + +This ensures that after dynamic applications are added or removed, the home screen does not need to rebuild LVGL objects; it only updates text and icons. + +## 12. Icon Setting and Resource Paths + +Icons are written by `panel_set_icon()`: + +```cpp +static void panel_set_icon(lv_obj_t *panel, const char *src) +{ + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_obj_set_align(img, LV_ALIGN_CENTER); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, icon_src); +} +``` + +Characteristics: + +- Each panel reuses the first child image and does not repeatedly create image objects. +- The image is stretched to the panel size. +- If the path is empty or unreadable, it writes a log but still calls `lv_image_set_src()`. + +Fixed applications generally use `img_path("xxx.png")`. The `Icon` field of dynamic `.desktop` applications is currently passed directly as `app_icon`. When writing `.desktop` files, make sure the icon path can be read by LVGL. + +## 13. Complete Flow from Key Press to Launch + +```text +User releases ENTER + -> LV_EVENT_KEYBOARD is delivered to ui_Screen1 + -> main_key_switch() + -> code == KEY_ENTER and key_state == 0 + -> audio_play_enter() + -> app_launch(NULL) + -> app_launch() + -> active_launch_page->launch_selected_app() + -> UILaunchPage::launch_selected_app() + -> launch_->launch_app() + -> Launch::launch_app() + -> impl_->launch_app() + -> LaunchImpl::launch_app() + -> auto it = std::next(app_list.begin(), current_app) + -> it->launch(this) + -> Enter built-in page / terminal page / external process based on app type +``` + +## 14. Notes + +- `Launch::bind_ui()` must be called before `LaunchImpl` is created. Otherwise, the home screen may be displayed, but application list updates, the status-bar timer, directory watching, and launch logic will not work. +- `current_app` defaults to `2`. The order of the first 5 fixed entries affects the initial center card; consider the initial home experience when changing this order. +- If built-in page construction can take a long time, keep `ui_loading_show()` + `lv_refr_now()` so the user sees immediate feedback. +- Launching an external application pauses APPLaunch LVGL timers and input group. The external program must exit normally or respond to the HOME logic, otherwise the user will feel stuck in the external UI. +- A dynamic `.desktop` application needs at least `Name` and `Exec`; `Terminal=true` is suitable for command-line programs, while graphical or exclusive-framebuffer programs should use `Terminal=false`. +- Dynamic applications are deduplicated by `Exec`, not by `Name`; if multiple entries use the same command, only the first one is kept. +- After modifying `applications/`, wait up to 3 seconds for the watcher to reload it. If the watcher is not initialized or the platform does not support it, restart APPLaunch to verify the change. diff --git a/docs/launcher-project-guide/05-built-in-page-framework.md b/docs/launcher-project-guide/05-built-in-page-framework.md new file mode 100644 index 00000000..c9f11c23 --- /dev/null +++ b/docs/launcher-project-guide/05-built-in-page-framework.md @@ -0,0 +1,333 @@ +# 05 - Built-in Page Framework + +This chapter explains the class hierarchy, lifecycle, page list, page registration method, and conventions for adding built-in APPLaunch pages. Key source files are `projects/APPLaunch/main/ui/components/ui_app_page.hpp`, `projects/APPLaunch/main/ui/components/page_app/*.hpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/UILaunchPage.cpp`. + +## 1. What a Built-in Page Is + +A built-in page is an LVGL page class compiled into the APPLaunch process. It is different from an external `.desktop` application: + +- A built-in page directly creates an `lv_obj_t *root_screen_` and switches to its own screen through `lv_disp_load_scr(page->screen())`. +- The page object is stored in `LaunchImpl::app_Page`, and is released asynchronously by the `navigate_home` callback when exiting. +- The page shares the APPLaunch process, LVGL main loop, input thread, resource resolution, and `cp0_lvgl_app.h` system interfaces with the home screen. +- Pages are usually header-only and placed under `projects/APPLaunch/main/ui/components/page_app/`, then aggregated by `components/page_app.h`. + +Simplified relationship: + +```text +UILaunchPage home carousel + | + v +LaunchImpl::launch_app() + | + +-- External command: cp0_process_exec_blocking() + +-- Terminal command: UIConsolePage + PTY + +-- Built-in page: std::make_shared() + | + v + lv_disp_load_scr(page->screen()) +``` + +## 2. Page Base-Class Hierarchy + +### 2.1 `AppPageRoot` + +`AppPageRoot` is the root base class for all built-in pages. It is located in `projects/APPLaunch/main/ui/components/ui_app_page.hpp`. It creates an independent screen and an LVGL input group. + +```cpp +class AppPageRoot +{ +public: + std::string page_title_ = "APP"; + lv_group_t *input_group_; + lv_obj_t *root_screen_; + std::function navigate_home; + bool has_bottom_bar_ = false; + int top_bar_height_px_ = 20; + + AppPageRoot() + { + creat_base_UI(); + creat_input_group(); + } + + virtual ~AppPageRoot() + { + lv_obj_del(root_screen_); + lv_group_delete(input_group_); + } +}; +``` + +Key points: + +- `root_screen_` is the page's own top-level screen, not a child of the home `ui_Screen1`. +- By default, `input_group_` only contains `root_screen_`. When the page is launched, it is bound to the current `lv_indev_t`. +- `navigate_home` is injected by `LaunchImpl`; a page calls it to return home after ESC or after finishing a task. +- The destructor deletes `root_screen_` and `input_group_`, so LVGL child objects created inside the page are released with the screen. + +### 2.2 Top Bar, Content Area, and Bottom Bar Regions + +`ui_app_page.hpp` splits a page into several reusable regions: + +| Class | Responsibility | Default size | +| --- | --- | --- | +| `AppTopBarRegion` | Creates the status top bar, showing title, WiFi, time, and battery | Height `20px` | +| `AppContentRegion` | Creates the `ui_APP_Container` content area | Height `150px`, or `130px` when a bottom bar exists | +| `AppBottomBarRegion` | Creates the `ui_BOTTOM_Container` bottom bar | Height `20px` | +| `AppPageLayout` | Top bar + content area | `20+150` within `320x170` | +| `AppPageWithBottomBarLayout` | Top bar + content area + bottom bar | `20+130+20` | +| `home_base` | Home-only base class, not exactly equivalent to AppPage | Home status bar + carousel container | + +A typical page directly inherits `AppPage`: + +```cpp +class UIIpPanelPage : public AppPage +{ +public: + UIIpPanelPage() : AppPage() + { + set_page_title("IP INFO"); + creat_UI(); + event_handler_init(); + } +}; +``` + +A few games or full-screen pages inherit `AppPageRoot`, occupy the full `320x170` area themselves, and do not use the default top bar. Examples include `UIGamePage`, `UICompassPage`, and `UITankBattlePage`. + +## 3. Top Bar and Status Refresh + +The common top bar is implemented by `UIAppTopBar` and contains: + +- Left title: `set_page_title()` ultimately updates `top_bar_.set_title()`. +- WiFi signal: `cp0_wifi_get_status()`; the WiFi panel is hidden when not connected. +- Time: `cp0_time_str()`, refreshed every 5 seconds by default. +- Battery: responds to `LV_EVENT_BATTERY`, using `cp0_battery_info_t` to update the percentage and bar. + +Key source paths: + +- `projects/APPLaunch/main/ui/components/ui_app_page.hpp`: `UIAppTopBar`, `AppTopBarRegion`. +- `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: declarations for interfaces such as `cp0_wifi_get_status()`, `cp0_time_str()`, and `cp0_battery_read()`. + +Top-bar resources use `cp0_file_path_c()`: + +```cpp +lv_obj_set_style_bg_img_src(time_panel_, + cp0_file_path_c("status_time_background.png"), + LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +Note: ordinary built-in pages have their own status refresh timer. A page must release timers it creates itself in its destructor; `AppTopBarRegion` already releases the top-bar status timer. + +## 4. Page Lifecycle + +### 4.1 Launching a Built-in Page from Home + +`Launch.cpp` constructs a built-in page app descriptor through a template: + +```cpp +template +app::app(std::string name, std::string icon, page_t) + : Name(std::move(name)), Icon(std::move(icon)) +{ + launch = [](LaunchImpl *ctx) { + auto p = std::make_shared(); + ctx->app_Page = p; + p->navigate_home = std::bind(&LaunchImpl::go_back_home, ctx); + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + }; +} +``` + +In the actual code in `projects/APPLaunch/main/ui/Launch.cpp`, the core flow is: + +1. After the user presses ENTER on the home screen, `cpp_app_launch()` is called. +2. `UILaunchPage::launch_selected_app()` forwards to `Launch::launch_app()`. +3. `LaunchImpl::launch_app()` finds the current app and executes that app's `launch` function. +4. The built-in page object is created, the screen is loaded, and the input group is switched. +5. The page calls `navigate_home()` after ESC or after completing its business logic. +6. `LaunchImpl::go_back_home()` uses `lv_async_call()` to return to the home screen, rebinds the home input group, and resets `app_Page`. + +### 4.2 Returning Home + +All returns to home should go through `navigate_home`; do not directly delete the page from inside the page. + +```cpp +if (navigate_home) + navigate_home(); +``` + +`LaunchImpl::lv_go_back_home()` will: + +- `lv_timer_enable(true)` to restore LVGL timers. +- `UILaunchPage::bind_home_input_group()` to bind the home input group. +- `lv_disp_load_scr(ui_Screen1)` to load the home screen. +- `app_Page.reset()` to release the current page object. + +Notes: + +- A page destructor must stop any `lv_timer_t`, background thread, file watcher, PTY, or audio resource that the page created. +- Do not directly `delete this` from a keyboard event callback stack; use `navigate_home` and let `LaunchImpl` handle it asynchronously. +- If a page temporarily switches to a child or nested page, it must restore the correct input group. + +## 5. Current Built-in Page List + +Page implementations are concentrated in `projects/APPLaunch/main/ui/components/page_app/`. + +| Page class | File | Launcher name | Inheritance | Description | +| --- | --- | --- | --- | --- | +| `UIConsolePage` | `ui_app_console.hpp` | `CLI` or terminal external command | `AppPage` | Terminal emulator, PTY read/write, supports ANSI/VT sequences and keyboard escape sequences | +| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | Snake game, full-screen custom drawing, driven by an LVGL timer | +| `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | System settings, application toggles, brightness, volume, WiFi, camera resolution, and more | +| `UIMusicPage` | `ui_app_music.hpp` | `MUSIC` | `AppPage` | Music player, directory browsing, playlist, audio callbacks | +| `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | Compass page, sensor thread + UI timer | +| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | `IP_PANEL` | `AppPage` | Network interface/IP information list, refreshed every second | +| `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | File browser, directory list and enter/back navigation | +| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH parameter input, embeds `UIConsolePage` after connection | +| `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh message list, input overlay, send/refresh | +| `UIRecPage` | `ui_app_rec.hpp` | `REC` | Custom `rec_page` | Recording/playback/file list with asynchronous resource management | +| `UICameraPage` | `ui_app_camera.hpp` | `CAMERA` | `AppPage` | Camera preview, gallery, capture, status page | +| `UILoraPage` | `ui_app_lora.hpp` | `LORA` | `AppPage` | LoRa business page, also contains C-style create/destroy interfaces internally | +| `UITankBattlePage` | `ui_app_tank_battle.hpp` | `TANK` | `AppPageRoot` | Tank mini-game, full-screen, fixed key mapping | + +`Python`, `STORE`, and `MATH` are not built-in pages: they are launched through commands or external processes. + +## 6. Page Registration and Display Order + +Built-in pages are inserted into `app_list` in `LaunchImpl::LaunchImpl()`. The first 5 fixed applications initialize the 5 home carousel slots first: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +Optional built-in pages are then added according to setting toggles: + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("Music")) + app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); + +if (APP_ENABLED("IP_Panel")) + app_list.emplace_back("IP_PANEL", img_path("ip_panel_100.png"), page_v); +``` + +Conventions: + +- `Store`, `CLI`, `Game`, and `Setting` are always-on in the settings page and cannot be disabled. +- `Compass` is currently added unconditionally in `Launch.cpp` and is not controlled by the Launcher toggle list in `UISetupPage`. +- Pages such as `IP_PANEL`, `FILE`, `SSH`, `MESH`, `REC`, `CAMERA`, `LORA`, and `TANK` are added only in Linux device builds; SDL builds are limited by `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)`. +- Dynamic `.desktop` applications are scanned and added after built-in pages. Directory changes are checked by a watcher every 3 seconds. + +## 7. Page Code Skeleton + +New ordinary pages should usually inherit `AppPage`: + +```cpp +#pragma once +#include "../ui_app_page.hpp" +#include "compat/input_keys.h" + +class UINewPage : public AppPage +{ +public: + UINewPage() : AppPage() + { + set_page_title("NEW"); + create_ui(); + event_handler_init(); + } + + ~UINewPage() + { + if (timer_) { + lv_timer_delete(timer_); + timer_ = nullptr; + } + } + +private: + lv_timer_t *timer_ = nullptr; + + void create_ui() + { + lv_obj_t *bg = lv_obj_create(ui_APP_Container); + lv_obj_set_size(bg, 320, 150); + lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); + } + + void event_handler_init() + { + lv_obj_add_event_cb(root_screen_, &UINewPage::event_cb, LV_EVENT_ALL, this); + } + + static void event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self || !IS_KEY_RELEASED(e)) + return; + + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + if (key == KEY_ESC && self->navigate_home) + self->navigate_home(); + } +}; +``` + +A new full-screen page may inherit `AppPageRoot`, but it must handle the `320x170` layout, status hints, and return key by itself. + +## 8. Page UI Conventions + +- Design for a `320x170` resolution. The common page content area is `320x150`; the top `20px` is occupied by the top bar. +- Page objects are usually stored in `std::unordered_map ui_obj_` to make repainting/deletion easier. +- For list pages, prefer fixed row height + virtual scrolling instead of free LVGL container scrolling, to avoid focus confusion on a small screen. +- For frequently refreshed pages, use `lv_timer_create()` and call `lv_timer_delete()` in the destructor. +- For background threads or asynchronous callbacks, use an `std::atomic` alive flag and stop the thread in the destructor to avoid callbacks touching a freed page. +- Do not hard-code relative paths for images, audio, or fonts; use `img_path()`, `audio_path()`, or `cp0_file_path_c()`. + +## 9. Nested Pages and Special Pages + +`UISSHPage` is a typical nested page: while entering SSH parameters, the keyboard is handled by `UISSHPage`; after connection, it creates `UIConsolePage` and switches the screen and input group. + +```cpp +console_page_ = std::make_shared(); +console_page_->navigate_home = [this]() { + console_page_.reset(); + view_state_ = ViewState::INPUT; + lv_disp_load_scr(this->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); +}; + +lv_disp_load_scr(console_page_->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); +``` + +Special care is required for this type of page: + +- Exiting a child page does not necessarily mean returning home; it may only return to the parent page. +- The input group must switch with the current screen, otherwise keys will be delivered to an invisible page. +- The parent page destructor must release child page objects first. + +## 10. Relationship with the Home Carousel + +The home carousel itself is managed by `UILaunchPage.cpp`: + +- `carousel_elements` stores 5 cards, 5 titles, and 5 page dots. +- When switching left/right, `switch_left()` / `switch_right()` are called. After the animation finishes, the array is rotated and `LaunchImpl` updates the far-side slot content. +- ENTER triggers `app_launch()`, which ultimately calls the current app's `launch()`. + +Built-in pages do not directly manipulate the home carousel. After returning home, the carousel state is preserved by `LaunchImpl`. + +## 11. Common Notes + +- Do not perform long blocking operations in a page constructor; display the page or loading state first, then start the task. +- Do not assume `lv_indev_get_next(NULL)` is always non-null; preferably check before switching the input group. +- Do not directly access home global objects from a page unless it is clearly a home-screen feature. +- For page titles, call `set_page_title()` instead of modifying the internal top-bar label directly. +- Every page that can exit should support `KEY_ESC` and call `navigate_home` or return to the previous view. +- Page toggle keys must stay consistent with `UISetupPage::save_app_toggle()` and `APP_ENABLED()` in `Launch.cpp`. diff --git a/docs/launcher-project-guide/06-resources-and-configuration.md b/docs/launcher-project-guide/06-resources-and-configuration.md new file mode 100644 index 00000000..ae7a2a38 --- /dev/null +++ b/docs/launcher-project-guide/06-resources-and-configuration.md @@ -0,0 +1,361 @@ +# 06 - Resources and Configuration System + +This chapter explains APPLaunch runtime resource directories, path resolution rules, `.desktop` dynamic application files, configuration APIs, settings-page configuration keys, and resource usage notes. Key source files are `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`. + +## 1. Resource System Overview + +APPLaunch pages should not manually concatenate runtime paths. Instead, use `cp0_file_path()` / `cp0_file_path_c()` for unified resolution. + +```text +Page code / Launch.cpp + | + v +img_path(), audio_path(), cp0_file_path_c() + | + v +cp0_lvgl_file.cpp / sdl_lvgl_file.cpp + | + +-- Images: share/images/... + +-- Audio: /usr/share/APPLaunch/share/audio/... + +-- Fonts: /usr/share/APPLaunch/share/font/... + +-- applications: /usr/share/APPLaunch/applications + +-- Special paths such as keyboard_device / keyboard_map / lock_file +``` + +Common wrapper functions for pages are located in `projects/APPLaunch/main/ui/components/ui_app_page.hpp`: + +```cpp +static inline std::string img_path(const char *name) +{ + return cp0_file_path(name); +} + +static inline std::string audio_path(const char *name) +{ + return cp0_file_path(name); +} +``` + +## 2. Runtime Resource Tree + +The source resource tree is located at: + +```text +projects/APPLaunch/APPLaunch/ +├── applications/ +├── bin/ +├── lib/ +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +After installation on a device, it usually maps to: + +```text +/usr/share/APPLaunch/ +├── applications/ +├── bin/ +├── lib/ +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +| Directory | Contents | Used by | +| --- | --- | --- | +| `applications/` | `.desktop` application descriptors | `LaunchImpl::applications_load()` | +| `share/images/` | Icons, status-bar backgrounds, page images, GIFs | Home screen, top bar, built-in pages | +| `share/audio/` | `startup.mp3`, `switch.wav`, `enter.wav`, page key sounds | Home sound effects, settings page, page sound effects | +| `share/font/` | TTF/OTF fonts | `LauncherFonts`, page custom fonts | +| `bin/` | Packaged scripts and external programs | Store, update scripts, dynamic applications | +| `lib/` | Packaged dynamic libraries | External programs or platform capabilities | + +## 3. Path Resolution Rules + +### 3.1 Device-Side `cp0_lvgl_file.cpp` + +The device-side implementation is in `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, with a fixed root directory: + +```cpp +constexpr const char *kAppRoot = "/usr/share/APPLaunch"; +``` + +Core rules: + +| Input | Output | +| --- | --- | +| `applications` | `/usr/share/APPLaunch/applications` | +| `lock_file` | `/tmp/M5CardputerZero-APPLaunch_fcntl.lock` | +| `keyboard_device` | `/dev/input/by-path/platform-3f804000.i2c-event` | +| `keyboard_map` | `/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map` | +| `store_sync_cmd` | `python /usr/share/APPLaunch/bin/store_cache_sync.py` | +| `*.png` / `*.gif` / `*.jpg` / `*.jpeg` / `*.svg` | `share/images/` | +| `*.wav` / `*.mp3` / `*.ogg` | `/usr/share/APPLaunch/share/audio/` | +| `*.ttf` / `*.otf` | `/usr/share/APPLaunch/share/font/` | +| Other strings | Returned unchanged | + +The current device-side image rule returns a relative path such as `share/images/`, while audio and fonts return absolute paths under `/usr/share/APPLaunch/...`. Page code should follow the existing `img_path("xxx.png")` convention and not mix multiple root directories. + +### 3.2 SDL Implementation `sdl_lvgl_file.cpp` + +The SDL implementation is in `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`. Its rules are mostly the same as the device-side implementation, but the root is decided by `get_app_root_path()`, and functions such as `app_relative_path(root_path, file, "share/images/")` adapt to development-machine run directories. + +Special names are still kept under SDL: + +```cpp +if (file == "applications") return root_path + "/applications"; +if (file == "keyboard_device") return "/dev/input/by-path/platform-3f804000.i2c-event"; +if (file == "keyboard_map") return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; +``` + +Note: SDL mode only makes APPLaunch runnable on a development machine. It does not mean every device resource exists. For example, camera, LoRa, and some Linux device pages may be excluded by compile-time conditions or unavailable at runtime. + +### 3.3 `cp0_file_path_c()` Cache + +The C interface is declared in `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: + +```c +const char *cp0_file_path_c(const char *file); +``` + +The implementation uses a `thread_local std::unordered_map` cache: + +```cpp +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::unordered_map paths; + std::string key = file ? std::string(file) : std::string(); + auto it = paths.find(key); + if (it == paths.end()) { + it = paths.emplace(key, cp0_file_path(key)).first; + } + return it->second.c_str(); +} +``` + +Therefore, the returned `const char *` is stable within the thread and can be passed directly to LVGL style or image APIs. If saving the pointer across threads, prefer saving a `std::string` instead. + +## 4. Image, Audio, and Font Usage Examples + +### 4.1 Images + +Common home-screen and built-in-page usage: + +```cpp +app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); + +lv_obj_set_style_bg_img_src(time_panel_, + cp0_file_path_c("status_time_background.png"), + LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +Home card icons are set by `Launch.cpp::panel_set_icon()`: + +```cpp +static void panel_set_icon(lv_obj_t *panel, const char *src) +{ + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, src); +} +``` + +Note: `panel_set_icon()` checks `access(icon_src, R_OK)` and writes a log. If the image path on the device side is relative, the runtime working directory must be correct; otherwise the log will report the image as missing/unreadable. + +### 4.2 Audio + +Home sound effects play asset names through system audio signals: + +```cpp +static void audio_play_ui_asset(const char *name) +{ + cp0_signal_system_play_asset(name); +} + +static void audio_play_switch(void) { audio_play_ui_asset("switch.wav"); } +static void audio_play_enter(void) { audio_play_ui_asset("enter.wav"); } +``` + +The startup sound is played after the home screen loads: + +```cpp +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +Inside pages, use `audio_path("key_enter.wav")` if a file path is needed. If the lower-level API expects an asset name rather than a path, pass the asset name directly to avoid resolving the path twice. + +### 4.3 Fonts + +The home screen and top bar use `LauncherFonts` to manage freetype fonts: + +```cpp +launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD) +``` + +Font paths are ultimately resolved by `cp0_file_path()` into `share/font/`. If freetype font loading fails, `LauncherFonts` falls back to LVGL's built-in Montserrat font. + +## 5. `.desktop` Dynamic Applications + +Dynamic application files are placed in the directory pointed to by `cp0_file_path("applications")`. `LaunchImpl::applications_load()` only processes `*.desktop` files and parses the `[Desktop Entry]` section. + +Supported keys: + +| key | Required | Description | +| --- | --- | --- | +| `Name` | Yes | Carousel display name | +| `Exec` | Yes | Launch command or executable path | +| `Icon` | No | Icon path; can be `share/images/...` or a path readable by LVGL | +| `Terminal` | No | `true`/`True`/`1` means run inside `UIConsolePage` | +| `Sysplause` | No | Whether to pause and wait for user confirmation after a terminal command exits; defaults to `true` | + +Example: + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/e-Mail_80.png +Sysplause=true +``` + +Loading rules: + +- Only key-value pairs inside the `[Desktop Entry]` section are read. +- Blank lines and comments starting with `#` or `;` are skipped. +- Entries missing either `Name` or `Exec` are skipped. +- If `Exec` duplicates an existing app, the entry is skipped. +- `TryExec` is currently not used by `applications_load()`. +- The `applications/` directory is watched. It is polled every 3 seconds, and changes clear dynamic apps and rescan the directory. + +## 6. Configuration API + +The configuration interface is declared in `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: + +```c +void cp0_config_init(void); +int cp0_config_get_int(const char *key, int default_val); +void cp0_config_set_int(const char *key, int val); +const char *cp0_config_get_str(const char *key, const char *default_val); +void cp0_config_set_str(const char *key, const char *val); +void cp0_config_save(void); +``` + +Usage conventions: + +- Always provide a default value when reading, so pages keep running if a setting is missing. +- Call `cp0_config_save()` after writing to persist changes. +- The device-side implementation is in `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`. +- The SDL compatibility implementation is in `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp`. + +Typical usage: + +```cpp +int volume = cp0_config_get_int("volume", cp0_volume_read()); +cp0_volume_write(new_val); +cp0_config_set_int("volume", new_val); +cp0_config_save(); +``` + +## 7. Configuration Key List + +### 7.1 Launcher Application Toggles + +The `Launcher` menu in `UISetupPage` saves `app_`: + +| Configuration key | Default | Meaning | Notes | +| --- | --- | --- | --- | +| `app_Python` | `1` | Python entry display toggle | Visible in settings, but Python is fixed in `Launch.cpp`; currently this toggle does not affect fixed entries | +| `app_Store` | `1` | Store entry | always-on, cannot be disabled | +| `app_CLI` | `1` | CLI entry | always-on, cannot be disabled | +| `app_Game` | `1` | GAME entry | always-on, cannot be disabled | +| `app_Setting` | `1` | SETTING entry | always-on, cannot be disabled | +| `app_Music` | `1` | MUSIC built-in page | Read by `Launch.cpp` | +| `app_Math` | `1` | Calculator external application | Read by `Launch.cpp` | +| `app_IP_Panel` | `1` | IP_PANEL built-in page | Read under Linux non-SDL builds | +| `app_File` | `1` | FILE built-in page | Read under Linux non-SDL builds | +| `app_SSH` | `1` | SSH built-in page | Read under Linux non-SDL builds | +| `app_Mesh` | `1` | MESH built-in page | Read under Linux non-SDL builds | +| `app_Rec` | `1` | REC built-in page | Read under Linux non-SDL builds | +| `app_Camera` | `1` | CAMERA built-in page | Read under Linux non-SDL builds | +| `app_LoRa` | `1` | LORA built-in page | Read under Linux non-SDL builds | +| `app_Tank` | `1` | TANK built-in page | Read under Linux non-SDL builds | + +Note: `Compass` currently has no corresponding `app_Compass` setting and is added unconditionally by `Launch.cpp`. + +### 7.2 System and Page Configuration + +| Configuration key | Read/write location | Meaning | +| --- | --- | --- | +| `brightness` | `UISetupPage`, `ext_components/cp0_lvgl/src/commount.c` | Backlight brightness value; restored at startup and written by the settings page | +| `volume` | `UISetupPage`, `commount.c` | System volume; restored at startup and written by the settings page | +| `dark_time` | `UISetupPage` | Screen-off timeout, options are `0/10/30/60/300` seconds | +| `cam_resolution` | `UISetupPage`, camera page may read it | Camera resolution option index | +| `startup_mode` | `UISetupPage` | Startup mode, currently `Launcher` / `CLI` | +| `extport_usb` | `UISetupPage` | Expansion-port USB toggle | +| `extport_5vout` | `UISetupPage` | Expansion-port 5V output toggle | +| `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | User configuration for dropping privileges in external processes / PTY commands | + +### 7.3 Temporary Business Inputs + +The following are mostly in-memory page state and are not persisted by default: + +- Host/Port/User defaults in `UISSHPage` are initialized in the constructor and are not written to configuration. +- The `UIMeshPage` message input buffer only exists in page memory. +- The current path and selected row in `UIFilePage` only exist in page memory. +- The network interface list in `UIIpPanelPage` is refreshed every second from `cp0_network_list()`. + +## 8. Settings-Page Configuration Write Paths + +`UISetupPage` is where configuration keys are concentrated. Typical functions: + +- `menu_init()`: builds the settings menu and reads `app_*`, `extport_*`. +- `save_app_toggle()`: saves launcher application toggles. +- `enter_brightness_adjust()` / `apply_value_selection()`: applies brightness, volume, screen-off timeout, resolution, and startup mode. +- `apply_volume()`: writes system volume and saves `volume`. + +Example: + +```cpp +void save_app_toggle(int idx) +{ + char cfg_key[64]; + snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); + bool enabled = menu_items_[0].sub_items[idx].toggle_state; + cp0_config_set_int(cfg_key, enabled ? 1 : 0); + cp0_config_save(); +} +``` + +When changing configuration keys, check all of the following in sync: + +- `app_keys` / `app_labels` in `UISetupPage::menu_init()`. +- The `app_keys` and always-on list in `UISetupPage::save_app_toggle()`. +- `APP_ENABLED("...")` in `Launch.cpp`. +- Documentation and default configuration. + +## 9. Resource Naming Recommendations + +- Name home icons as `_100.png`, such as `music_100.png` and `setting_100.png`. +- Name small icons or status backgrounds by function, such as `status_time_background.png` and `status_battery_background.png`. +- Use a page prefix for page-specific resources, such as `setting_ok.png` and `setting_cross.png`. +- Use short names for sound effects, such as `switch.wav`, `enter.wav`, and `key_back.wav`. +- Use real file names for fonts, such as `Montserrat-Bold.ttf`, and load them through `launcher_fonts().get()`. + +## 10. Common Issues and Notes + +- Image and audio extensions are lowercased for classification, but the file system is still case-sensitive, so the file name itself must match. +- `cp0_file_path()` classifies only by extension and does not check whether the file exists. +- The `.desktop` `Icon` value does not automatically call `cp0_file_path()`; use a path that LVGL can read directly, or keep it consistent with existing templates. +- If a new resource is used on the device side, confirm that packaging scripts include `projects/APPLaunch/APPLaunch/share/...` in the install package. +- If `cp0_config_save()` is forgotten after writing configuration, the value will be lost after reboot. +- `app_*` toggles affect the list the next time `LaunchImpl` is constructed; changing them at runtime may not immediately update the fixed home list, depending on whether a rebuild/restart is triggered. +- `run_as_user` affects the execution identity of external processes and PTY commands. Check this setting when debugging permission issues. diff --git a/docs/launcher-project-guide/07-input-system-and-key-mapping.md b/docs/launcher-project-guide/07-input-system-and-key-mapping.md new file mode 100644 index 00000000..2fcae328 --- /dev/null +++ b/docs/launcher-project-guide/07-input-system-and-key-mapping.md @@ -0,0 +1,420 @@ +# 07 - Input System and Key Mapping + +This chapter explains APPLaunch's keyboard input thread, the `key_item` event structure, LVGL event dispatch, key mappings on the home screen and built-in pages, terminal input escaping, and debugging notes. The key source files are `projects/APPLaunch/main/include/keyboard_input.h`, `projects/APPLaunch/main/ui/ui.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/components/page_app/*.hpp`. + +## 1. Input System Overview + +APPLaunch has two input paths: + +1. Custom `LV_EVENT_KEYBOARD`: carries the full `struct key_item`; most pages listen to it directly. +2. LVGL indev key: `cp0_keypad_read_cb()` also converts evdev keys to `LV_KEY_*` for LVGL's group/focus mechanism. + +Data flow: + +```text +Physical keyboard / SDL keyboard + | + v +libinput / SDL keyboard backend + | + v +keyboard_read_thread() + | + v +enqueue_key(struct key_item) + | + v +keyboard_queue + keyboard_mutex + | + v +cp0_keypad_read_cb() + | + +-- lv_obj_send_event(lv_screen_active(), LV_EVENT_KEYBOARD, key_item) + +-- ui_global_hint_on_key(key_item) + +-- data->key = cp0_evdev_process_key(key_code) +``` + +`LV_EVENT_KEYBOARD` is an APPLaunch custom event, not a built-in LVGL key event. It is registered during startup in `main.cpp`: + +```cpp +if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); +``` + +## 2. `key_item` Data Structure + +`projects/APPLaunch/main/include/keyboard_input.h` defines input events: + +```c +struct key_item { + uint32_t key_code; // Linux evdev key code + uint32_t keysym; // primary XKB keysym + uint32_t codepoint; // Unicode code point, 0 if there is no character + uint32_t mods; // KBD_MOD_* modifier bitmap + int key_state; // 0=released, 1=pressed, 2=repeat + char sym_name[65]; // XKB keysym name + char utf8[16]; // UTF-8 character + char flage; + STAILQ_ENTRY(key_item) entries; +}; +``` + +Constants: + +| Constant | Value/meaning | +| --- | --- | +| `KBD_KEY_RELEASED` | `0`, released | +| `KBD_KEY_PRESSED` | `1`, pressed | +| `KBD_KEY_REPEATED` | `2`, long-press repeat | +| `KBD_MOD_SHIFT` | Shift modifier | +| `KBD_MOD_CTRL` | Ctrl modifier | +| `KBD_MOD_ALT` | Alt modifier | +| `KBD_MOD_LOGO` | Logo modifier | +| `KBD_MOD_CAPS` | CapsLock state | +| `KBD_MOD_NUM` | NumLock state | + +Pages can use `key_code` for physical key checks, or use `utf8` / `codepoint` to read text input. + +## 3. Event Macros and Page Access Pattern + +`projects/APPLaunch/main/ui/ui.h` provides common macros: + +```c +#define LV_EVENT_KEYBOARD_GET_KEY(e) \ + ((struct key_item *)lv_event_get_param(e))->key_code + +#define LV_EVENT_KEYBOARD_GET_KEY_STATE(e) \ + ((struct key_item *)lv_event_get_param(e))->key_state + +#define IS_KEY_PRESSED(e) \ + ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && \ + (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) + +#define IS_KEY_RELEASED(e) \ + ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && \ + (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) +``` + +Typical page event binding: + +```cpp +void event_handler_init() +{ + lv_obj_add_event_cb(root_screen_, UIIpPanelPage::static_lvgl_handler, + LV_EVENT_ALL, this); +} + +static void static_lvgl_handler(lv_event_t *e) +{ + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self || !IS_KEY_RELEASED(e)) + return; + + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + self->handle_key(key); +} +``` + +Note: most menu pages handle keys only on release to avoid duplicate triggers from press and repeat. Game-like pages may handle movement and shooting on press/repeat. + +## 4. Device-Side Input Thread + +The device implementation is in `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`. + +### 4.1 Initialization + +`init_input()` does three things: + +```c +if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + +pthread_create(&keyboard_read_thread_id, NULL, + keyboard_read_thread, (void *)keyboard_device); + +cp0_create_lvgl_input_devices(); +``` + +The keyboard device defaults to: + +```c +const char *keyboard_device = getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); +``` + +If the environment variable is empty, `keyboard_read_thread()` uses this default: + +```text +/dev/input/by-path/platform-3f804000.i2c-event +``` + +This path can also be queried with `cp0_file_path("keyboard_device")`. + +### 4.2 Reading and Enqueuing + +`keyboard_read_thread()` uses libinput to listen for keyboard events, uses xkbcommon to generate `keysym`, `codepoint`, and `utf8`, and uses timerfd to generate repeat events. + +The enqueue function `enqueue_key()`: + +```c +static void enqueue_key(const struct key_item *src) { + struct key_item *elm = malloc(sizeof(*elm)); + *elm = *src; + + if (elm->key_code == KEY_ESC) { + LVGL_HOME_KEY_FLAG = elm->key_state; + } + + if (LVGL_RUN_FLAGE) { + pthread_mutex_lock(&keyboard_mutex); + STAILQ_INSERT_TAIL(&keyboard_queue, elm, entries); + pthread_mutex_unlock(&keyboard_mutex); + } else { + free(elm); + } +} +``` + +Key global state: + +| Variable | Meaning | +| --- | --- | +| `keyboard_queue` | Queue of `key_item` events waiting for LVGL consumption | +| `keyboard_mutex` | Queue lock | +| `LVGL_HOME_KEY_FLAG` | Current ESC state; used by long-press return / process-kill logic when an external app is running | +| `LVGL_RUN_FLAGE` | Whether LVGL accepts input; external apps may set it to 0 while running | +| `LV_EVENT_KEYBOARD` | Custom LVGL event id | + +### 4.3 Dequeuing and Dispatch + +`cp0_keypad_read_cb()` takes events from the queue and dispatches them to the current active screen: + +```c +lv_obj_t *root = lv_screen_active(); +if (root) + lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); + +ui_global_hint_on_key(elm); + +data->key = cp0_evdev_process_key(elm->key_code); +if (data->key) { + data->state = (lv_indev_state_t)elm->key_state; + data->continue_reading = !STAILQ_EMPTY(&keyboard_queue); +} +free(elm); +``` + +Note: `elm` is freed after the callback returns, so pages must not keep the pointer returned by `lv_event_get_param(e)` for long-term use. Copy fields if asynchronous use is required. + +## 5. evdev to LVGL Key Conversion + +`cp0_evdev_process_key()` converts selected Linux evdev keys to LVGL navigation keys: + +| evdev key | LVGL key | +| --- | --- | +| `KEY_UP` | `LV_KEY_UP` | +| `KEY_DOWN` | `LV_KEY_DOWN` | +| `KEY_LEFT` | `LV_KEY_LEFT` | +| `KEY_RIGHT` | `LV_KEY_RIGHT` | +| `KEY_ESC` | `LV_KEY_ESC` | +| `KEY_DELETE` | `LV_KEY_DEL` | +| `KEY_BACKSPACE` | `LV_KEY_BACKSPACE` | +| `KEY_ENTER` | `LV_KEY_ENTER` | +| `KEY_NEXT` | `LV_KEY_NEXT` | +| `KEY_PREVIOUS` | `LV_KEY_PREV` | +| `KEY_HOME` | `LV_KEY_HOME` | +| `KEY_END` | `LV_KEY_END` | + +If a page handles `LV_EVENT_KEYBOARD` directly, it usually uses the raw `KEY_*` values. If a page delegates to LVGL's widget focus mechanism, it relies on `data->key`. + +`projects/APPLaunch/main/include/compat/input_keys.h` includes `` on Linux and provides common compatible `KEY_*` definitions on non-Linux platforms, so SDL/desktop builds can also compile page code. + +## 6. Home Screen Key Mapping + +Home screen key handling is in `projects/APPLaunch/main/ui/UILaunchPage.cpp::main_key_switch()`. + +First, the commonly used `F/X/Z/C` keys on CardputerZero are mapped to arrow keys: + +```cpp +static uint32_t fzxc_to_arrow(uint32_t key) +{ + switch (key) { + case KEY_F: return KEY_UP; + case KEY_X: return KEY_DOWN; + case KEY_Z: return KEY_LEFT; + case KEY_C: return KEY_RIGHT; + default: return key; + } +} +``` + +Home screen behavior: + +| Input | Trigger timing | Behavior | +| --- | --- | --- | +| `KEY_LEFT` or `Z` | pressed/repeat | Play `switch.wav`, call `switch_right()`, and rotate right to the next item | +| `KEY_RIGHT` or `C` | pressed/repeat | Play `switch.wav`, call `switch_left()`, and rotate left to the next item | +| `KEY_ENTER` | released | Play `enter.wav` and launch the current app | +| `KEY_F12` | released | Toggle the green full-screen debug overlay and set `lvping_lock` | +| `KEY_UP` / `KEY_DOWN` or `F` / `X` | pressed/repeat | No action is currently defined on the home screen | + +Note: `main_key_switch()` handles left/right keys on press, so a long press may generate repeat events and switch continuously. ENTER launches on release to avoid repeated launches while the key is held down. + +## 7. Built-In Page Key Mapping Overview + +Each page independently binds `LV_EVENT_KEYBOARD` on its `root_screen_`. Common conventions are: + +| Page | File | Main keys | +| --- | --- | --- | +| `UIConsolePage` | `ui_app_console.hpp` | ESC/arrow/Enter/Backspace are converted to PTY control sequences; HOME-related state is used for exit/external locks | +| `UIGamePage` | `ui_app_game.hpp` | Arrow keys move, ENTER starts/restarts, ESC returns | +| `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN or F/X selects, ENTER/RIGHT or C enters/confirms, ESC/LEFT or Z returns, some pages support R/D | +| `UIMusicPage` | `ui_app_music.hpp` | F/X/Z/C map to LV_KEY_UP/DOWN/LEFT/RIGHT; ENTER plays/loads; ESC returns | +| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | F/X/Z/C map to LV_KEY_*; UP/DOWN selects; ESC returns | +| `UIFilePage` | `ui_app_file.hpp` | UP/DOWN selects; RIGHT/ENTER enters; LEFT goes to parent; ESC returns home or to the parent | +| `UISSHPage` | `ui_app_ssh.hpp` | UP/DOWN switches Host/Port/User; character input; BACKSPACE deletes; ENTER connects; ESC returns | +| `UIMeshPage` | `ui_app_mesh.hpp` | S opens input; R refreshes; UP/DOWN browses; ENTER sends; BACKSPACE deletes; ESC cancels/returns | +| `UICameraPage` | `ui_app_camera.hpp` | ESC returns/exits page; ENTER takes photo/confirms; UP/DOWN/LEFT/RIGHT navigate; 1-5 shortcut buttons | +| `UIRecPage` | `ui_app_rec.hpp` | Handles navigation, confirm, and return based on recording/list state | +| `UICompassPage` | `ui_app_compass.hpp` | F4/F6 calibrates or switches; ESC returns | +| `UILoraPage` | `ui_app_lora.hpp` | Converts KEY_UP/DOWN/LEFT/RIGHT/ENTER/ESC/BACKSPACE/DELETE to LV_KEY_* and passes them to business logic | +| `UITankBattlePage` | `ui_app_tank_battle.hpp` | `33(F)` up, `45(X)` down, `44(Z)` left, `46(C)` right, `57(SPACE)` fire, ESC returns | + +## 8. F/X/Z/C Direction-Key Convention + +On the CardputerZero keyboard, `F/X/Z/C` are commonly used as arrow-key substitutes. The codebase uses three patterns: + +1. Home screen `UILaunchPage.cpp`: `fzxc_to_arrow()` converts `F/X/Z/C` to `KEY_UP/DOWN/LEFT/RIGHT`. +2. Page-local conversion to LVGL keys, for example in `UIMusicPage` and `UIIpPanelPage`: + +```cpp +switch (key) { +case KEY_F: return LV_KEY_UP; +case KEY_X: return LV_KEY_DOWN; +case KEY_Z: return LV_KEY_LEFT; +case KEY_C: return LV_KEY_RIGHT; +} +``` + +3. Games directly use evdev numbers: `KEY_MOVE_UP = 33`, `KEY_MOVE_DOWN = 45`, `KEY_MOVE_LEFT = 44`, `KEY_MOVE_RIGHT = 46` in `UITankBattlePage`. + +New pages should prefer symbolic names such as `KEY_F` and avoid bare numbers. If numeric values are kept for compatibility with historical hints, comments should state the corresponding key names. + +## 9. Text Input + +Some pages need character input, such as SSH, Mesh, WiFi passwords, and the terminal. + +### 9.1 Simple ASCII Mapping + +`UISSHPage` and `UIMeshPage` use `keycode_to_char()` to convert `KEY_1`, `KEY_Q`, and similar keys to lowercase characters: + +```cpp +static char keycode_to_char(uint32_t key) +{ + if (key >= KEY_1 && key <= KEY_9) return '1' + (key - KEY_1); + if (key == KEY_0) return '0'; + if (key >= KEY_Q && key <= KEY_P) return qwerty[key - KEY_Q]; + if (key == KEY_SPACE) return ' '; + if (key == 52) return '.'; // KEY_DOT + if (key == 12) return '-'; // KEY_MINUS + return 0; +} +``` + +This approach is simple, but it does not support Shift uppercase, input methods, or multibyte characters. For full text input capability, read `key_item::utf8` or `codepoint`. + +### 9.2 Terminal Input + +`UIConsolePage` reads `struct key_item` directly and converts physical keys and UTF-8 text to a PTY byte stream: + +- `KEY_ENTER` -> `\r` +- `KEY_BACKSPACE` -> `0x7f` +- `KEY_ESC` -> `0x1b` +- Arrow keys -> `\033[A/B/C/D` or `\033OA/OB/OC/OD` in application cursor mode +- Normal characters -> `key_item::utf8` + +The terminal page also handles child-process exit, screen refresh, cursor blinking, and ESC/Home return semantics, so it is more complex than ordinary pages. + +## 10. Input Handling While External Apps Are Running + +External apps are launched through `LaunchImpl::launch_Exec()`: + +```cpp +LVGL_RUN_FLAGE = 0; +lv_indev_set_group(indev, NULL); +lv_timer_enable(false); + +int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); + +lv_timer_enable(true); +lv_indev_set_group(indev, UILaunchPage::home_input_group()); +lv_disp_load_scr(ui_Screen1); +LVGL_RUN_FLAGE = 1; +``` + +Meaning: + +- While an external process is running, APPLaunch pauses the LVGL timer and stops receiving normal keyboard queue events. +- ESC state still updates `LVGL_HOME_KEY_FLAG`, which is used by `APPLaunch_lock()` or external-process return logic. +- After the external process exits, the home screen, input group, and LVGL timer are restored. + +`main.cpp::APPLaunch_lock()` also checks the lock-file holder. If an external app holds the lock and ESC is held for about 5 seconds, it calls `cp0_process_kill(holder_pid, 3000)` to try to terminate the external app. + +## 11. Input Group Switching + +The home screen and pages each have their own LVGL group: + +- Home screen: `UILaunchPage::home_input_group()`. +- Built-in pages: `AppPageRoot::input_group()`. +- Nested terminal: `UIConsolePage::input_group()`. + +When switching pages, the input group must be switched at the same time: + +```cpp +lv_disp_load_scr(p->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); +``` + +Returning to the home screen: + +```cpp +UILaunchPage::bind_home_input_group(); +lv_disp_load_scr(ui_Screen1); +``` + +If the screen has switched but the group still points to the old page, the following can occur: + +- The visible page does not respond to keys. +- An invisible page still responds to keys. +- ESC behavior is abnormal after exiting a nested page. + +## 12. Debugging Input Issues + +The device keyboard layer already has logs: + +```text +[KBD] enqueue code=... state=... sym=... utf8=... cp=... mods=... run=... home_flag=... +[INDEV] dequeue code=... state=... sym=... utf8=... cp=... active_screen=... +[LAUNCHER] main_key_switch raw=...->code=... state=... sym=... +``` + +Recommended investigation order: + +1. Confirm whether `keyboard_read_thread()` started and whether the device path is correct. +2. Check whether `[KBD] enqueue` appears; if not, the issue is in the libinput/device/xkb layer. +3. Check whether `[INDEV] dequeue` appears; if not, the queue may not be consumed by the LVGL indev. +4. Check whether `active_screen` is the current page's screen. +5. Check whether the page bound `LV_EVENT_KEYBOARD` on `root_screen_`. +6. Check whether the page handles press, release, or repeat, and whether the trigger timing is inconsistent. +7. Check whether `LVGL_RUN_FLAGE` is 0; normal events are discarded while an external app is running. + +## 13. Recommendations for New Page Key Handling + +New pages should follow these rules: + +- List and menu pages: handle `KEY_UP/DOWN/LEFT/RIGHT/ENTER/ESC` on `IS_KEY_RELEASED(e)`. +- Game pages: handle continuous actions on `IS_KEY_PRESSED(e)` and accept repeat if needed. +- Text input pages: prefer `key_item::utf8`; use `keycode_to_char()` only for simple cases. +- Back key: ESC must exit the current page or current popup; for multi-level views, return to the previous level first, then return home. +- Direction substitute keys: if the device keyboard is supported, consistently support `F/X/Z/C`. +- Do not save `struct key_item *` pointers; copy `key_code`, `utf8`, and other fields when asynchronous handling is needed. +- For keys that can be held down, explicitly distinguish `KBD_KEY_PRESSED`, `KBD_KEY_REPEATED`, and `KBD_KEY_RELEASED` to avoid repeated confirmation or repeated launch. diff --git a/docs/launcher-project-guide/08-build-and-compilation-guide.md b/docs/launcher-project-guide/08-build-and-compilation-guide.md new file mode 100644 index 00000000..0cf3b273 --- /dev/null +++ b/docs/launcher-project-guide/08-build-and-compilation-guide.md @@ -0,0 +1,907 @@ +# 08 - Build and Compilation Guide + +This chapter explains the complete build process for `projects/APPLaunch`, covering Linux SDL2 native simulation, native device builds, Linux x86 cross-compilation, macOS cross-compilation, dependency installation, environment variables, key SCons logic, and common error handling. + +All commands are assumed to start from the repository root by default: + +```bash +cd /home/nihao/w2T/github/launcher +``` + +## 1. Build Target Overview + +APPLaunch can be built in several forms. The core difference is determined by the configuration file pointed to by `CONFIG_DEFAULT_FILE`. + +| Build target | Run location | Configuration file | Display/input backend | Typical use | +| --- | --- | --- | --- | --- | +| Linux SDL2 native simulation | Linux x86_64 development machine | `linux_x86_sdl2_config_defaults.mk` | SDL2 window + SDL input | Daily UI debugging and rapid development | +| Native device build | M5CardputerZero AArch64 Linux | `config_defaults.mk` | Linux framebuffer + evdev | Build and run directly on the device | +| Linux x86 cross-compilation | Linux x86_64 development machine, output runs on the device | `linux_x86_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | Recommended way to build official device artifacts | +| macOS cross-compilation | macOS development machine, output runs on the device | `mac_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | Generate arm64 device artifacts on macOS | +| macOS SDL/Darwin configuration | macOS development machine | `darwin_config_defaults.mk` | SDL-related configuration | Base configuration for native SDL work | + +Build artifacts usually appear in: + +```text +projects/APPLaunch/dist/ +├── M5CardputerZero-APPLaunch +├── APPLaunch/ +└── store_cache_sync.py +``` + +Where: + +- `M5CardputerZero-APPLaunch` is the main executable. +- `APPLaunch/` is the runtime resource tree and is copied to `dist/APPLaunch`. +- `store_cache_sync.py` comes from repository file `doc/store_cache_sync.py` and is copied together with `STATIC_FILES`. + +## 2. Prerequisites + +### 2.1 Submodules and Directory Layout + +For a first clone, use: + +```bash +git clone --recursive https://github.com/CardputerZero/launcher.git +cd launcher +``` + +If the repository was already cloned but submodules were not initialized: + +```bash +git submodule update --init --recursive +``` + +APPLaunch's top-level `SConstruct` assumes this directory relationship: + +```text +launcher/ +├── SDK/ +├── ext_components/ +└── projects/ + └── APPLaunch/ + ├── SConstruct + └── main/SConstruct +``` + +Enter the APPLaunch project directory before building: + +```bash +cd projects/APPLaunch +``` + +Do not run APPLaunch's `scons` directly from the repository root, because `PROJECT_PATH`, `SDK_PATH`, and `EXT_COMPONENTS_PATH` are derived from the current project directory. + +### 2.2 Python Dependencies + +SCons and the Kconfig tools require Python 3.8 or later. + +```bash +python3 --version +``` + +Common Python packages: + +```bash +python3 -m pip install --user parse scons requests tqdm +python3 -m pip install --user setuptools-rust paramiko scp +``` + +Package purposes: + +| Package | Purpose | +| --- | --- | +| `scons` | Main build entry point | +| `parse` | Used by SCons scripts and SDK build tools to parse configuration/command output | +| `requests`, `tqdm` | Used when SDK tools download dependency source code or sysroot packages | +| `paramiko`, `scp` | Used by `scons push` to upload `dist` over SSH | +| `setuptools-rust` | May be required when building some Python dependencies | + +If using a virtual environment: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +## 3. Installing Dependencies on a Linux Development Machine + +### 3.1 Basic Dependencies + +Debian/Ubuntu example: + +```bash +sudo apt update +sudo apt install -y \ + python3 python3-pip python3-venv \ + build-essential pkg-config git \ + libffi-dev +``` + +### 3.2 SDL2 Simulation Dependencies + +The Linux SDL2 build calls the following in `main/SConstruct`: + +```python +pkg_config_cflags("freetype2") +pkg_config_cflags("sdl2") +pkg_config_ldflags("sdl2") +``` + +Therefore the host needs SDL2, FreeType, and input-related libraries: + +```bash +sudo apt install -y \ + libsdl2-dev libfreetype6-dev \ + libinput-dev libxkbcommon-dev libudev-dev +``` + +It is recommended to first confirm that `pkg-config` can find the libraries: + +```bash +pkg-config --cflags sdl2 +pkg-config --libs sdl2 +pkg-config --cflags freetype2 +pkg-config --libs freetype2 +``` + +### 3.3 Linux x86 Cross-Compilation Dependencies + +Cross-compiling from Linux x86_64 to M5CardputerZero AArch64 requires the GNU AArch64 cross toolchain: + +```bash +sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +``` + +Verify it: + +```bash +aarch64-linux-gnu-gcc --version +aarch64-linux-gnu-g++ --version +``` + +Cross-compilation also requires device-side headers and libraries. APPLaunch's top-level `SConstruct` automatically prepares the SDK static sysroot during cross-compilation: + +```text +SDK/github_source/static_lib_v0.0.4 +``` + +If that directory is missing or its `version` file does not match `v0.0.4`, the build script downloads this release package: + +```text +https://github.com/CardputerZero/M5CardputerZero-UserDemo/releases/download/v0.0.4/sdk_bsp.tar.gz +``` + +Therefore the first cross-compilation needs network access. In offline environments, prepare `SDK/github_source/static_lib_v0.0.4` in advance. + +## 4. Installing Dependencies on macOS + +### 4.1 Python Environment + +A virtual environment is recommended: + +```bash +python3 -m venv launcher-python-venv +source launcher-python-venv/bin/activate +pip3 install parse scons requests tqdm setuptools-rust paramiko scp +``` + +### 4.2 macOS Cross Toolchain + +`mac_cross_cp0_config_defaults.mk` specifies: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-unknown-linux-gnu-" +``` + +Install it with: + +```bash +brew tap messense/macos-cross-toolchains +brew install aarch64-unknown-linux-gnu +``` + +Verify it: + +```bash +aarch64-unknown-linux-gnu-gcc --version +aarch64-unknown-linux-gnu-g++ --version +``` + +### 4.3 macOS SDL/Darwin Dependencies + +If using `darwin_config_defaults.mk` for native SDL debugging, prepare SDL2 and FreeType. A common installation method is: + +```bash +brew install sdl2 freetype pkg-config +``` + +Confirm: + +```bash +pkg-config --cflags sdl2 +pkg-config --cflags freetype2 +``` + +## 5. Key Environment Variables + +### 5.1 `CONFIG_DEFAULT_FILE` + +`CONFIG_DEFAULT_FILE` is the most important build selection variable. + +Example: + +```bash +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +``` + +SCons passes it to Kconfig to generate: + +```text +build/config/global_config.mk +build/config/global_config.h +``` + +If it is not set, `projects/APPLaunch/SConstruct` has automatic logic: + +- When `platform.machine()` is `x86_64`, it defaults to `linux_x86_sdl2_config_defaults.mk`. +- If environment variable `CardputerZero=y`, it forces `linux_x86_cross_cp0_config_defaults.mk`. +- Native device builds usually need `CONFIG_DEFAULT_FILE=config_defaults.mk` specified explicitly to avoid incorrect detection by the default logic. + +### 5.2 `CardputerZero` + +Shortcut for selecting the cross-compilation configuration: + +```bash +export CardputerZero=y +``` + +This is equivalent to having the top-level `SConstruct` set: + +```text +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +``` + +In automation scripts, it is still recommended to write `CONFIG_DEFAULT_FILE` explicitly because it makes build-target troubleshooting easier. + +### 5.3 `SDK_PATH` and `EXT_COMPONENTS_PATH` + +APPLaunch's top-level `SConstruct` automatically sets: + +```python +os.environ["SDK_PATH"] = str(sdk_path) +os.environ["EXT_COMPONENTS_PATH"] = str(sdk_path.parent / "ext_components") +``` + +Meaning: + +| Variable | Default value | Purpose | +| --- | --- | --- | +| `SDK_PATH` | `SDK` under the repository root | Lets the SDK build system find Kconfig, SCons tools, and built-in components | +| `EXT_COMPONENTS_PATH` | `ext_components` under the repository root | Lets the build system load extension components such as `cp0_lvgl`, `Miniaudio`, and `Sigslot` | + +Usually do not override these variables manually unless you are actually testing an external SDK or component directory. + +### 5.4 `CONFIG_TOOLCHAIN_SYSROOT` + +During cross-compilation, the top-level `SConstruct` automatically writes a temporary configuration: + +```text +build/config/config_tmp.mk +``` + +The content looks like: + +```make +CONFIG_TOOLCHAIN_SYSROOT="/path/to/launcher/SDK/github_source/static_lib_v0.0.4" +CONFIG_TOOLCHAIN_FLAGS="-I/path/to/launcher/SDK/github_source/static_lib_v0.0.4/usr/include/aarch64-linux-gnu" +``` + +After reading it, the SDK build system appends: + +```text +--sysroot=$CONFIG_TOOLCHAIN_SYSROOT +-I$CONFIG_TOOLCHAIN_SYSROOT/usr/include +-I$CONFIG_TOOLCHAIN_SYSROOT/usr/include/ +-L$CONFIG_TOOLCHAIN_SYSROOT/lib/ +-L$CONFIG_TOOLCHAIN_SYSROOT/usr/lib/ +``` + +`main/SConstruct` also uses it to append include and link paths for FreeType, libpng, and libcamera. + +### 5.5 `APPLAUNCH_STARTUP_ANIMATION` + +The startup animation is an optional compile-time macro: + +```bash +export APPLAUNCH_STARTUP_ANIMATION=1 +``` + +When this variable is `1`, `main/SConstruct` adds: + +```text +-DAPPLAUNCH_STARTUP_ANIMATION +``` + +If it is not set, startup-animation code is not enabled. + +### 5.6 Debugging Build Output + +When `CONFIG_COMMPILE_DEBUG` is not set, the SDK build system uses concise output such as `CXX ...` and `Linking ...`. To see full compiler commands, try: + +```bash +export CONFIG_COMMPILE_DEBUG=y +scons -j8 +``` + +## 6. Linux SDL2 Native Build and Run + +This is the most common mode for UI development. The artifact runs in an SDL2 window on a Linux x86_64 development machine. + +### 6.1 Clean Old Configuration + +Clean before switching build targets. This is especially important when switching from cross-compilation back to SDL2 because the old `build/config/global_config.mk` keeps the previous target configuration. + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +``` + +### 6.2 Build + +```bash +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons -j8 +``` + +If the current machine is `x86_64`, you can also omit `CONFIG_DEFAULT_FILE`, because the top-level `SConstruct` defaults to the SDL2 configuration. In documentation and scripts, explicit selection is recommended. + +### 6.3 Run + +```bash +cd dist +./M5CardputerZero-APPLaunch +``` + +The SDL2 configuration enables: + +```make +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_V9_5_LV_OS_PTHREAD=y +``` + +Therefore, when running from the `dist` directory, LVGL's POSIX filesystem root is the current directory and resource paths can resolve through `./APPLaunch/...`. If you run `dist/M5CardputerZero-APPLaunch` directly from `projects/APPLaunch`, resource-relative paths may differ, so entering `dist` first is recommended. + +### 6.4 Libraries Linked by the SDL2 Build + +When the configuration file includes `linux_x86_sdl2_config_defaults.mk`, `main/SConstruct` additionally: + +- Adds FreeType compile and link parameters to the LVGL component. +- Adds SDL2 compile and link parameters to APPLaunch. +- Links `input`, `xkbcommon`, and `udev`. +- Filters out `lv_sdl_keyboard.c` from the LVGL component to avoid conflicts with the project's custom keyboard input path. + +## 7. Native Device Build + +A native device build means building APPLaunch directly on the M5CardputerZero AArch64 Linux system. The advantage is that the toolchain and runtime libraries naturally match the device; the disadvantage is that device performance and storage are limited, so builds are slower. + +### 7.1 Install Dependencies on the Device + +Run on the device: + +```bash +sudo apt update +sudo apt install -y \ + python3 python3-pip python3-venv \ + build-essential pkg-config git \ + libffi-dev libfreetype6-dev \ + libinput-dev libxkbcommon-dev libudev-dev \ + libcamera-dev libjpeg-dev +python3 -m pip install --user parse scons requests tqdm setuptools-rust paramiko scp +``` + +Package names may differ slightly between device images. If `libcamera-dev` does not exist, first confirm whether the image's package sources are enabled, or use the libcamera headers and libraries already provided by the system. + +### 7.2 Build + +```bash +cd /home/pi/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=config_defaults.mk +scons -j2 +``` + +On the device, `-j2` or `-j4` is recommended to avoid running out of memory. `config_defaults.mk` enables: + +```make +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +### 7.3 Run + +The resource root path for the device configuration is `/usr/share/APPLaunch/`, so running directly from `dist` may fail to find resources under the formal deployment path. For temporary testing, choose one of the following: + +1. Copy resources to the formal location: + +```bash +sudo mkdir -p /usr/share/APPLaunch/bin +sudo cp -a dist/APPLaunch/. /usr/share/APPLaunch/ +sudo install -m 0755 dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +2. Use the SDL2 configuration for host-side resource-path debugging; do not use it for formal device execution. + +Formal device deployment should use the `.deb` packaging and systemd service described in Chapter 09. + +## 8. Linux x86 Cross-Compilation to the Device + +This is the recommended formal build method: generate arm64 artifacts on a Linux x86_64 development machine, then package or upload them to the device. + +### 8.1 Clean and Select Configuration + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +You can also use: + +```bash +export CardputerZero=y +scons -j8 +``` + +But setting `CONFIG_DEFAULT_FILE` explicitly is recommended. + +### 8.2 Cross-Compilation Configuration Details + +Key entries in `linux_x86_cross_cp0_config_defaults.mk`: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_LINUX_FBDEV_RENDER_MODE_FULL=y +CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +Meaning: + +- Use `aarch64-linux-gnu-gcc/g++`. +- Use the device framebuffer; no SDL2 window is created. +- Use evdev to read keyboard/input events. +- Fix the resource path to `/usr/share/APPLaunch/`. +- Enable NEON assembly optimization. +- Use full render mode, which suits the device's full-screen refresh strategy. + +### 8.3 Automatic Sysroot Logic + +When the top-level `SConstruct` sees `cross` in `CONFIG_DEFAULT_FILE`, it enables `cross_package_enabled`: + +```python +if "cross" in os.environ.get("CONFIG_DEFAULT_FILE", ''): + cross_package_enabled = True +``` + +It then generates `build/config/config_tmp.mk` with: + +```text +CONFIG_TOOLCHAIN_SYSROOT="SDK/github_source/static_lib_v0.0.4" +CONFIG_TOOLCHAIN_FLAGS="-I.../usr/include/aarch64-linux-gnu" +``` + +If `SDK/github_source/static_lib_v0.0.4` is missing or its version does not match, it downloads `sdk_bsp.tar.gz`. This sysroot provides cross-compilation with: + +- Device-side system libraries. +- Headers and libraries for FreeType, libpng, libcamera, libjpeg, and others. +- Runtime-library references such as `libstdc++.so.6` needed for cross-linking. + +### 8.4 Verify Artifact Architecture + +After the build completes: + +```bash +file dist/M5CardputerZero-APPLaunch +``` + +Expected output should contain something like: + +```text +ELF 64-bit LSB executable, ARM aarch64 +``` + +Check dynamic dependency names: + +```bash +aarch64-linux-gnu-readelf -d dist/M5CardputerZero-APPLaunch | grep NEEDED +``` + +If you need to inspect symbols or segment information on the development machine: + +```bash +aarch64-linux-gnu-readelf -h dist/M5CardputerZero-APPLaunch +aarch64-linux-gnu-objdump -p dist/M5CardputerZero-APPLaunch | grep NEEDED +``` + +## 9. macOS Cross-Compilation to the Device + +### 9.1 Build Command + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk +scons -j8 +``` + +`mac_cross_cp0_config_defaults.mk` uses: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-unknown-linux-gnu-" +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +### 9.2 Additional macOS Link Paths + +`main/SConstruct` performs extra handling for `mac_cross_cp0_config_defaults.mk`: + +- Adds FreeType and libpng includes: `$CONFIG_TOOLCHAIN_SYSROOT/usr/include/freetype2` and `libpng16`. +- Adds libcamera includes, preferring `pkg-config --cflags libcamera` and falling back to `$CONFIG_TOOLCHAIN_SYSROOT/usr/include/libcamera` if that fails. +- Links `$CONFIG_TOOLCHAIN_SYSROOT/usr/lib/aarch64-linux-gnu/libstdc++.so.6`. +- Appends `-Wl,-rpath-link,...` and `-B...` to help the macOS cross linker find Linux libraries inside the sysroot. + +### 9.3 Common macOS Notes + +- Homebrew is usually under `/opt/homebrew` on Apple Silicon and `/usr/local` on Intel Mac. If the toolchain is not in `PATH`, add it manually. +- If `pkg-config` cannot find `libcamera`, the script falls back, but the sysroot must still contain the actual headers and libraries. +- The generated file is a Linux arm64 ELF and cannot run directly on macOS. + +Verify: + +```bash +file dist/M5CardputerZero-APPLaunch +``` + +The expected result is an `ARM aarch64` Linux ELF, not Mach-O. + +## 10. Key SCons Logic + +### 10.1 Top-Level `projects/APPLaunch/SConstruct` + +This file is responsible for the build entry point and global environment preparation: + +1. Defines the SDK path: + +```text +sdk_path = projects/APPLaunch/../../SDK +``` + +2. Selects the default configuration based on environment variables: + +```text +CardputerZero=y -> linux_x86_cross_cp0_config_defaults.mk +x86_64 and CONFIG_DEFAULT_FILE unset -> linux_x86_sdl2_config_defaults.mk +``` + +3. Generates `build/config/config_tmp.mk` during cross-compilation to add the sysroot. + +4. Sets: + +```text +SDK_PATH +EXT_COMPONENTS_PATH +``` + +5. Calls the SDK build system: + +```python +SConscript(str(sdk_path / "tools" / "scons" / "project.py"), variant_dir=os.getcwd(), duplicate=0) +``` + +6. Checks and downloads `static_lib_v0.0.4` during cross-compilation. + +### 10.2 SDK `project.py` + +The SDK build system does the following: + +1. Handles special commands: `menuconfig`, `clean`, `distclean`, `save`, `SET_CROSS`, and `push`. +2. Calls the Kconfig tool to generate `global_config.mk` and `global_config.h`. +3. Loads `CONFIG_...` variables from `global_config.mk` into environment variables. +4. Creates the SCons build environment and toolchain prefix. +5. Scans SDK component directories and the `ext_components` directory. +6. Loads `projects/APPLaunch/main/SConstruct` to register the main project component. +7. Builds static libraries, shared libraries, and executables. +8. Copies the executable and `STATIC_FILES` to `dist`. + +### 10.3 `projects/APPLaunch/main/SConstruct` + +This file registers the APPLaunch main-program component: + +- Runs `ui/components/generate_page_app_includes.py` to generate the built-in page include aggregation file. +- Reads the current short git hash and injects compile macro `LAUNCHER_GIT_COMMIT_RAW`. +- Collects `src/*.c*` and all source files under the `ui` directory. +- Adds includes: `main`, `main/include`, `ext_components/cp0_lvgl/include`, and `SDK/components/utilities/include`. +- Depends on components: `cp0_lvgl`, `eventpp`, `lvgl_component`, `pthread`, and `Miniaudio`. +- Optional dependency: `Backward_cpp`. +- Adds SDL2, FreeType, libinput, xkbcommon, udev, libcamera, jpeg, and other dependencies according to different configuration files. +- Pulls RadioLib through `wget_github('https://github.com/jgromes/RadioLib.git')` and directly compiles SX1262-related source files. +- Adds the `../APPLaunch` resource tree and `doc/store_cache_sync.py` to `STATIC_FILES`. +- Registers project target: `M5CardputerZero-APPLaunch`. + +## 11. Common SCons Commands + +| Command | Purpose | +| --- | --- | +| `scons -j8` | Build with 8 parallel jobs | +| `scons -c` | Clean targets known to SCons | +| `scons distclean` | Delete `build`, `dist`, `.sconsign.dblite`, `.config*`, and other configuration/artifact files | +| `scons menuconfig` | Open the Kconfig menu and regenerate configuration | +| `scons save` | Save the current `build/config/global_config.mk` back to the file pointed to by `CONFIG_DEFAULT_FILE` | +| `scons push` | Upload `dist` over SSH according to `setup.ini` | + +Recommended flow when switching targets: + +```bash +scons distclean +export CONFIG_DEFAULT_FILE=target-configuration-file +scons -j8 +``` + +Do not simply change `CONFIG_DEFAULT_FILE` and immediately run `scons -j8`, because the old `build/config/global_config.mk` may already exist and the SDK build system will not automatically regenerate the configuration. + +## 12. `menuconfig` Recommendations + +Run: + +```bash +cd projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons menuconfig +``` + +`menuconfig` generates the final configuration based on `CONFIG_DEFAULT_FILE` and the temporary configuration. After modification, it outputs to: + +```text +build/config/global_config.mk +build/config/global_config.h +``` + +If you are sure you want to make the changes persistent: + +```bash +scons save +``` + +Note: `scons save` writes back to the configuration file. In multi-person collaboration, do not casually save to shared `*_config_defaults.mk` files unless this task explicitly requires that change. + +## 13. Common Errors and Fixes + +### 13.1 `scons: command not found` + +Cause: SCons is not installed, or the Python user bin directory is not in `PATH`. + +Fix: + +```bash +python3 -m pip install --user scons +python3 -m scons --version +``` + +If `python3 -m scons` works, you can also build this way: + +```bash +python3 -m scons -j8 +``` + +### 13.2 `ModuleNotFoundError: No module named 'parse'` + +Cause: missing Python package. + +Fix: + +```bash +python3 -m pip install --user parse requests tqdm paramiko scp +``` + +In a virtual environment, run `source .venv/bin/activate` first. + +### 13.3 `Package sdl2 was not found in the pkg-config search path` + +Cause: Linux SDL2 simulation dependencies are not installed, or `PKG_CONFIG_PATH` does not include the directory containing SDL2 `.pc` files. + +Fix: + +```bash +sudo apt install -y libsdl2-dev pkg-config +pkg-config --cflags sdl2 +``` + +macOS: + +```bash +brew install sdl2 pkg-config +pkg-config --cflags sdl2 +``` + +### 13.4 `Package freetype2 was not found` + +Fix: + +```bash +sudo apt install -y libfreetype6-dev +pkg-config --cflags freetype2 +``` + +macOS: + +```bash +brew install freetype pkg-config +pkg-config --cflags freetype2 +``` + +### 13.5 `aarch64-linux-gnu-gcc: not found` + +Cause: Linux cross toolchain is not installed, or `PATH` does not include the toolchain. + +Fix: + +```bash +sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +aarch64-linux-gnu-gcc --version +``` + +macOS cross-compilation should use `aarch64-unknown-linux-gnu-gcc`; the corresponding configuration file is `mac_cross_cp0_config_defaults.mk`. + +### 13.6 Failed to Download `sdk_bsp.tar.gz` + +Cause: the first cross-compilation needs to download `static_lib_v0.0.4`, but the network is unavailable or GitHub access failed. + +Fix: + +1. Confirm that the network can access the GitHub release. +2. Run `scons -j8` again. +3. For offline environments, prepare this manually: + +```text +SDK/github_source/static_lib_v0.0.4/ +└── version # content should be v0.0.4 +``` + +If the directory exists but the version does not match, the top-level `SConstruct` still tries to update it. + +### 13.7 `libcamera` Headers or Libraries Not Found + +In cross-compilation configurations, `main/SConstruct` adds: + +```text +$CONFIG_TOOLCHAIN_SYSROOT/usr/include/libcamera +-lcamera -lcamera-base -ljpeg +``` + +Fix: + +```bash +ls SDK/github_source/static_lib_v0.0.4/usr/include/libcamera +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu | grep camera +``` + +If they are missing, update the sysroot package or install device-side development libraries and rebuild the sysroot. + +### 13.8 Link Errors: `cannot find -linput`, `-lxkbcommon`, or `-ludev` + +Native SDL2 build: install development packages. + +```bash +sudo apt install -y libinput-dev libxkbcommon-dev libudev-dev +``` + +Cross-compilation: check the sysroot: + +```bash +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libinput.* +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libxkbcommon.* +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libudev.* +``` + +### 13.9 Old Backend Still Used After Switching Configuration + +Cause: `build/config/global_config.mk` already exists, and the build system will not automatically regenerate the configuration just because the environment variable changed. + +Fix: + +```bash +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +Check the final configuration: + +```bash +grep -E 'LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk +``` + +### 13.10 SDL2 Runs to a Black Screen or Missing Resources + +Common cause: the program was not run from the `dist` directory, so `CONFIG_V9_5_LV_FS_POSIX_PATH="./"` points to the wrong location. + +Fix: + +```bash +cd projects/APPLaunch/dist +ls APPLaunch/share/images +./M5CardputerZero-APPLaunch +``` + +### 13.11 Device Reports Missing Resource Files + +The device configuration resource path is: + +```text +/usr/share/APPLaunch/ +``` + +Check: + +```bash +ls /usr/share/APPLaunch/share/images +ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +For manual deployment, make sure you copied the contents of `dist/APPLaunch`, not only the executable. + +### 13.12 RadioLib Download Failure + +`main/SConstruct` uses `wget_github('https://github.com/jgromes/RadioLib.git')` to fetch RadioLib. The first build may need network access. + +Fix: + +- Confirm that the network can access GitHub. +- Check whether a RadioLib cache already exists under `SDK/github_source`. +- Prepare the corresponding source cache in advance for offline environments. + +## 14. Recommended Build Flows + +### 14.1 Daily UI Development + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch +``` + +### 14.2 Generate Formal Device Artifacts + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +Then follow Chapter 09 for `.deb` packaging, installation, and systemd verification. + +### 14.3 Quickly Confirm the Build Target + +```bash +grep CONFIG_DEFAULT_FILE /proc/$$/environ 2>/dev/null || true +grep -E 'CONFIG_TOOLCHAIN_PREFIX|LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk +file dist/M5CardputerZero-APPLaunch +``` diff --git a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md new file mode 100644 index 00000000..1f3f0bbf --- /dev/null +++ b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md @@ -0,0 +1,901 @@ +# 09 - Packaging, Deployment, and systemd + +This chapter explains how APPLaunch is packaged from the `dist` directory into a Debian `.deb`, how it is deployed to M5CardputerZero, how systemd autostart is configured, and how to verify and troubleshoot deployment issues. + +All commands are assumed to start from the repository root by default: + +```bash +cd /home/nihao/w2T/github/launcher +``` + +## 1. Deployment Form Overview + +APPLaunch depends on two types of files on the device: + +1. Main program: `M5CardputerZero-APPLaunch`. +2. Runtime resource tree: `APPLaunch/`, including application descriptors, fonts, images, audio, scripts, and optional sub-applications. + +After formal installation, the target path is: + +```text +/usr/share/APPLaunch/ +├── applications/ +├── bin/ +│ ├── M5CardputerZero-APPLaunch +│ ├── M5CardputerZero-AppStore # packaged if it exists in dist/bin +│ ├── M5CardputerZero-Calculator # packaged if it exists in dist/bin +│ └── appstore.py # packaged if it exists in dist/bin +├── lib/ +├── share/ +│ ├── font/ +│ └── images/ +└── cache -> /var/cache/APPLaunch # created by postinst +``` + +The systemd service file is installed to: + +```text +/lib/systemd/system/APPLaunch.service +``` + +The service start command is: + +```text +/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +The working directory is: + +```text +/usr/share/APPLaunch +``` + +## 2. Build the Device Target Before Packaging + +The `.deb` should use arm64 device artifacts, not Linux SDL2 x86_64 simulation artifacts. + +Recommended cross-compilation on a Linux x86_64 development machine: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +The `file` result should contain: + +```text +ARM aarch64 +``` + +If it shows `x86-64`, you are packaging an SDL2 host artifact, which cannot be installed on the device as the formal launcher. + +Native device builds can also be used for packaging: + +```bash +cd /home/pi/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=config_defaults.mk +scons -j2 +file dist/M5CardputerZero-APPLaunch +``` + +## 3. `llm_pack.py` Packaging Script + +The packaging script is located at: + +```text +projects/APPLaunch/tools/llm_pack.py +``` + +Core constants: + +| Constant | Value | Description | +| --- | --- | --- | +| `PACKAGE_NAME` | `applaunch` | Debian package name | +| `APP_NAME` | `APPLaunch` | Base application and service name | +| `BIN_NAME` | `M5CardputerZero-APPLaunch` | Main executable name | +| `INSTALL_PPREFIX` | `usr/share` | Parent installation prefix | +| `INSTALL_PREFIX` | `usr/share/APPLaunch` | Application installation root | +| `BIN_PATH` | `usr/share/APPLaunch/bin` | Executable directory | +| `LIB_PATH` | `usr/share/APPLaunch/lib` | Dynamic-library directory | +| `SHARE_PATH` | `usr/share/APPLaunch/share` | Shared-resource directory | +| `APP_PATH` | `usr/share/APPLaunch/applications` | `.desktop` application descriptor directory | +| `SERVICE_PATH` | `lib/systemd/system` | systemd service directory | + +Default version information at the script entry point: + +```python +version = '0.2.1' +src_folder = '../dist' +revision = 'm5stack1' +``` + +Generated package filename format: + +```text +applaunch_0.2.1-m5stack1_arm64.deb +``` + +## 4. `.deb` Package Directory Structure + +After running the script, a temporary directory is generated under `projects/APPLaunch/tools`: + +```text +projects/APPLaunch/tools/debian-APPLaunch/ +├── DEBIAN/ +│ ├── control +│ ├── postinst +│ └── prerm +├── lib/ +│ └── systemd/ +│ └── system/ +│ └── APPLaunch.service +└── usr/ + └── share/ + └── APPLaunch/ + ├── applications/ + ├── bin/ + │ └── M5CardputerZero-APPLaunch + ├── lib/ + └── share/ + ├── font/ + └── images/ +``` + +The final `.deb` file is located at: + +```text +projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +``` + +## 5. Packaging Commands + +### 5.1 Install Packaging Tools + +Linux development machine: + +```bash +sudo apt update +sudo apt install -y dpkg-dev fakeroot +``` + +Only `dpkg-deb` is required: + +```bash +dpkg-deb --version +``` + +### 5.2 Run Packaging + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +python3 llm_pack.py +``` + +On success, output similar to the following appears: + +```text +Creating Debian package applaunch 0.2.1 ... +Debian package created: .../applaunch_0.2.1-m5stack1_arm64.deb +applaunch create success! +``` + +### 5.3 Specify a Custom Version + +The current script entry point uses fixed values `0.2.1` and `m5stack1`. To build a temporary custom version without modifying repository files, call the function directly from Python: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +python3 - <<'PY' +from llm_pack import create_applaunch_deb +print(create_applaunch_deb(version='0.2.1', src_folder='../dist', revision='m5stack1')) +PY +``` + +If the version number needs to change long-term, make a formal code change to `llm_pack.py` and update the release notes accordingly. + +### 5.4 Clean Packaging Artifacts + +The script supports: + +```bash +python3 llm_pack.py clean +python3 llm_pack.py distclean +``` + +Differences: + +| Command | Behavior | +| --- | --- | +| `clean` | Deletes `*.deb` in the current directory and deletes first-level subdirectories under the current directory | +| `distclean` | Deletes `*.deb` and `m5stack_*` under the current directory | + +Note: `clean` deletes first-level directories under `tools`, including temporary directories such as `debian-APPLaunch`. Do not run it from an unintended directory that contains important subdirectories. + +## 6. Packaging Script Copy Rules + +### 6.1 Main Program Lookup + +The script looks for the main program under `src_folder`, which defaults to `../dist`. + +Lookup order: + +1. `../dist/M5CardputerZero-APPLaunch` +2. `../dist/bin/M5CardputerZero-APPLaunch` + +If neither exists, it raises: + +```text +FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist +``` + +### 6.2 Additional Apps and Backends + +The script attempts to include these optional files: + +```text +../dist/bin/M5CardputerZero-AppStore +../dist/bin/appstore.py +../dist/bin/M5CardputerZero-Calculator +``` + +If present, they are copied to: + +```text +/usr/share/APPLaunch/bin/ +``` + +Non-`.py` files are set to `0755`. + +### 6.3 Resource Tree Copy + +The script preferentially copies the resource tree from source: + +```text +projects/APPLaunch/APPLaunch +``` + +The target inside the package is: + +```text +usr/share/APPLaunch +``` + +If the source resource tree does not exist, it tries: + +```text +../dist/APPLaunch +``` + +This means packaging usually does not rely only on `dist/APPLaunch`; it also copies the `APPLaunch/` resource tree from the project source directory. + +### 6.4 AppStore Image Additions + +If this directory exists: + +```text +projects/AppStore/share/images +``` + +The script copies the following images into `usr/share/APPLaunch/share/images` inside the package: + +```text +store_wordmark.png +store_arrow_*.png +``` + +## 7. Debian Control Scripts + +### 7.1 `DEBIAN/control` + +The generated control file contains: + +```text +Package: applaunch +Version: 0.2.1 +Architecture: arm64 +Maintainer: dianjixz +Original-Maintainer: m5stack +Section: APPLaunch +Priority: optional +Homepage: https://www.m5stack.com +Packaged-Date: +Description: M5CardputerZero APPLaunch +``` + +Important points: + +- `Architecture` is fixed to `arm64`. +- The script does not automatically declare `Depends`, so dependent libraries must be provided by the base image or declared in a future version. + +### 7.2 `DEBIAN/postinst` + +The post-install script runs: + +```sh +mkdir -p /var/cache/APPLaunch +ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl enable APPLaunch.service +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl start APPLaunch.service +exit 0 +``` + +Purpose: + +- Creates writable cache directory `/var/cache/APPLaunch`. +- Creates a `cache` symlink under the read-only/system resource directory. +- Enables and starts the systemd service. + +Note: if `/usr/share/APPLaunch/cache` already exists, `ln -s` may fail. The current script does not use `ln -sfn`, so check install logs on repeated installation. + +### 7.3 `DEBIAN/prerm` + +The pre-removal script runs: + +```sh +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl stop APPLaunch.service +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl disable APPLaunch.service +rm -rf /var/cache/APPLaunch +exit 0 +``` + +Purpose: + +- Stops the service. +- Disables autostart at boot. +- Deletes the cache directory. + +Note: uninstalling deletes `/var/cache/APPLaunch`; any runtime cache or app-store cache stored there is removed as well. + +## 8. systemd Service File + +The script generates: + +```ini +[Unit] +Description=APPLaunch Service + +[Service] +ExecStart=/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +WorkingDirectory=/usr/share/APPLaunch +Restart=always +RestartSec=1 +StartLimitInterval=0 + +[Install] +WantedBy=multi-user.target +``` + +Field descriptions: + +| Field | Description | +| --- | --- | +| `ExecStart` | Starts the APPLaunch main program | +| `WorkingDirectory` | Sets the current directory to `/usr/share/APPLaunch` for convenient relative-path access | +| `Restart=always` | Always restarts after the process exits | +| `RestartSec=1` | Restarts 1 second after exit | +| `StartLimitInterval=0` | Disables the default start-rate limit so systemd does not stop restarting after frequent crashes | +| `WantedBy=multi-user.target` | Starts with the multi-user target after enable | + +The current service file does not explicitly set a user, so it runs as root as a systemd system service by default. This usually helps access framebuffer, evdev, GPIO, audio, and camera devices, but it also means the program has high privileges. + +## 9. Install on the Device + +### 9.1 Copy `.deb` to the Device + +Assume the device IP is `192.168.28.177` and the username is `pi`: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +``` + +### 9.2 Install on the Device + +```bash +ssh pi@192.168.28.177 +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +If the installer reports missing dependencies, fix dependencies first: + +```bash +sudo apt-get -f install +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +### 9.3 Overwrite Installation + +Install the same package name or a higher version again: + +```bash +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +If the service is running, `postinst` attempts to enable/start it. To reduce framebuffer or input-device contention during installation, you can stop the service manually first: + +```bash +sudo systemctl stop APPLaunch.service || true +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +sudo systemctl restart APPLaunch.service +``` + +## 10. Quick Deployment with `scons push` + +In addition to `.deb`, the project supports uploading the `dist` directory through `setup.ini`. + +Configuration file: + +```text +projects/APPLaunch/setup.ini +``` + +Default content example: + +```ini +[ssh] +local_file_path = dist +remote_file_path = /home/pi/dist +remote_host = 192.168.28.177 +remote_port = 22 +username = pi +password = pi +; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' +; after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +``` + +Run: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons push +``` + +`SDK/tools/scons/push.py` will: + +1. Read `setup.ini`. +2. Iterate over all files under `local_file_path`. +3. Calculate local MD5 hashes. +4. Fetch remote file MD5 hashes through SSH. +5. Upload only changed files. +6. Optionally run `before_cmd` and `after_cmd`. + +Suitable for: + +- Quickly replacing `dist` during development. +- Quickly uploading a single build result. +- Situations where Debian install scripts do not need to be tested. + +Not suitable for: + +- Verifying the formal installation path. +- Verifying `postinst` and `prerm`. +- Verifying systemd enable/install behavior. +- Generating a distributable installation package. + +## 11. Manual Deployment + +If you do not want to use `.deb` or `scons push`, you can copy files manually. + +Upload from the development machine: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scp dist/M5CardputerZero-APPLaunch pi@192.168.28.177:/home/pi/ +scp -r dist/APPLaunch pi@192.168.28.177:/home/pi/APPLaunch-new +``` + +Install on the device: + +```bash +sudo systemctl stop APPLaunch.service || true +sudo mkdir -p /usr/share/APPLaunch/bin +sudo install -m 0755 /home/pi/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +sudo rsync -a --delete /home/pi/APPLaunch-new/ /usr/share/APPLaunch/ +sudo mkdir -p /var/cache/APPLaunch +sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +sudo systemctl daemon-reload +sudo systemctl restart APPLaunch.service +``` + +If the service file has not been installed, create `/lib/systemd/system/APPLaunch.service` manually, using the content in Section 8 as reference. + +## 12. Deployment Verification Commands + +### 12.1 Package Status + +```bash +dpkg -l | grep applaunch +dpkg -s applaunch +``` + +List files installed by the package: + +```bash +dpkg -L applaunch +``` + +Inspect `.deb` package contents without installing: + +```bash +dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb +``` + +Inspect `.deb` metadata: + +```bash +dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb +``` + +### 12.2 Files and Permissions + +```bash +ls -l /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +file /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +ls -ld /usr/share/APPLaunch +ls -l /usr/share/APPLaunch/cache +ls -l /var/cache/APPLaunch +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | head +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | head +``` + +Expected: + +- The main program has execute permission. +- The main program architecture is `ARM aarch64`. +- `/usr/share/APPLaunch/cache` points to `/var/cache/APPLaunch`. +- Image and font resources exist. + +### 12.3 Dynamic Library Dependencies + +On the device: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +Check for missing dependencies: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +``` + +If libraries are missing, install the corresponding system packages, or extend the packaging rules to place private libraries under `/usr/share/APPLaunch/lib` and configure the runtime search path. + +### 12.4 systemd Status + +```bash +systemctl status APPLaunch.service --no-pager +systemctl is-enabled APPLaunch.service +systemctl is-active APPLaunch.service +``` + +View logs: + +```bash +journalctl -u APPLaunch.service -b --no-pager +journalctl -u APPLaunch.service -b -f +``` + +Restart: + +```bash +sudo systemctl restart APPLaunch.service +``` + +Stop: + +```bash +sudo systemctl stop APPLaunch.service +``` + +Enable boot autostart: + +```bash +sudo systemctl enable APPLaunch.service +``` + +Disable boot autostart: + +```bash +sudo systemctl disable APPLaunch.service +``` + +Reload service files: + +```bash +sudo systemctl daemon-reload +``` + +### 12.5 Manual Foreground Run + +Before troubleshooting systemd, run it in the foreground first: + +```bash +sudo systemctl stop APPLaunch.service || true +cd /usr/share/APPLaunch +sudo ./bin/M5CardputerZero-APPLaunch +``` + +This lets you see standard output and crash messages directly. If foreground execution works but systemd does not, check the service file, permissions, and working directory. + +### 12.6 framebuffer and Input Devices + +Check framebuffer: + +```bash +ls -l /dev/fb* +cat /sys/class/graphics/fb0/name 2>/dev/null || true +``` + +Check input devices: + +```bash +ls -l /dev/input/ +cat /proc/bus/input/devices +``` + +Check who currently holds framebuffer or input devices: + +```bash +sudo fuser -v /dev/fb0 2>/dev/null || true +sudo fuser -v /dev/input/event* 2>/dev/null || true +``` + +If another graphics program is running, APPLaunch may not display correctly or read input correctly. + +## 13. Uninstall and Rollback + +### 13.1 Uninstall + +```bash +sudo dpkg -r applaunch +``` + +This triggers `prerm`: it stops the service, disables it, and deletes `/var/cache/APPLaunch`. + +To also clean configuration files: + +```bash +sudo dpkg -P applaunch +``` + +### 13.2 Roll Back by Installing an Older Package + +```bash +sudo systemctl stop APPLaunch.service || true +sudo dpkg -i /home/pi/applaunch_old-version-m5stack1_arm64.deb +sudo systemctl restart APPLaunch.service +``` + +Verify: + +```bash +dpkg -s applaunch | grep Version +systemctl status APPLaunch.service --no-pager +``` + +### 13.3 Temporarily Disable the Launcher + +```bash +sudo systemctl disable --now APPLaunch.service +``` + +Restore: + +```bash +sudo systemctl enable --now APPLaunch.service +``` + +## 14. Common Deployment Errors + +### 14.1 Install Error: `package architecture (arm64) does not match system` + +Cause: the device system is not arm64, or an arm64 package was installed directly on an x86_64 development machine. + +Fix: + +```bash +uname -m +dpkg --print-architecture +``` + +The `.deb` should be installed on the M5CardputerZero device, not on a Linux x86_64 development machine. + +### 14.2 Runtime Error: `Exec format error` + +Cause: wrong main-program architecture. A common case is packaging a Linux SDL2 x86_64 artifact into an arm64 package. + +Check: + +```bash +file /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +Correct fix: cross-compile again: + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +Then repackage and reinstall. + +### 14.3 Service Keeps Restarting + +Check: + +```bash +systemctl status APPLaunch.service --no-pager +journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +Common causes: + +- Missing dynamic libraries. +- Resource path does not exist. +- framebuffer or input devices are unavailable. +- The program crashes immediately at startup. +- The installed artifact has the wrong architecture. + +Further checks: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +ls /usr/share/APPLaunch/share/images +ls /dev/fb0 +``` + +### 14.4 `ln: failed to create symbolic link '/usr/share/APPLaunch/cache': File exists` + +Cause: during repeated installation, `postinst` uses `ln -s` and the target already exists. + +Fix: + +```bash +sudo rm -rf /usr/share/APPLaunch/cache +sudo mkdir -p /var/cache/APPLaunch +sudo ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache +sudo systemctl restart APPLaunch.service +``` + +To fix this at the root, change the packaging script to use `ln -sfn`, but that is a code change. + +### 14.5 `dpkg-deb: error: failed to open package info file .../DEBIAN/control` + +Cause: the packaging directory structure is incomplete, or the script failed midway and left an abnormal directory behind. + +Fix: + +```bash +cd projects/APPLaunch/tools +python3 llm_pack.py clean +python3 llm_pack.py +``` + +### 14.6 `FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist` + +Cause: the project has not been built, the build directory is not `projects/APPLaunch/dist`, or the packaging script was not run from the `tools` directory. + +Fix: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +ls -l dist/M5CardputerZero-APPLaunch +cd tools +python3 llm_pack.py +``` + +### 14.7 Black Screen After Service Starts + +Investigation order: + +1. Confirm that the executable can run in the foreground. +2. Confirm that framebuffer exists. +3. Confirm no other process is occupying the display. +4. Confirm that resource paths exist. +5. Check journal logs. + +Commands: + +```bash +sudo systemctl stop APPLaunch.service || true +cd /usr/share/APPLaunch +sudo ./bin/M5CardputerZero-APPLaunch +ls -l /dev/fb0 +sudo fuser -v /dev/fb0 2>/dev/null || true +journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +### 14.8 External Apps Cannot Start + +APPLaunch finds external apps from the resource tree and `.desktop` descriptors. First check: + +```bash +find /usr/share/APPLaunch/applications -maxdepth 1 -type f -print +find /usr/share/APPLaunch/bin -maxdepth 1 -type f -print +``` + +Confirm that external apps have execute permission: + +```bash +ls -l /usr/share/APPLaunch/bin +``` + +If `Exec` in a `.desktop` file points to a missing path, fix the resource tree or repackage. + +## 15. Pre-Release Checklist + +Before packaging: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +After packaging: + +```bash +cd tools +python3 llm_pack.py +dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb +dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 +``` + +After installation: + +```bash +dpkg -s applaunch | grep -E 'Package|Version|Architecture' +systemctl status APPLaunch.service --no-pager +systemctl is-enabled APPLaunch.service +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +ls -l /usr/share/APPLaunch/cache +journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +Functional verification: + +- APPLaunch automatically shows the home screen after the device boots. +- Keyboard/button input works. +- The home-screen application carousel can switch items. +- Resource images and fonts display correctly. +- Built-in pages can be entered and exited. +- External apps can exit and return to APPLaunch after launch. +- Optional sub-applications such as AppStore/Calculator, if packaged, can launch normally from the launcher. + +## 16. Recommended Deployment Flow + +For formal releases, use: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +cd tools +python3 llm_pack.py +scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl status APPLaunch.service --no-pager' +``` + +For fast replacement during development, use: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +scons push +``` + +Difference: `.deb` verifies the complete installation and systemd lifecycle; `scons push` is faster but cannot replace formal packaging verification. diff --git a/docs/launcher-project-guide/10-extension-development-guide.md b/docs/launcher-project-guide/10-extension-development-guide.md new file mode 100644 index 00000000..74308988 --- /dev/null +++ b/docs/launcher-project-guide/10-extension-development-guide.md @@ -0,0 +1,420 @@ +# 10 - Extension Development Guide + +This chapter explains how to extend APPLaunch, focusing on four common change types: adding a built-in page, adding an external `.desktop` app, adding image/audio/font assets, and changing settings toggles. The core code is under `projects/APPLaunch/main/ui`; platform adaptation and path resolution live under `ext_components/cp0_lvgl`. + +## 1. Entry Points to Understand Before Extending + +| Entry point | Purpose | +| --- | --- | +| `projects/APPLaunch/main/ui/Launch.cpp` | Fixed app list, dynamic `.desktop` scanning, launching built-in pages or external processes | +| `projects/APPLaunch/main/ui/components/page_app/` | Built-in page implementation directory; pages are usually header-only `.hpp` files | +| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | Shared page capabilities such as `AppPage`, top bar, `img_path()`, and `audio_path()` | +| `projects/APPLaunch/main/ui/components/generate_page_app_includes.py` | Automatically generates `page_app.h` before build and includes every `page_app/*.hpp` file | +| `projects/APPLaunch/APPLaunch/` | Runtime asset tree; after packaging it maps to `/usr/share/APPLaunch/` on the device | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | Device-side `cp0_file_path()` path rules | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 development-host `cp0_file_path()` path rules | +| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Device-side settings persistence, saved to `/var/lib/applaunch/settings` | + +APPLaunch has two kinds of app sources: + +- **Built-in pages**: compiled into the APPLaunch process and registered with `app("NAME", icon, page_v)`. When opened, APPLaunch creates a `PageT` object and switches to its screen. +- **External apps**: launched as independent processes through a fixed `Exec` value or a `.desktop` descriptor. For non-terminal apps, the Launcher pauses its LVGL timer, waits for the child process to exit, and then returns to the home page. + +## 2. Adding a Built-in Page + +Built-in pages are suitable for features that run in the same process as Launcher, use LVGL directly, and need to share the input group, top bar, or status bar. Examples include Settings, Music, Files, Camera, and LoRa pages. + +### 2.1 Create the Page File + +Create a new `.hpp` under `projects/APPLaunch/main/ui/components/page_app/`. The recommended naming style is `ui_app_xxx.hpp`. The page class should inherit from `AppPage`; set the title, create the UI, and bind key events in the constructor. + +Minimal skeleton: + +```cpp +#pragma once + +#include "../ui_app_page.hpp" + +class UIMyToolPage : public AppPage +{ +public: + UIMyToolPage() : AppPage() + { + set_page_title("MY TOOL"); + create_ui(); + event_handler_init(); + } + +private: + lv_obj_t *title_ = nullptr; + + void create_ui() + { + lv_obj_t *root = screen(); + lv_obj_set_style_bg_color(root, lv_color_hex(0x101820), LV_PART_MAIN | LV_STATE_DEFAULT); + + UIAppTopBar top("MY TOOL"); + top.create(root); + + title_ = lv_label_create(root); + lv_label_set_text(title_, "Hello APPLaunch"); + lv_obj_center(title_); + } + + void event_handler_init() + { + lv_obj_add_event_cb(screen(), &UIMyToolPage::key_event_cb, LV_EVENT_KEY, this); + } + + static void key_event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + uint32_t key = lv_event_get_key(e); + if (key == LV_KEY_ESC && self->navigate_home) { + self->navigate_home(); + } + } +}; +``` + +Notes: + +- The page must inherit from `AppPage` so it can reuse mechanisms such as `screen()`, `input_group()`, and `navigate_home`. +- Prefer calling `navigate_home()` to return to the home page. Do not call `lv_disp_load_scr(ui_Screen1)` directly, or `LaunchImpl` will not be able to release the current page object correctly. +- If the page creates LVGL timers, file descriptors, threads, or peripheral handles, release them in the destructor. +- Use 320x170 as the baseline page size. A common layout is a 20 px top bar and a 320x150 body. +- Do not hard-code absolute asset paths. Use `img_path("xxx.png")` for images and `audio_path("xxx.wav")` for audio. + +### 2.2 Confirm the Page Is Included + +`projects/APPLaunch/main/SConstruct` runs this script before building: + +```python +ui/components/generate_page_app_includes.py +``` + +The script scans `projects/APPLaunch/main/ui/components/page_app/*.hpp` and generates `projects/APPLaunch/main/ui/components/page_app.h`. In most cases, as long as the file suffix is `.hpp`, it will be included automatically during the build. + +If you check manually, `page_app.h` should contain: + +```cpp +#include "page_app/ui_app_my_tool.hpp" +``` + +### 2.3 Register the Page in the Home App List + +Open `projects/APPLaunch/main/ui/Launch.cpp` and find `LaunchImpl::LaunchImpl()`. Register a built-in page like this: + +```cpp +app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); +``` + +It is recommended to place it inside the `APP_ENABLED` control section so the Settings page can later control whether it is shown: + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("MyTool")) + app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); + +#undef APP_ENABLED +``` + +Registration rules: + +- The first argument is the display name in the home carousel. Keep it short to avoid truncation on the small screen. +- The second argument is the icon path, usually `img_path("xxx_100.png")`. +- The third argument, `page_v`, means a built-in page is created when the app is clicked. +- If the page only supports device-side hardware, place it inside `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` to avoid SDL2 build failures. + +### 2.4 Add a Settings Page Toggle + +If you want the `Launcher` menu in Settings to control whether the new page is shown, update `app_keys` and `app_labels` in `UISetupPage::menu_init()`. + +Example: + +```cpp +static const char *app_keys[] = { + "Python", "Store", "CLI", "Game", "Setting", + "Music", "Math", "MyTool" +}; + +static const char *app_labels[] = { + "Python", "Store", "CLI", "Game", "Setting", + "Music", "Math", "My Tool" +}; +``` + +`save_app_toggle()` stores the switch as `app_`, for example `app_MyTool=0`. Read the same key in `Launch.cpp`: + +```cpp +cp0_config_get_int("app_MyTool", 1) +``` + +The device-side persistence file is: + +```text +/var/lib/applaunch/settings +``` + +### 2.5 Build Verification + +SDL2 local verification: + +```bash +cd projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed +./dist/M5CardputerZero-APPLaunch +``` + +Device cross-compile verification: + +```bash +cd projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed +``` + +For device-only pages, run at least the cross build. If the page can also be displayed on the development host, use SDL2 first to quickly verify the UI and keys. + +## 3. Adding an External `.desktop` App + +External `.desktop` apps are suitable for independent executables, scripts, or terminal commands. They do not require changing the C++ app list; APPLaunch scans the `applications` directory and dynamically adds them to the home page. + +### 3.1 Place the `.desktop` File + +Development-tree path: + +```text +projects/APPLaunch/APPLaunch/applications/ +``` + +Installed device path: + +```text +/usr/share/APPLaunch/applications/ +``` + +Existing template: + +```text +projects/APPLaunch/APPLaunch/applications/vim.desktop.temple +``` + +Note that the current scanning logic only handles filenames ending in `.desktop`. `.desktop.temple` is only a template and will not be loaded. + +### 3.2 `.desktop` Field Format + +Minimal example: + +```ini +[Desktop Entry] +Name=Vim +Exec=vim +Terminal=true +Icon=share/images/email.png +Type=Application +``` + +Fields currently parsed by APPLaunch: + +| Field | Required | Description | +| --- | --- | --- | +| `Name` | Yes | Display name on the home page | +| `Exec` | Yes | Command to execute; can be an absolute path or a shell command | +| `Icon` | No | Icon path; recommended format is `share/images/xxx.png` or any path readable by LVGL | +| `Terminal` | No | `true`/`True`/`1` means run in the built-in `UIConsolePage` | +| `Sysplause` | No | Terminal apps only; controls pause behavior after the terminal command ends, default true | +| `Type` | No | Kept for desktop-file convention compatibility; APPLaunch does not currently depend on it | +| `TryExec` | No | Not currently parsed by APPLaunch; can only serve as a descriptive field | + +Example 1: launch a terminal command. + +```ini +[Desktop Entry] +Name=TOP +Exec=top +Terminal=true +Sysplause=false +Icon=share/images/cli_100.png +Type=Application +``` + +Example 2: launch an independent program. + +```ini +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/my_app +Terminal=false +Icon=share/images/my_app_100.png +Type=Application +``` + +Example 3: launch a script. + +```ini +[Desktop Entry] +Name=NetInfo +Exec=/bin/sh /usr/share/APPLaunch/bin/netinfo.sh +Terminal=true +Icon=share/images/ip_panel_100.png +Type=Application +``` + +### 3.3 External App Launch Behavior + +`Launch.cpp` supports two external-app launch modes: + +- `Terminal=true`: creates `UIConsolePage`, displays a PTY terminal inside the APPLaunch process, and executes `Exec`. +- `Terminal=false`: calls `cp0_process_exec_blocking()` to start an external process. APPLaunch pauses the LVGL timer and input group, waits for the child process to exit, and then restores the home page. + +Returning from non-terminal external apps depends on these behaviors: + +- If the child process exits normally, APPLaunch restores `ui_Screen1`. +- On the device, holding ESC for about 3 seconds sends SIGTERM to the external app process group; if it still has not exited after another 3 seconds, SIGKILL is sent. +- `cp0_process_exec_blocking()` pauses the Launcher keyboard thread so the external program can read evdev input directly. + +### 3.4 Dynamic Refresh + +APPLaunch calls `applications_load()` at startup to scan `.desktop` files. After that, `inotify`/SDL directory watching checks the application directory every 3 seconds. After adding, deleting, or editing a `.desktop` file, the carousel normally refreshes automatically without restarting Launcher. + +If it does not refresh: + +```bash +# Device side +ls -l /usr/share/APPLaunch/applications +journalctl -u APPLaunch.service -f + +# SDL2 development host: confirm APPLaunch/applications exists near the run directory +find projects/APPLaunch -path '*APPLaunch/applications*' -maxdepth 5 -type f +``` + +### 3.5 Deduplication Rules + +Dynamic apps are deduplicated by `Exec`. If two `.desktop` files have exactly the same `Exec`, the later scanned file is skipped and this message is printed: + +```text +applications_load: skip ... (duplicate Exec) +``` + +## 4. Adding Assets + +Assets include images, audio, fonts, and external programs/scripts. In the development tree, place them under `projects/APPLaunch/APPLaunch/`. During build, `main/SConstruct` copies this tree into the output/install package through `STATIC_FILES += [ADir('../APPLaunch')]`. + +### 4.1 Asset Directories + +| Type | Development-tree path | Device path | Recommended access method | +| --- | --- | --- | --- | +| Images | `projects/APPLaunch/APPLaunch/share/images/` | `/usr/share/APPLaunch/share/images/` | `img_path("xxx.png")` or `.desktop` `Icon=share/images/xxx.png` | +| Audio | `projects/APPLaunch/APPLaunch/share/audio/` | `/usr/share/APPLaunch/share/audio/` | `audio_path("xxx.wav")` | +| Fonts | `projects/APPLaunch/APPLaunch/share/font/` | `/usr/share/APPLaunch/share/font/` | `launcher_fonts().get("xxx.ttf", size, style)` | +| External apps | `projects/APPLaunch/APPLaunch/bin/` | `/usr/share/APPLaunch/bin/` | `.desktop` `Exec=/usr/share/APPLaunch/bin/xxx` | +| `.desktop` | `projects/APPLaunch/APPLaunch/applications/` | `/usr/share/APPLaunch/applications/` | Automatically scanned | + +If you add a `bin/` directory or scripts, make sure scripts have execute permission, or invoke them through `/bin/sh script.sh` in the `.desktop` file. + +### 4.2 `cp0_file_path()` Path Rules + +Key rules in the device-side `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`: + +- `cp0_file_path("applications")` -> `/usr/share/APPLaunch/applications` +- `cp0_file_path("lock_file")` -> `/tmp/M5CardputerZero-APPLaunch_fcntl.lock` +- Image extensions `png/gif/jpg/jpeg/svg` -> `share/images/` +- Audio extensions `wav/mp3/ogg` -> `/usr/share/APPLaunch/share/audio/` +- Font extensions `ttf/otf` -> `/usr/share/APPLaunch/share/font/` + +On SDL2, `sdl_lvgl_file.cpp` infers the asset root from the executable directory, current working directory, and `APPLaunch/share`, then converts paths relative to the current working directory for convenient development-host runs. + +### 4.3 Image Asset Recommendations + +- Home carousel icons should have a 100 px version, such as `mytool_100.png`. +- For small icons inside pages, provide 80 px or smaller versions if needed. +- LVGL is sensitive to image paths and formats; reusing the repository's existing PNG naming and size style is the safest option. +- If `panel_set_icon()` prints `missing/unreadable`, first check whether the file exists in the runtime asset tree, not only in the source directory. + +### 4.4 Audio Asset Recommendations + +For key sounds in a page, refer to `UISetupPage`: + +```cpp +std::string snd_enter_ = audio_path("key_enter.wav"); +cp0_signal_audio_api({"PlayFile", snd_enter_}, nullptr); +``` + +Device-side audio normally uses `/usr/share/APPLaunch/share/audio/xxx.wav`; the SDL2 side is resolved by the path adaptation layer. + +### 4.5 Font Asset Recommendations + +Place fonts under `share/font/`. In pages, prefer the shared font cache to avoid repeated creation: + +```cpp +lv_font_t *font = launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD); +lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +After adding a font, verify that FreeType is enabled in both SDL2 and device builds. The SDL2 and cross-build configurations both add FreeType-related include/link parameters for LVGL. + +## 5. Changing Settings Toggles + +The Settings page is centralized in `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`. Current settings include Launcher app visibility toggles, Boot, Screen, WiFi, Speaker, Camera, Info, About, Help, ExtPort, and others. + +### 5.1 Add a Launcher App Toggle + +Steps: + +1. Add an internal key such as `MyTool` to `app_keys` in `UISetupPage::menu_init()`. +2. Add a display label such as `My Tool` to `app_labels` in the same location. +3. Use the same key when registering the app in `Launch.cpp`: `APP_ENABLED("MyTool")`. +4. Open the Settings page, enter the `Launcher` menu, and toggle O/X. +5. If the list does not refresh after returning to the home page, restart APPLaunch. The current fixed/built-in list reads configuration when `LaunchImpl` is constructed. + +### 5.2 Add a Regular Setting + +Find the corresponding group in `menu_init()` and add an item to `sub_items`: + +```cpp +{"My Option", true, cp0_config_get_int("my_option", 1) != 0, [this]() { + bool en = cp0_config_get_int("my_option", 1) == 0; + cp0_config_set_int("my_option", en ? 1 : 0); + cp0_config_save(); +}}, +``` + +For second-level or third-level pages that choose values, refer to these existing implementations: + +- `enter_brightness_adjust()`: brightness selection. +- `enter_darktime_adjust()`: screen-off timeout selection. +- `enter_volume_adjust()` and `apply_volume()`: volume saving and application. +- `enter_camera_resolution()`: camera resolution. +- `enter_startup_mode()`: startup mode. + +### 5.3 Configuration Persistence Location + +Device-side configuration implementation: `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`. + +- Configuration directory: `/var/lib/applaunch` +- Configuration file: `/var/lib/applaunch/settings` +- Format: one `key=value` per line +- Maximum entries: `MAX_ENTRIES=32` + +Common commands: + +```bash +sudo cat /var/lib/applaunch/settings +sudo sed -i 's/^app_Music=.*/app_Music=1/' /var/lib/applaunch/settings +sudo systemctl restart APPLaunch.service +``` + +If you add many configuration items, remember that the current maximum is 32 entries. After that limit, `cp0_config_set_*` returns directly and the setting will not be saved. + +## 6. Verification Checklist When Extending + +| Check item | Method | +| --- | --- | +| Files are placed only in the correct directories | Built-in pages in `main/ui/components/page_app/`, assets in `APPLaunch/share/`, `.desktop` files in `APPLaunch/applications/` | +| SDL2 builds successfully | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | +| Device cross build succeeds | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | +| Icons display correctly | Check logs for `set panel icon missing/unreadable` | +| Page can return home | Built-in page calls `navigate_home()` on ESC; external page exits itself or returns on long ESC press | +| `.desktop` is loaded | Filename ends in `.desktop` and contains `[Desktop Entry]`, `Name`, and `Exec` | +| Settings are saved | Check whether the corresponding key is written to `/var/lib/applaunch/settings` | diff --git a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md new file mode 100644 index 00000000..3e343e04 --- /dev/null +++ b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md @@ -0,0 +1,513 @@ +# 11 - Debugging and Troubleshooting + +This chapter covers common issues during APPLaunch development and device deployment. In general, reproduce UI, asset, and input logic issues with the SDL2 build first, then use device logs to locate framebuffer, evdev, permission, and systemd problems. + +## 1. Common Debugging Commands + +### 1.1 Check Repository and Build Status + +```bash +cd /home/nihao/w2T/github/launcher + +git status --short +find docs/launcher工程详细说明 -maxdepth 1 -type f | sort +find projects/APPLaunch/APPLaunch -maxdepth 3 -type f | sort | sed -n '1,160p' +``` + +### 1.2 Build and Run SDL2 Locally + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed +./dist/M5CardputerZero-APPLaunch +``` + +Use this to: + +- Quickly verify the home page, built-in pages, carousel animation, and `.desktop` scanning. +- Check LVGL object creation and asset paths. +- Avoid device-side framebuffer, evdev, and systemd permission issues. + +### 1.3 Device-side / Cross Build + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed +``` + +If building natively on the device: + +```bash +cd /path/to/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed +``` + +### 1.4 View APPLaunch Runtime Logs + +If started by systemd: + +```bash +sudo systemctl status APPLaunch.service --no-pager +sudo journalctl -u APPLaunch.service -b --no-pager +sudo journalctl -u APPLaunch.service -f +``` + +If running the device binary manually: + +```bash +sudo systemctl stop APPLaunch.service +cd /usr/share/APPLaunch +sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch 2>&1 | tee /tmp/applaunch.log +``` + +The actual binary path depends on packaging and installation. It may also be the build output `projects/APPLaunch/dist/M5CardputerZero-APPLaunch`. + +### 1.5 Check Runtime Assets + +```bash +ls -l /usr/share/APPLaunch +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | sort | sed -n '1,120p' +find /usr/share/APPLaunch/share/audio -maxdepth 1 -type f | sort +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort +find /usr/share/APPLaunch/applications -maxdepth 1 -type f | sort +``` + +### 1.6 Check Input Devices + +```bash +ls -l /dev/input/by-path/ +ls -l /dev/input/event* +sudo evtest +``` + +Default keyboard device in code: + +```text +/dev/input/by-path/platform-3f804000.i2c-event +``` + +Override it with an environment variable: + +```bash +APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +### 1.7 Check Configuration Files + +```bash +sudo ls -l /var/lib/applaunch +sudo cat /var/lib/applaunch/settings +``` + +Common configuration keys: + +- `app_Music`, `app_Math`, `app_File`, `app_Camera`, etc.: Launcher page visibility toggles. +- `brightness`: brightness. +- `volume`: volume. +- `dark_time`: screen-off timeout. +- `cam_resolution`: camera resolution. +- `startup_mode`: startup mode. +- `extport_usb`, `extport_5vout`: extension port settings. +- `run_as_user`: user used when lowering privileges for external processes. + +## 2. Log Keyword Quick Reference + +| Keyword | Location | Meaning | +| --- | --- | --- | +| `[BOOT] lv_init() done` | `main.cpp` | LVGL initialization completed | +| `[BOOT] cp0_lvgl_init() starting...` | `main.cpp` | Starting platform adaptation layer, display, input, audio, and other initialization | +| `[BOOT] First frame flushed to fb0.` | `main.cpp` | First frame was forcibly flushed to the display device | +| `Entering main loop` | `main.cpp` | Main loop has started | +| `[LAUNCHER] set panel icon` | `Launch.cpp` | Home icon was set successfully | +| `set panel icon missing/unreadable` | `Launch.cpp` | Icon path does not exist or is unreadable | +| `applications_load: opendir failed` | `Launch.cpp` | applications directory does not exist or is unreadable | +| `missing Name or Exec` | `Launch.cpp` | `.desktop` is missing required fields | +| `duplicate Exec` | `Launch.cpp` | `.desktop` has the same Exec as an existing app | +| `Launching terminal app` | `Launch.cpp` | Entering the built-in terminal page to run a command | +| `Launching external app` | `Launch.cpp` | Starting a non-terminal external program | +| `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | Parent process read ESC while an external app was running | +| `[cp0] Returned to launcher` | `cp0_app_process.cpp` | External app exited; preparing to return home | +| `[HOME_STATUS] connected=` | `Launch.cpp` | Home status bar refreshed WiFi/battery state | + +## 3. Black Screen Troubleshooting + +For black screens, first determine whether the process did not start, LVGL did not flush the first frame, asset/page construction crashed, an external app is occupying the framebuffer, or the backlight/brightness is wrong. + +### 3.1 Quickly Check Process State + +```bash +pgrep -a M5CardputerZero-APPLaunch +sudo systemctl status APPLaunch.service --no-pager +sudo journalctl -u APPLaunch.service -b --no-pager | tail -120 +``` + +If there is no process: + +- Check whether the systemd unit's `ExecStart` path exists. +- Check whether the binary has execute permission. +- Run the binary manually and inspect stderr. + +If the process restarts repeatedly: + +```bash +sudo journalctl -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' +``` + +### 3.2 Check the Startup Log Stage + +Different stopping points suggest different directions: + +| Stop point | Possible cause | Troubleshooting direction | +| --- | --- | --- | +| No `[BOOT] lv_init()` | Program did not execute or crashed very early | systemd, binary path, dynamic libraries, permissions | +| Stops at `cp0_lvgl_init() starting` | Display/input/audio/hardware initialization stuck | framebuffer, evdev, audio device, hardware HAL | +| `ui_init done` appears but screen is black | First-frame flush failed, backlight is 0, assets make objects invisible | framebuffer, backlight, asset paths | +| Black after entering main loop | Page drawing issue or external app locked the display | Logs, lock file, external process | + +### 3.3 Check Framebuffer and Backlight + +```bash +ls -l /dev/fb0 +id +sudo cat /sys/class/backlight/backlight/brightness +sudo cat /sys/class/backlight/backlight/max_brightness +``` + +Try increasing brightness: + +```bash +echo 80 | sudo tee /sys/class/backlight/backlight/brightness +``` + +If the Settings page previously saved very low brightness, check: + +```bash +sudo grep '^brightness=' /var/lib/applaunch/settings +``` + +### 3.4 Check Whether an External App Is Occupying the Display + +When APPLaunch starts a non-terminal external app, it pauses its own LVGL timer and waits for the child process to exit. If the external app hangs, it may look like Launcher is black-screened or unresponsive. + +```bash +ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|Calculator|AppStore|my_app' | grep -v grep +``` + +First try to terminate the external app process group gracefully, or hold ESC for about 3 seconds so `cp0_process_exec_blocking()` triggers SIGTERM. + +### 3.5 Use SDL2 to Narrow the Scope + +If the SDL2 build works but the device build is black, prioritize device HAL, framebuffer, backlight, evdev, permissions, and systemd. If SDL2 is also black, prioritize UI construction, asset paths, and LVGL object styles. + +## 4. Missing Asset Troubleshooting + +Common symptoms of missing assets: blank icons, missing backgrounds, font fallback, no audio, and `missing/unreadable` in logs. + +### 4.1 Check Assets in Both Source and Runtime Directories + +```bash +# Source tree +find projects/APPLaunch/APPLaunch/share/images -maxdepth 1 -type f | sort | grep my_icon + +# Device side +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | sort | grep my_icon +``` + +If the source tree has the file but the device does not: + +- Rebuild, repackage, and reinstall. +- Check whether `STATIC_FILES += [ADir('../APPLaunch')]` still exists in `projects/APPLaunch/main/SConstruct`. +- Check whether the packaging script copies the `APPLaunch/` asset tree into the package. + +### 4.2 Check Path Spelling + +Recommended for built-in pages: + +```cpp +img_path("my_icon_100.png") +audio_path("key_enter.wav") +``` + +Recommended for `.desktop`: + +```ini +Icon=share/images/my_icon_100.png +``` + +Do not write development-host absolute paths in device-side pages, such as `/home/nihao/.../projects/APPLaunch/...`. After installation, the device asset root is `/usr/share/APPLaunch/`. + +### 4.3 Understand Image Path Special Cases + +On the device, `cp0_file_path("xxx.png")` returns `share/images/xxx.png`, which is relative to the current working directory. If you manually start the device binary from an unexpected directory, images may not be found. Run with `/usr/share/APPLaunch` as the working directory, or use the correct systemd `WorkingDirectory`. + +On SDL2, APPLaunch automatically probes `APPLaunch/share`, but it is still recommended to run the build output from `projects/APPLaunch`. + +### 4.4 Missing Fonts + +Check font files: + +```bash +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort +``` + +If adding a font causes crashes or does not display: + +- Confirm the extension is `.ttf` or `.otf`. +- Confirm the FreeType build dependency is available. +- First use existing fonts such as `Montserrat-Bold.ttf` or `AlibabaPuHuiTi-3-55-Regular.ttf` to verify page logic. + +## 5. Input Not Working + +Input failures include the home page not responding, a built-in page not responding, an external app not responding, and incorrect key-code mapping. + +### 5.1 Home Page or Built-in Page Not Responding + +Check whether the correct input group is bound: + +- Home page: `UILaunchPage::bind_home_input_group()`. +- Built-in page: after creating the page, `Launch.cpp` calls `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())`. +- Return home: `LaunchImpl::lv_go_back_home()` rebinds the home input group. + +When adding events to a built-in page, make sure the event is attached to the correct object and that the object belongs to the page input group. Refer to existing pages' `event_handler_init()` implementations. + +### 5.2 Check Whether Device evdev Has Events + +```bash +ls -l /dev/input/by-path/platform-3f804000.i2c-event +sudo evtest /dev/input/by-path/platform-3f804000.i2c-event +``` + +If the default path does not exist, temporarily override it: + +```bash +APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +### 5.3 Key-code Mapping Issues + +Related files: + +| File | Purpose | +| --- | --- | +| `ext_components/cp0_lvgl/include/compat/input_keys.h` | Compatible input key definitions | +| `projects/APPLaunch/main/include/keyboard_input.h` | APPLaunch private input header | +| `ext_components/cp0_lvgl/include/keyboard_input.h` | cp0_lvgl input interface | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | Device-side keyboard input implementation | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | SDL2 keyboard input implementation | + +Troubleshooting method: + +- In SDL2, first confirm that arrow keys, Enter, and Esc trigger the expected behavior. +- On the device, use `evtest` to read raw key codes. +- Compare against `LV_KEY_*` and the project's custom key values. +- During external app execution, check `[CP0-APP] evdev code=... value=...` logs. + +### 5.4 External App Input Not Working + +After a non-terminal external app starts, APPLaunch calls `keyboard_pause()` to pause its own keyboard thread, but it does not EVIOCGRAB the device. Both the parent and child processes can read the same evdev device. If the external app has no input: + +- Confirm the external app reads the same `/dev/input/event*`. +- Confirm the runtime user has permission to read that input device; the external app may be lowered from root to a normal user by default. +- Check the `run_as_user` configuration, or use a fixed built-in registration with `run_as_root`. +- Use `Terminal=true` first to verify whether the command can receive keyboard input in the PTY. + +## 6. External App Cannot Return + +External apps usually fail to return because the child process does not exit, the process group is not killed, ESC cannot be read from the input device, or the app took over the display and did not restore it. + +### 6.1 Normal Return Path + +`launch_Exec()` in `Launch.cpp`: + +1. Shows Loading. +2. Sets `LVGL_RUN_FLAGE = 0`. +3. Unbinds the LVGL input group. +4. Calls `lv_timer_enable(false)` to pause the LVGL timer. +5. Calls `cp0_process_exec_blocking(exec, &LVGL_HOME_KEY_FLAG, keep_root)`. +6. After the child process exits, re-enables the timer, binds the home input group, loads `ui_Screen1`, and hides Loading. + +### 6.2 First Confirm Whether the Child Process Is Still Running + +```bash +ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|my_app|sh -c' | grep -v grep +``` + +If the child process is still running: + +- Let the app exit by itself. +- Hold ESC for about 3 seconds. +- Check whether logs contain `[cp0] ESC held ... SIGTERM pgid ...`. + +### 6.3 Long ESC Press Does Not Work + +Check: + +```bash +sudo journalctl -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' +``` + +If there are no `[CP0-APP] evdev` logs: + +- The default keyboard path may be wrong. +- The parent process may not have input-device permission. +- The external app or another process may have exclusive access to the input device. + +If there is ESC DOWN but no SIGTERM: + +- The hold duration is insufficient; the current threshold is 3 seconds. +- The key code is not `KEY_ESC`; check the keyboard mapping. + +### 6.4 Child Exited but Home Page Did Not Restore + +Check whether logs contain: + +```text +[cp0] Returned to launcher +App ... exited with code ... +``` + +If return logs exist but the screen is still abnormal: + +- The external app may have changed framebuffer state or terminal mode. +- APPLaunch already attempts to force refresh after switching home through `lv_obj_invalidate()` and `lv_refr_now()`; if the screen still does not display, check framebuffer/backlight. +- Check whether the external app left a lock or background process that continues occupying the display. + +## 7. Build Failure Troubleshooting + +### 7.1 SCons Cannot Find the SDK or Components + +Symptom: `project.py`, components, or headers cannot be found. + +Troubleshoot: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +python3 - <<'PY' +from pathlib import Path +p = Path.cwd() +print('cwd=', p) +print('SDK=', p.parent.parent / 'SDK') +print('ext=', p.parent.parent / 'ext_components') +PY +ls ../../SDK/tools/scons/project.py +ls ../../ext_components/cp0_lvgl +``` + +APPLaunch's `SConstruct` automatically sets: + +```python +SDK_PATH = ../../SDK +EXT_COMPONENTS_PATH = ../../ext_components +``` + +### 7.2 Missing SDL2 / FreeType / libinput Dependencies + +The SDL2 configuration uses `pkg-config` to find `sdl2` and `freetype2`, and links `input`, `xkbcommon`, and `udev`. + +Check: + +```bash +pkg-config --cflags --libs sdl2 freetype2 +ldconfig -p | grep -E 'libinput|libxkbcommon|libudev' +``` + +Common Ubuntu/Debian dependency package names: + +```bash +sudo apt-get install scons pkg-config libsdl2-dev libfreetype-dev libinput-dev libxkbcommon-dev libudev-dev +``` + +### 7.3 Missing Cross-compilation Sysroot + +`linux_x86_cross_cp0_config_defaults.mk` uses `SDK/github_source/static_lib_v0.0.4` as the sysroot. If it does not exist, `SConstruct` tries to download `sdk_bsp.tar.gz`. This fails when network access is restricted. + +Check: + +```bash +ls -l ../../SDK/github_source/static_lib_v0.0.4 +cat ../../SDK/github_source/static_lib_v0.0.4/version 2>/dev/null || true +``` + +If the download fails, prepare the sysroot ahead of time, or complete one build in a network-enabled environment to populate the cache. + +### 7.4 Build Fails After Adding a Page + +Common causes: + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `PageT not declared` | Page class name and registration name do not match, or `.hpp` was not included by `page_app.h` | Check `page_app.h` and rerun scons | +| SDL2 build cannot find Linux headers | Page directly includes device-only headers | Wrap device-only code with `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` | +| Linker cannot find symbols | Functions called by the new page were not added to component dependencies | Check `REQUIREMENTS`/`LDFLAGS` in `main/SConstruct` | +| Duplicate definition | A header-only page defines non-inline global variables/functions | Convert them to class members, `static`, `inline`, or move them into a `.cpp` | + +### 7.5 `page_app.h` Auto-generation Changes the Working Tree + +`generate_page_app_includes.py` generates `page_app.h` sorted by filename. After adding or deleting `page_app/*.hpp`, a build may modify this file. This is expected, but before committing, confirm that the diff only contains the intended include-list change. + +## 8. `.desktop` Load Failure Troubleshooting + +### 8.1 File Was Not Scanned + +Check: + +```bash +ls -l /usr/share/APPLaunch/applications +``` + +Requirements: + +- Filename must end in `.desktop`. +- Content must contain `[Desktop Entry]`. +- At least `Name=` and `Exec=` are required. +- Blank lines and comments beginning with `#` or `;` are skipped. + +### 8.2 App Was Skipped by Deduplication + +If logs contain: + +```text +applications_load: skip ... (duplicate Exec) +``` + +An existing fixed app or another `.desktop` uses the same `Exec`. Change `Exec` to a unique command. + +### 8.3 Icon Does Not Display + +The `.desktop` `Icon` field does not automatically call `img_path()`; it is passed as-is to `panel_set_icon()`. Therefore, use: + +```ini +Icon=share/images/my_app_100.png +``` + +If you use an absolute path, also ensure the file exists and is readable on the device. + +### 8.4 Command Execution Failed + +For terminal apps, verify on the command line first: + +```bash +which vim +vim --version +``` + +For non-terminal apps, check: + +```bash +ls -l /usr/share/APPLaunch/bin/my_app +ldd /usr/share/APPLaunch/bin/my_app +sudo -u pi /usr/share/APPLaunch/bin/my_app +``` + +If APPLaunch starts as root, external apps normally attempt to lower privileges to a normal user. Apps that need root should either be registered as fixed built-in entries with `run_as_root`, or have their program permissions/group permissions adjusted to avoid unnecessary root access. + +## 9. Recommended Fault-location Order + +1. Run `git status --short` to confirm the current change scope. +2. Build and run SDL2 to eliminate basic UI/syntax issues. +3. Check whether assets exist in both `projects/APPLaunch/APPLaunch` and device `/usr/share/APPLaunch`. +4. Watch `journalctl -u APPLaunch.service -f` to identify the startup stage. +5. Use `evtest` to verify the input device and key codes. +6. Use `ps` to inspect external apps and process groups. +7. Check `/var/lib/applaunch/settings` to rule out settings toggles, brightness, or runtime user issues. +8. Finally inspect the HAL layer under `ext_components/cp0_lvgl/src/cp0/`, including framebuffer, keyboard, process, settings, and audio implementations. diff --git a/docs/launcher-project-guide/12-common-modification-entry-points.md b/docs/launcher-project-guide/12-common-modification-entry-points.md new file mode 100644 index 00000000..effbe2fd --- /dev/null +++ b/docs/launcher-project-guide/12-common-modification-entry-points.md @@ -0,0 +1,215 @@ +# 12 - Common Modification Entry Points + +This chapter organizes common APPLaunch entry points by “what do I want to change?”. Before making changes, first check the current working-tree state to avoid overwriting changes from other agents: + +```bash +cd /home/nihao/w2T/github/launcher +git status --short +``` + +## 1. High-frequency Task Entry Table + +| Task | Main files/directories | Key points | Verification | +| --- | --- | --- | --- | +| Add a built-in page | `projects/APPLaunch/main/ui/components/page_app/` | Create `ui_app_xxx.hpp` and inherit from `AppPage` | Build with SDL2 and open the page | +| Register a built-in page on home | `projects/APPLaunch/main/ui/Launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | Icon appears in the home carousel | +| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/Launch.cpp` | Settings page writes `app_Key`, Launcher reads `APP_ENABLED("Key")` | Toggle in Settings, then restart or refresh home | +| Add external `.desktop` app | `projects/APPLaunch/APPLaunch/applications/` | Filename must end in `.desktop` and include `Name` and `Exec` | No skip logs; app appears on home | +| Add icon | `projects/APPLaunch/APPLaunch/share/images/` | Built-in pages use `img_path()`, `.desktop` uses `Icon=share/images/xxx.png` | No `missing/unreadable` logs | +| Add sound effect | `projects/APPLaunch/APPLaunch/share/audio/` | Pages use `audio_path()` and `cp0_signal_audio_api()` | Sound plays on device | +| Add font | `projects/APPLaunch/APPLaunch/share/font/` | Use `launcher_fonts().get()` and confirm FreeType dependency | Page text uses the new font | +| Change home carousel layout | `projects/APPLaunch/main/ui/UILaunchPage.cpp`, `projects/APPLaunch/main/ui/UILaunchPage.h` | 5 slots, left/right switching, center card | Check animation and input in SDL2 | +| Change carousel animation | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | Card movement, scale, opacity, and other animations | Switch left/right repeatedly in SDL2 | +| Change home status bar | `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/ui.c` | `update_home_status_bar()` refreshes WiFi/time/battery | Check `[HOME_STATUS]` logs | +| Change Settings menu | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | Add `MenuItem`/`SubItem` in `menu_init()` | Enter the SETTING page and test | +| Change configuration saving logic | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Currently saves to `/var/lib/applaunch/settings`, max 32 entries | Inspect the settings file | +| Change asset path rules | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | Consider device and SDL2 consistently | Check assets on both SDL2 and device | +| Change external app launch/return | `projects/APPLaunch/main/ui/Launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | External app starts, ESC returns | +| Change terminal apps | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY, command execution, input/output | Verify with a `Terminal=true` app | +| Change input mapping | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | Device and SDL2 input differences | `evtest` + SDL2 keyboard | +| Change startup flow | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`, `cp0_lvgl_init()`, `ui_init()`, main loop | Check `[BOOT]` logs | +| Change build dependencies | `projects/APPLaunch/main/SConstruct` | `SRCS`, `INCLUDE`, `REQUIREMENTS`, `STATIC_FILES` | scons build | +| Change build configuration | `projects/APPLaunch/*.mk` | Different configs for SDL2, device, and cross build | Build with a specific `CONFIG_DEFAULT_FILE` | +| Change package contents | `projects/APPLaunch/tools/llm_pack.py`, `projects/APPLaunch/APPLaunch/` | Asset tree and install path | Check file list after building package | +| Change platform HAL | `ext_components/cp0_lvgl/src/cp0/`, `ext_components/cp0_lvgl/include/hal/` | framebuffer, audio, network, settings, process, etc. | Test on the device | + +## 2. Source Directory Quick Reference + +| Path | Purpose | +| --- | --- | +| `projects/APPLaunch/main/src/main.cpp` | APPLaunch process entry, initialization order, main loop, external app lock detection | +| `projects/APPLaunch/main/ui/ui.c` | Creates global LVGL UI objects; most `ui_*` globals originate here | +| `projects/APPLaunch/main/ui/ui.cpp` | C++ UI initialization bridge | +| `projects/APPLaunch/main/ui/ui.h` | UI global declarations and C/C++ shared interface | +| `projects/APPLaunch/main/ui/Launch.cpp` | App model, app list, launch logic, dynamic `.desktop` loading, status bar refresh | +| `projects/APPLaunch/main/ui/Launch.h` | Public wrapper class for `Launch` | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home screen, carousel slots, input events, home-page behavior | +| `projects/APPLaunch/main/ui/UILaunchPage.h` | Home class interface, including panel/label/input group accessors | +| `projects/APPLaunch/main/ui/ui_loading.cpp` | Loading overlay show/hide | +| `projects/APPLaunch/main/ui/ui_global_hint.cpp` | Global hint overlay | +| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | LVGL OS/thread helpers | +| `projects/APPLaunch/main/ui/Animation/` | Home carousel animation implementation | +| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | Built-in page base class, top bar, shared asset path helpers | +| `projects/APPLaunch/main/ui/components/page_app.h` | Auto-generated built-in page include aggregate | +| `projects/APPLaunch/main/ui/components/page_app/` | Built-in page implementation directory | +| `projects/APPLaunch/main/include/` | APPLaunch private headers and compatible input headers | + +## 3. Built-in Page Entry Table + +| Page/feature | File | Registered name or icon | Description | +| --- | --- | --- | --- | +| GAME | `projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | +| SETTING | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings page, including app toggles, brightness, volume, WiFi, camera, etc. | +| MUSIC | `projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | Music page | +| Compass | `projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass page | +| IP_PANEL | `projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP information panel, enabled on device | +| FILE | `projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | File page, enabled on device | +| SSH | `projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH page, enabled on device | +| MESH | `projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh page, enabled on device | +| REC | `projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | Recording page, enabled on device | +| CAMERA | `projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | Camera page, enabled on device | +| LORA | `projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa page, enabled on device | +| TANK | `projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game, enabled on device | +| CLI/terminal | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`, used by bash, python, and `Terminal=true` apps | + +Fixed registration entry in `LaunchImpl::LaunchImpl()`: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +## 4. External App Entry Table + +| Item | Path/function | Description | +| --- | --- | --- | +| `.desktop` directory | `projects/APPLaunch/APPLaunch/applications/` | Development tree; packaged as `/usr/share/APPLaunch/applications/` | +| Template | `projects/APPLaunch/APPLaunch/applications/vim.desktop.temple` | Example template; not scanned because the suffix is not `.desktop` | +| Scan function | `LaunchImpl::applications_load()` in `projects/APPLaunch/main/ui/Launch.cpp` | Parses `[Desktop Entry]`, `Name`, `Icon`, `Exec`, `Terminal`, and `Sysplause` | +| Directory watching | `LaunchImpl::inotify_init_watch()`, `app_dir_watch_cb()` | Watches application changes and refreshes the dynamic app list | +| Dynamic refresh | `LaunchImpl::applications_reload()` | Keeps fixed apps, deletes dynamic apps, then rescans | +| Terminal launch | `LaunchImpl::launch_Exec_in_terminal()` | Creates `UIConsolePage` and executes the command | +| Non-terminal launch | `LaunchImpl::launch_Exec()` | Pauses LVGL and calls `cp0_process_exec_blocking()` | +| Device-side process execution | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | fork, privilege lowering, long ESC press to exit, keyboard restore | +| PTY execution | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | Terminal page command execution and user selection | + +Minimal `.desktop` template: + +```ini +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/my_app +Terminal=false +Icon=share/images/my_app_100.png +Type=Application +``` + +## 5. Asset Entry Table + +| Asset | Development path | Access method | Common issue | +| --- | --- | --- | --- | +| Home/app icons | `projects/APPLaunch/APPLaunch/share/images/` | `img_path("xxx.png")` | Device-side relative paths depend on working directory; `.desktop` should use `share/images/xxx.png` | +| Page images | `projects/APPLaunch/APPLaunch/share/images/` | `img_path("xxx.png")` or `cp0_file_path_c("xxx.png")` | Filename case must match exactly | +| Audio | `projects/APPLaunch/APPLaunch/share/audio/` | `audio_path("xxx.wav")` | Device-side audio path is absolute `/usr/share/APPLaunch/share/audio/` | +| Fonts | `projects/APPLaunch/APPLaunch/share/font/` | `launcher_fonts().get("xxx.ttf", size, style)` | Requires FreeType; font objects should be cached and reused | +| External binaries/scripts | `projects/APPLaunch/APPLaunch/bin/` | `.desktop` `Exec=/usr/share/APPLaunch/bin/xxx` | Watch execute permissions and dynamic library dependencies | +| External app descriptors | `projects/APPLaunch/APPLaunch/applications/` | Automatically scans `.desktop` | `.desktop.temple` is not scanned | +| Packaged libraries | `projects/APPLaunch/APPLaunch/lib/` | Loaded by programs or scripts | Watch runtime `LD_LIBRARY_PATH` or rpath | + +Path resolution code: + +| Platform | File | Focus | +| --- | --- | --- | +| Device | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | Asset root fixed to `/usr/share/APPLaunch`; images return relative `share/images/` paths | +| SDL2 | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | Infers `APPLaunch/share` from executable directory, current directory, and parent directory | +| C interface | `cp0_file_path_c()` | Returns a thread-local cached `const char *`, suitable for LVGL style APIs | +| C++ interface | `cp0_file_path()` | Returns `std::string`; recommended inside pages | + +## 6. Settings and Persistence Entry Table + +| Setting item | UI entry | Configuration key | Implementation location | +| --- | --- | --- | --- | +| App visibility toggle | SETTING -> Launcher | `app_` | `save_app_toggle()` in `ui_app_setup.hpp`, `APP_ENABLED()` in `Launch.cpp` | +| Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | +| Screen-off timeout | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | +| Volume | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`, `cp0_volume_read/write()` | +| Camera resolution | SETTING -> Camera -> Resolution | `cam_resolution` | `ui_app_setup.hpp`, read by the camera page | +| Startup mode | Related selection in Settings page | `startup_mode` | `ui_app_setup.hpp` | +| USB extension port | SETTING -> ExtPort | `extport_usb` | `ui_app_setup.hpp` | +| 5V output | SETTING -> ExtPort | `extport_5vout` | `ui_app_setup.hpp` | +| External app runtime user | Manual configuration | `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | + +Configuration implementation: + +| File | Description | +| --- | --- | +| `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` | Declarations for `cp0_config_get_int/set_int/get_str/set_str/save` | +| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Device-side configuration read/write, saved to `/var/lib/applaunch/settings` | +| `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` | SDL2 compatibility implementation | +| `ext_components/cp0_lvgl/src/commount.c` | Applies saved brightness and volume at startup | + +## 7. Build Entry Table + +| Scenario | Command/file | Description | +| --- | --- | --- | +| Default SDL2 build | `projects/APPLaunch/SConstruct` automatically selects `linux_x86_sdl2_config_defaults.mk` | Default configuration on x86_64 development hosts | +| Explicit SDL2 build | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | Recommended for local development verification | +| Cross build | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | x86 Linux to AArch64 device | +| Native device build | `CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed` | Device-side framebuffer/evdev configuration | +| macOS cross build | `CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk scons ...` | macOS to device | +| macOS/Darwin | `darwin_config_defaults.mk` | Darwin/SDL-related configuration | +| Main build script | `projects/APPLaunch/SConstruct` | Sets SDK, EXT_COMPONENTS, sysroot download | +| Component build script | `projects/APPLaunch/main/SConstruct` | Sources, dependencies, static files, git commit macro | +| APPLaunch configuration | `projects/APPLaunch/main/Kconfig` | Main project Kconfig | +| cp0_lvgl configuration | `ext_components/cp0_lvgl/Kconfig` | Platform adaptation component configuration | + +## 8. Platform Adaptation Entry Table + +| Capability | Header | Device implementation | SDL2/compat implementation | +| --- | --- | --- | --- | +| LVGL initialization | `ext_components/cp0_lvgl/include/hal_lvgl_bsp.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c` | +| framebuffer/display | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c` | +| Keyboard input | `ext_components/cp0_lvgl/include/keyboard_input.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | +| File paths | `ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | +| Process | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp` | +| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp` | +| Audio | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c` | +| Settings/brightness/volume | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp` | +| Network/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp` | +| Screenshot | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp` | +| Camera | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp` | Device camera | `ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp` | + +## 9. Debugging Command Quick Reference + +| Purpose | Command | +| --- | --- | +| View current changes | `git status --short` | +| SDL2 build | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | +| SDL2 run | `cd projects/APPLaunch && ./dist/M5CardputerZero-APPLaunch` | +| Cross build | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | +| View systemd status | `sudo systemctl status APPLaunch.service --no-pager` | +| Follow logs | `sudo journalctl -u APPLaunch.service -f` | +| View boot logs | `sudo journalctl -u APPLaunch.service -b --no-pager` | +| Check assets | `find /usr/share/APPLaunch -maxdepth 3 -type f | sort` | +| Check `.desktop` files | `find /usr/share/APPLaunch/applications -maxdepth 1 -type f -name '*.desktop' -print -exec sed -n '1,80p' {} \;` | +| Check settings | `sudo cat /var/lib/applaunch/settings` | +| Check input devices | `ls -l /dev/input/by-path/ && sudo evtest` | +| Check external app processes | `ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|sh -c|M5CardputerZero'` | +| Check dynamic libraries | `ldd /usr/share/APPLaunch/bin/my_app` | +| Check icon logs | `sudo journalctl -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | + +## 10. Pre-/Post-change Checklist + +| Stage | Check item | +| --- | --- | +| Before change | Run `git status --short` and confirm which files already have changes from others | +| After adding a page | Confirm the `.hpp` file is in `page_app/`, and the class name matches the registration in `Launch.cpp` | +| After adding assets | Confirm files can be found in both the source tree and device `/usr/share/APPLaunch` | +| After adding `.desktop` | File suffix is `.desktop`, with `[Desktop Entry]`, `Name`, and `Exec` | +| After changing settings | `/var/lib/applaunch/settings` contains the correct key and has not exceeded the configuration entry limit | +| After build | SDL2 or cross build passes, with no unexpected auto-generated diff | +| After running on device | `journalctl` has no missing, skip, segfault, or permission denied messages | +| After external app changes | The app exits normally or returns home via long ESC press | diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216.md" new file mode 100644 index 00000000..1ef26cc1 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216.md" @@ -0,0 +1,27 @@ +# Launcher 工程详细说明 + +本文档集用于说明 `launcher` 仓库,重点覆盖 `projects/APPLaunch` 启动器工程的架构、源码框架、构建、运行、打包、扩展和调试。 + +建议按顺序阅读: + +1. [00-总览与阅读路线](launcher工程详细说明/00-总览与阅读路线.md) +2. [01-工程目录与模块职责](launcher工程详细说明/01-工程目录与模块职责.md) +3. [02-运行框架与启动流程](launcher工程详细说明/02-运行框架与启动流程.md) +4. [03-UI 框架与首页轮播](launcher工程详细说明/03-UI框架与首页轮播.md) +5. [04-应用模型与启动机制](launcher工程详细说明/04-应用模型与启动机制.md) +6. [05-内置页面框架](launcher工程详细说明/05-内置页面框架.md) +7. [06-资源与配置系统](launcher工程详细说明/06-资源与配置系统.md) +8. [07-输入系统与按键映射](launcher工程详细说明/07-输入系统与按键映射.md) +9. [08-构建与编译指南](launcher工程详细说明/08-构建与编译指南.md) +10. [09-打包部署与 systemd](launcher工程详细说明/09-打包部署与systemd.md) +11. [10-扩展开发指南](launcher工程详细说明/10-扩展开发指南.md) +12. [11-调试与故障排查](launcher工程详细说明/11-调试与故障排查.md) +13. [12-常用修改入口速查](launcher工程详细说明/12-常用修改入口速查.md) + +## 快速入口 + +- 只想编译运行:看 [08-构建与编译指南](launcher工程详细说明/08-构建与编译指南.md)。 +- 想理解启动器怎么启动应用:看 [04-应用模型与启动机制](launcher工程详细说明/04-应用模型与启动机制.md)。 +- 想改首页 UI:看 [03-UI 框架与首页轮播](launcher工程详细说明/03-UI框架与首页轮播.md)。 +- 想新增内置应用页面:看 [10-扩展开发指南](launcher工程详细说明/10-扩展开发指南.md)。 +- 设备上黑屏或启动失败:看 [11-调试与故障排查](launcher工程详细说明/11-调试与故障排查.md)。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" new file mode 100644 index 00000000..a25c5620 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" @@ -0,0 +1,87 @@ +# 00 - 总览与阅读路线 + +`launcher` 是 M5CardputerZero 的应用工程集合,核心工程是 `projects/APPLaunch`。APPLaunch 是设备端主启动器:开机后负责初始化 LVGL、显示首页轮播、展示状态栏、启动内置页面或外部应用,并提供设置、终端、音乐、录音、相机、LoRa 等能力。 + +## 1. 文档目标 + +这组文档回答以下问题: + +- 这个仓库每个目录做什么? +- APPLaunch 进程如何启动,主循环在哪里? +- 首页轮播 UI 是如何组织和更新的? +- 内置页面和外部应用如何统一注册到启动器? +- `.desktop` 动态应用如何被扫描和启动? +- 资源、字体、图片、音频路径如何解析? +- SDL2 仿真、设备端原生编译、交叉编译分别怎么做? +- 如何打包成 `.deb` 并通过 systemd 自启动? +- 如何新增一个页面、一个外部应用或一个资源? +- 出现黑屏、资源缺失、外部应用无法返回时如何排查? + +## 2. 工程一句话说明 + +APPLaunch 可以理解为一个基于 LVGL 的小型桌面环境: + +```text +Linux 设备 / SDL2 仿真 + | + v +cp0_lvgl 平台适配层 + | + v +LVGL 9.5 UI 框架 + | + v +APPLaunch 首页、状态栏、轮播、应用管理器 + | + +--> 内置页面 AppPage + +--> PTY 终端应用 UIConsolePage + +--> 外部独立进程 cp0_process_exec_blocking() +``` + +## 3. 推荐阅读顺序 + +如果你是第一次接触工程,建议按下列顺序阅读: + +1. `01-工程目录与模块职责.md`:先建立目录结构认知。 +2. `02-运行框架与启动流程.md`:理解从 `main()` 到首页显示。 +3. `03-UI框架与首页轮播.md`:理解首页 UI 和轮播卡片。 +4. `04-应用模型与启动机制.md`:理解应用列表和启动方式。 +5. `08-构建与编译指南.md`:实际编译运行。 +6. `10-扩展开发指南.md`:新增页面或应用。 + +如果你只想完成某个任务: + +| 任务 | 阅读文件 | +| --- | --- | +| 本机编译运行 SDL2 版本 | `08-构建与编译指南.md` | +| 交叉编译到设备 | `08-构建与编译指南.md` | +| 打包 `.deb` | `09-打包部署与systemd.md` | +| 修改首页卡片布局 | `03-UI框架与首页轮播.md` | +| 添加内置页面 | `10-扩展开发指南.md` | +| 添加 `.desktop` 外部应用 | `04-应用模型与启动机制.md`、`10-扩展开发指南.md` | +| 排查黑屏 | `11-调试与故障排查.md` | +| 找到某功能入口文件 | `12-常用修改入口速查.md` | + +## 4. 关键工程路径 + +| 路径 | 说明 | +| --- | --- | +| `projects/APPLaunch` | 启动器主工程 | +| `projects/APPLaunch/main/src/main.cpp` | APPLaunch 入口和 LVGL 主循环 | +| `projects/APPLaunch/main/ui/Launch.cpp` | 应用列表、启动逻辑、状态栏刷新 | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 UI、轮播、首页按键 | +| `projects/APPLaunch/main/ui/components/page_app` | 内置页面实现 | +| `projects/APPLaunch/APPLaunch` | 打包进运行环境的资源树 | +| `ext_components/cp0_lvgl` | 平台适配层,封装文件、进程、输入、系统接口 | +| `projects/APPLaunch/tools/llm_pack.py` | Debian 包打包脚本 | + +## 5. 名词约定 + +- **APPLaunch**:启动器工程或启动器进程。 +- **首页**:APPLaunch 的主 screen,即带状态栏和应用轮播的界面。 +- **内置页面**:编译在 APPLaunch 进程内的页面类,例如 `UISetupPage`。 +- **终端应用**:通过 `UIConsolePage` + PTY 在 APPLaunch 中运行的命令,例如 `bash`。 +- **外部应用**:独立可执行程序,启动时 APPLaunch 暂停自身 LVGL 渲染,等待外部程序退出。 +- **资源树**:`APPLaunch/share/images`、`APPLaunch/share/audio`、`APPLaunch/share/font` 等运行时文件。 +- **设备端**:M5CardputerZero 的 AArch64 Linux 环境。 +- **SDL2 模式**:在开发机上通过 SDL2 窗口模拟运行。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" new file mode 100644 index 00000000..545eb8fb --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" @@ -0,0 +1,250 @@ +# 01 - 工程目录与模块职责 + +本章说明仓库整体结构和 APPLaunch 工程内部结构。 + +## 1. 仓库总结构 + +```text +launcher/ +├── SDK/ +├── ext_components/ +├── projects/ +├── doc/ +├── docs/ +├── README.md +└── README_ZH.md +``` + +### 1.1 `SDK/` + +`SDK` 是 `M5Stack_Linux_Libs`,为工程提供: + +- SCons/Kconfig 构建框架。 +- LVGL 组件。 +- 设备驱动、工具函数、示例代码。 +- 编译脚本和组件注册机制。 + +APPLaunch 的 `SConstruct` 会设置: + +```python +os.environ["SDK_PATH"] = str(sdk_path) +``` + +然后调用: + +```python +env = SConscript( + str(sdk_path / "tools" / "scons" / "project.py"), + variant_dir=os.getcwd(), + duplicate=0, +) +``` + +### 1.2 `ext_components/` + +`ext_components` 是仓库内扩展组件目录,APPLaunch 通过 `EXT_COMPONENTS_PATH` 引入。 + +```text +ext_components/ +├── cp0_lvgl/ +├── Miniaudio/ +└── Sigslot/ +``` + +| 组件 | 作用 | +| --- | --- | +| `cp0_lvgl` | CardputerZero 平台适配,封装 LVGL 初始化、文件路径、输入、进程、PTY、系统能力 | +| `Miniaudio` | 音频播放/录音相关依赖 | +| `Sigslot` | 信号槽机制 | + +### 1.3 `projects/` + +```text +projects/ +├── APPLaunch/ +├── AppStore/ +├── Calculator/ +├── CardputerZero-Emulator/ +├── HelloWorld/ +└── UserDemo/ +``` + +| 工程 | 说明 | +| --- | --- | +| `APPLaunch` | 主启动器,本文档重点 | +| `AppStore` | 应用商店,可被 APPLaunch 作为外部应用启动 | +| `Calculator` | 计算器应用,可被 APPLaunch 启动 | +| `CardputerZero-Emulator` | 设备仿真器 | +| `HelloWorld` | 最小示例工程,用于学习构建流程 | +| `UserDemo` | 用户演示工程 | + +### 1.4 `doc/` 和 `docs/` + +- `doc/`:历史文档、打包指南、辅助脚本,例如 `APPLaunch-App-打包指南.md`、`store_cache_sync.py`。 +- `docs/`:面向开发者的说明文档,本组文档放在这里。 + +## 2. APPLaunch 顶层结构 + +```text +projects/APPLaunch/ +├── APPLaunch/ +├── main/ +├── tools/ +├── docs/ +├── SConstruct +├── config_defaults.mk +├── linux_x86_sdl2_config_defaults.mk +├── linux_x86_cross_cp0_config_defaults.mk +├── mac_cross_cp0_config_defaults.mk +├── darwin_config_defaults.mk +└── setup.ini +``` + +### 2.1 顶层构建文件 + +| 文件 | 说明 | +| --- | --- | +| `SConstruct` | 工程入口,决定默认配置、SDK 路径、交叉编译 sysroot、调用 SDK 构建系统 | +| `config_defaults.mk` | 设备端默认配置,启用 Linux framebuffer / evdev | +| `linux_x86_sdl2_config_defaults.mk` | Linux x86 SDL2 仿真配置 | +| `linux_x86_cross_cp0_config_defaults.mk` | Linux x86 交叉编译到 AArch64 配置 | +| `mac_cross_cp0_config_defaults.mk` | macOS 交叉编译到 AArch64 配置 | +| `darwin_config_defaults.mk` | macOS SDL / Darwin 相关配置 | + +### 2.2 `APPLaunch/` 运行时资源树 + +```text +projects/APPLaunch/APPLaunch/ +├── applications/ +│ └── vim.desktop.temple +├── lib/ +│ └── nihao.so +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +这个目录在构建/打包时被复制到运行目录。设备端安装后对应: + +```text +/usr/share/APPLaunch/ +``` + +资源树职责: + +- `applications/`:放置 `.desktop` 外部应用描述文件。 +- `share/images/`:应用图标、首页轮播、状态栏、页面图片。 +- `share/audio/`:启动音、按键音、切换音。 +- `share/font/`:TTF 字体。 +- `lib/`:随包库文件。 + +### 2.3 `main/` 主源码目录 + +```text +projects/APPLaunch/main/ +├── Kconfig +├── SConstruct +├── include/ +├── src/ +└── ui/ +``` + +| 路径 | 说明 | +| --- | --- | +| `Kconfig` | 组件配置入口 | +| `SConstruct` | 注册 APPLaunch 编译目标和依赖 | +| `include/` | APPLaunch 私有头文件、兼容头 | +| `src/main.cpp` | 进程入口,LVGL 初始化和主循环 | +| `ui/` | 所有 UI 页面、首页、动画、Loading 等实现 | + +### 2.4 `main/ui/` UI 目录 + +```text +main/ui/ +├── ui.c +├── ui.cpp +├── ui.h +├── ui_obj.h +├── Launch.cpp +├── Launch.h +├── UILaunchPage.cpp +├── UILaunchPage.h +├── ui_loading.cpp +├── ui_loading.h +├── ui_global_hint.cpp +├── ui_global_hint.h +├── zero_lvgl_os.cpp +├── zero_lvgl_os.h +├── Animation/ +└── components/ +``` + +| 文件/目录 | 作用 | +| --- | --- | +| `ui.c` / `ui.cpp` / `ui.h` | UI 初始化、全局对象、C/C++ 桥接 | +| `Launch.cpp` | 应用管理器,实现应用列表、启动、状态栏刷新、目录监听 | +| `UILaunchPage.cpp` | 首页 UI 创建、轮播槽位、按键处理、启动动画 | +| `ui_loading.cpp` | Loading 遮罩 | +| `ui_global_hint.cpp` | 全局提示 | +| `zero_lvgl_os.cpp` | LVGL OS/线程相关辅助 | +| `Animation/` | 首页轮播动画实现 | +| `components/` | 页面基类、组件、自定义页面 | + +### 2.5 `components/page_app/` 内置页面目录 + +```text +main/ui/components/page_app/ +├── ui_app_camera.hpp +├── ui_app_compass.hpp +├── ui_app_console.hpp +├── ui_app_file.hpp +├── ui_app_game.hpp +├── ui_app_lora.hpp +├── ui_app_mesh.hpp +├── ui_app_music.hpp +├── ui_app_rec.hpp +├── ui_app_setup.hpp +├── ui_app_ssh.hpp +├── ui_app_tank_battle.hpp +└── ui_app_IpPanel.hpp +``` + +这些页面通常以 header-only 方式实现,便于被 `generate_page_app_includes.py` 自动包含。 + +## 3. 模块依赖关系 + +简化依赖图: + +```text +main.cpp + ├── ui/ui.h + ├── cp0_lvgl_app.h + ├── cp0_lvgl_file.hpp + └── hal_lvgl_bsp.h + +ui_init() + ├── UILaunchPage + ├── Launch + ├── ui_loading + └── page_app/* + +LaunchImpl + ├── UILaunchPage::panel()/label() + ├── page_v + ├── cp0_file_path() + ├── cp0_process_* + ├── cp0_dir_watch_* + ├── cp0_wifi_* + └── cp0_battery_* +``` + +## 4. 代码风格特征 + +APPLaunch 当前代码有几个明显特征: + +- C 和 C++ 混合:LVGL 自动生成/兼容代码常是 C,业务页面多为 C++。 +- 通过 `extern "C"` 暴露 C/C++ 桥接函数,例如 `cpp_app_launch()`。 +- 页面类通常直接构造 LVGL 对象,不使用额外 UI 框架。 +- 硬件能力优先通过 `cp0_lvgl` 的统一接口封装。 +- 资源访问优先使用 `cp0_file_path()`,避免设备端和 SDL 端路径差异。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" new file mode 100644 index 00000000..c7f61275 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" @@ -0,0 +1,358 @@ +# 02 - 运行框架与启动流程 + +本章说明 APPLaunch 从进程入口到首页首帧显示的完整路径,重点参考 `projects/APPLaunch/main/src/main.cpp`、`projects/APPLaunch/main/ui/ui.cpp`、`projects/APPLaunch/main/ui/zero_lvgl_os.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`。 + +## 1. 运行框架概览 + +APPLaunch 是一个单进程 LVGL 应用。主线程完成平台初始化、UI 对象创建、首帧刷新,然后进入 `lv_timer_handler()` 驱动的循环。 + +```text +APPLaunch 进程 +├── main.cpp +│ ├── lv_init() +│ ├── cp0_lvgl_init() +│ ├── lv_event_register_id() +│ ├── ui_init() +│ └── while (1) +│ ├── APPLaunch_lock() +│ ├── lv_timer_handler() +│ └── usleep(5000) +└── ui_init() + └── zero_lvgl_os() + ├── creat_display() + ├── 创建 Launch / UILaunchPage 绑定对象 + └── create_launcher_home() +``` + +核心特点: + +- LVGL 初始化和平台适配初始化只在 `main()` 中执行一次。 +- 首页 UI 的创建由 `zero_lvgl_os` 驱动,实际对象在 `UILaunchPage::create_screen()` 中创建。 +- `Launch` / `LaunchImpl` 负责应用列表、启动方式、状态栏刷新、动态应用目录监听。 +- 首页首帧在 `ui_init()` 后立即通过 `lv_obj_invalidate()` + `lv_refr_now(NULL)` 强制刷新,避免启动后黑屏等待下一次自然刷新。 + +## 2. 入口文件与关键源码路径 + +| 路径 | 作用 | +| --- | --- | +| `projects/APPLaunch/main/src/main.cpp` | 进程入口、LVGL 主循环、外部应用运行锁检测 | +| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`,创建全局 `zero_lvgl_os home` | +| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | 设置 LVGL theme、创建首页、创建 Launch 绑定对象 | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 screen、启动 GIF、首页加载、输入 group | +| `projects/APPLaunch/main/ui/Launch.cpp` | 应用管理器、外部/终端/内置页面启动、状态栏 timer | +| `ext_components/cp0_lvgl` | `cp0_lvgl_init()`、文件路径、输入、进程、系统能力封装 | + +## 3. `main()` 启动流程 + +`main()` 的框架代码如下: + +```cpp +int main(void) +{ + static const std::string default_lock_file = cp0_file_path("lock_file"); + lock_file = default_lock_file.c_str(); + + lv_init(); + cp0_lvgl_init(); + + if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + + ui_init(); + + lv_obj_invalidate(lv_scr_act()); + lv_refr_now(NULL); + + while (1) { + APPLaunch_lock(); + lv_timer_handler(); + usleep(5000); + } +} +``` + +### 3.1 初始化阶段 + +1. `cp0_file_path("lock_file")` 解析运行时锁文件路径。 +2. `lv_init()` 初始化 LVGL 核心对象、内存、timer、display/indev 抽象。 +3. `cp0_lvgl_init()` 初始化平台层:显示、输入、framebuffer/SDL、系统信号等能力。 +4. `lv_event_register_id()` 注册自定义键盘事件 `LV_EVENT_KEYBOARD`。 +5. `ui_init()` 进入 APPLaunch 自身 UI 构造流程。 + +### 3.2 首帧刷新 + +`ui_init()` 返回后,代码立即执行: + +```cpp +lv_obj_invalidate(lv_scr_act()); +lv_refr_now(NULL); +``` + +这一步的目的不是普通刷新,而是强制把当前 active screen 的内容刷到 framebuffer/SDL 窗口。首页对象刚创建完时,如果只依赖后续 `lv_timer_handler()`,用户可能短暂看到黑屏;强刷首帧可以让启动体验更确定。 + +### 3.3 主循环 + +主循环节奏是 5ms: + +```text +每轮循环 + -> APPLaunch_lock() + -> lv_timer_handler() + -> sleep 5ms +``` + +- `APPLaunch_lock()` 用于检测是否有外部应用占用前台。 +- `lv_timer_handler()` 驱动 LVGL timer、动画、输入事件、重绘。 +- `usleep(5000)` 控制 CPU 占用和刷新节奏。 + +## 4. `ui_init()` 到首页对象创建 + +`ui_init()` 位于 `projects/APPLaunch/main/ui/ui.cpp`: + +```cpp +std::unique_ptr home; + +void ui_init(void) +{ + home = std::make_unique(); +} +``` + +`zero_lvgl_os` 构造函数继续执行: + +```cpp +zero_lvgl_os::zero_lvgl_os() +{ + creat_display(); + + launch_ = std::make_shared(); + launch_page_ = std::make_shared(launch_); + launch_->set_launch_page(launch_page_); + + create_launcher_home(); +} +``` + +需要注意这里的顺序: + +1. `creat_display()` 先创建字体管理器并设置 LVGL theme。 +2. 构造 `Launch` 与 `UILaunchPage`,并通过 `Launch::set_launch_page()` 建立双向协作关系。 +3. `create_launcher_home()` 创建首页 screen、调用 `Launch::bind_ui()` 建立应用列表、初始化 input group,并显示首页或启动 GIF。 + +## 5. Display / Theme 初始化 + +`zero_lvgl_os::creat_display()` 的核心代码: + +```cpp +void zero_lvgl_os::creat_display() +{ + fonts_ = std::make_shared(); + + dispp_ = lv_disp_get_default(); + theme_ = lv_theme_default_init( + dispp_, + lv_palette_main(LV_PALETTE_BLUE), + lv_palette_main(LV_PALETTE_RED), + false, + LV_FONT_DEFAULT); + lv_disp_set_theme(dispp_, theme_); +} +``` + +说明: + +- `LauncherFonts` 是首页和页面共享的 FreeType 字体缓存,入口函数为 `launcher_fonts()`。 +- `lv_disp_get_default()` 依赖 `cp0_lvgl_init()` 已经注册显示设备。 +- theme 只是基础主题,首页多数控件仍在 `UILaunchPage.cpp` 中手工设置尺寸、颜色、背景图和字体。 + +## 6. 首页创建与显示流程 + +`zero_lvgl_os::create_launcher_home()` 是首页显示的主要入口: + +```cpp +void zero_lvgl_os::create_launcher_home() +{ + LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); + + launch_page_->create_screen(); + launch_->bind_ui(); + launch_page_->init_input_group(); + +#ifndef APPLAUNCH_STARTUP_ANIMATION + launch_page_->load_home_screen(); +#else +#ifdef HAL_PLATFORM_SDL + launch_page_->load_home_screen(); +#else + const char *gif_path = cp0_file_path_c("logo_output.gif"); + FILE *gif_file = fopen(gif_path, "r"); + if (gif_file) { + fclose(gif_file); + launch_page_->start_startup_gif(); + } else { + launch_page_->load_home_screen(); + } +#endif +#endif +} +``` + +### 6.1 首页 screen 创建 + +`UILaunchPage::create_screen()` 只创建一次: + +```cpp +void UILaunchPage::create_screen() +{ + if (ui_Screen1) + return; + + ui_Screen1 = lv_obj_create(NULL); + lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); + + create_top(ui_Screen1); + create_app_container(ui_Screen1); +} +``` + +它会创建两个区域: + +- `create_top()`:左上角 logo、WiFi、时间、电量状态栏。 +- `create_app_container()`:首页轮播容器、5 个卡片、5 个标题、5 个页点、左右箭头。 + +### 6.2 输入 group 绑定 + +首页的输入 group 在 `UILaunchPage::init_input_group()` 中创建: + +```cpp +home_input_group = lv_group_create(); +lv_group_add_obj(home_input_group, ui_Screen1); +lv_indev_set_group(indev, home_input_group); +``` + +这使键盘事件可以投递到 `ui_Screen1`,再由 `main_key_switch()` 处理左右切换和 Enter 启动。 + +### 6.3 启动 GIF 与首页显示 + +当启用 `APPLAUNCH_STARTUP_ANIMATION` 且不在 SDL 平台时: + +```text +检查 cp0_file_path_c("logo_output.gif") + -> 文件存在:UILaunchPage::start_startup_gif() + -> 文件不存在:UILaunchPage::load_home_screen() +``` + +`start_startup_gif()` 创建一个独立的 GIF screen: + +```cpp +startup_gif = lv_gif_create(NULL); +lv_gif_set_src(startup_gif, gif_path); +lv_obj_center(startup_gif); +lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); +lv_disp_load_scr(startup_gif); +``` + +GIF 播放完成时收到 `LV_EVENT_READY`,回调 `ui_event_logo_over()` 会暂停 GIF 并加载首页: + +```cpp +if (event_code == LV_EVENT_READY && !done) { + if (startup_gif) lv_gif_pause(startup_gif); + UILaunchPage::load_home_screen(); +} +``` + +`load_home_screen()` 的职责: + +```cpp +ui____initial_actions0 = lv_obj_create(NULL); +lv_disp_load_scr(ui_Screen1); +UILaunchPage::bind_home_input_group(); +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +## 7. 启动时序文本 + +```text +main() + -> cp0_file_path("lock_file") + -> lv_init() + -> cp0_lvgl_init() + -> register LV_EVENT_KEYBOARD + -> ui_init() + -> new zero_lvgl_os + -> creat_display() + -> new LauncherFonts + -> lv_disp_get_default() + -> lv_theme_default_init() + -> new Launch + -> new UILaunchPage(Launch) + -> Launch::set_launch_page() + -> create_launcher_home() + -> register LV_EVENT_GET_COMP_CHILD + -> launch_page_->create_screen() + -> create_top() + -> create_app_container() + -> launch_->bind_ui() + -> new LaunchImpl + -> 注册固定/动态应用并写入首页槽位 + -> 创建状态栏和应用目录监听 timer + -> launch_page_->init_input_group() + -> load_home_screen() 或 start_startup_gif() + -> lv_obj_invalidate(lv_scr_act()) + -> lv_refr_now(NULL) + -> while forever + -> APPLaunch_lock() + -> lv_timer_handler() + -> usleep(5000) +``` + +## 8. 外部应用运行锁 `APPLaunch_lock()` + +`APPLaunch_lock()` 负责协调 APPLaunch 与外部独立进程的前台渲染关系。 + +```cpp +void APPLaunch_lock() +{ + int holder_pid = 0; + cp0_process_check_lock(lock_file, &holder_pid); + + if (holder_pid == 0) { + LVGL_RUN_FLAGE = 1; + lv_obj_invalidate(lv_scr_act()); + } else { + if (LVGL_HOME_KEY_FLAG) { + // HOME 按住 5 秒后 kill 外部应用 + cp0_process_kill(holder_pid, 3000); + } + LVGL_RUN_FLAGE = 0; + } +} +``` + +实际代码有几个状态变量: + +- `lvgl_lock`:避免每轮都重复恢复 LVGL 刷新,只在锁释放后做一次 `invalidate`。 +- `home_back_status` / `start_time`:统计 HOME 键持续按下时间。 +- `holder_pid`:当前持有 lock 文件的外部进程 PID。 + +逻辑含义: + +```text +没有外部应用持锁 + -> APPLaunch 恢复 LVGL_RUN_FLAGE=1 + -> 如刚从锁定恢复,重绘当前 screen + +有外部应用持锁 + -> APPLaunch 设置 LVGL_RUN_FLAGE=0,暂停自身渲染 + -> 如果 HOME 键持续按住 >= 5 秒,尝试 kill 外部应用 +``` + +## 9. 注意事项 + +- `ui_init()` 内部已经创建并可能加载首页 screen,`main()` 后面的 `lv_refr_now(NULL)` 是首帧保障,不要轻易删除。 +- `cp0_lvgl_init()` 必须在 `ui_init()` 之前执行,否则 `lv_disp_get_default()`、输入设备、路径与系统接口可能未就绪。 +- SDL 平台默认跳过启动 GIF,设备端才会检查并播放 `logo_output.gif`。 +- 首页输入必须通过 `UILaunchPage::bind_home_input_group()` 重新绑定;内置页面或终端页面返回首页时也需要恢复这个 group。 +- 外部独立应用运行时会让 `LVGL_RUN_FLAGE=0`,不要在这段时间假设 APPLaunch 仍会持续刷新 UI。 +- `APPLaunch_lock()` 依赖 `cp0_process_exec_blocking()`/锁文件协作;如果外部应用异常退出但锁未释放,首页可能表现为不刷新,需要从 lock 文件和持有 PID 排查。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" new file mode 100644 index 00000000..a3518dc5 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" @@ -0,0 +1,464 @@ +# 03 - UI框架与首页轮播 + +本章说明 APPLaunch 首页 UI 的组织方式、轮播卡片的数据流和按键事件。重点参考 `projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.h`、`projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp`、`projects/APPLaunch/main/ui/Launch.cpp`。 + +## 1. UI 框架概览 + +APPLaunch 的 UI 不是传统桌面框架,而是直接基于 LVGL 对象树构建: + +```text +ui_Screen1 +├── 顶部状态栏 create_top() +│ ├── ZERO / logo +│ ├── WiFi 信号条 +│ ├── 时间面板 +│ └── 电量面板 +└── ui_APP_Container create_app_container() + ├── 5 个轮播卡片 panel + ├── 5 个标题 label + ├── 左右箭头按钮 + └── 5 个页点 dot +``` + +首页对象的全局入口来自 `ui_obj.h` 中的 `ui_Screen1`、`ui_APP_Container`、`ui_timeLabel`、`ui_Bar1` 等对象声明;实际创建和样式设置集中在 `UILaunchPage.cpp`。 + +## 2. 关键源码路径 + +| 路径 | 说明 | +| --- | --- | +| `projects/APPLaunch/main/ui/UILaunchPage.h` | 首页类定义、轮播元素枚举、`carousel_elements` 数组 | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 screen 创建、轮播切换、键盘事件、启动 GIF、字体缓存 | +| `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | 轮播左右切换动画 | +| `projects/APPLaunch/main/ui/Launch.cpp` | 切换后填充新卡片内容、启动当前应用、刷新状态栏 | +| `projects/APPLaunch/main/ui/ui.h` | 首页布局常量,例如 `LABEL_Y_CENTER`、`BORDER_COLOR_CENTER` | +| `projects/APPLaunch/main/ui/ui_obj.h` | 全局 LVGL 对象声明 | + +## 3. `UILaunchPage` 的职责 + +`UILaunchPage` 是首页 UI 门面类: + +```cpp +class UILaunchPage : public home_base +{ +public: + static void load_home_screen(); + static void start_startup_gif(); + static void create_screen(); + + static void init_input_group(); + static void bind_home_input_group(); + static lv_group_t *home_input_group(); + static lv_obj_t *panel(size_t slot); + static lv_obj_t *label(size_t slot); + + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_selected_app(); + + static std::array carousel_elements; +}; +``` + +它有两类职责: + +- 静态职责:创建 screen、维护首页输入 group、提供 `panel()` / `label()` 访问器。 +- 实例职责:持有 `Launch` 指针,把轮播更新和应用启动转发给 `LaunchImpl`。 + +当前代码通过 `active_launch_page` 保存活动首页实例,供静态事件回调调用: + +```cpp +namespace { +UILaunchPage *active_launch_page = nullptr; +} + +UILaunchPage::UILaunchPage(std::shared_ptr launch) + : home_base(), launch_(std::move(launch)) +{ + active_launch_page = this; +} +``` + +## 4. 轮播元素数组 + +首页轮播的所有核心对象都存放在一个固定数组里: + +```cpp +std::array + UILaunchPage::carousel_elements = {}; +``` + +枚举定义在 `UILaunchPage.h`: + +```cpp +enum LauncherCarouselElement : size_t { + kCardFarLeft = 0, + kCardLeft, + kCardCenter, + kCardRight, + kCardFarRight, + kTitleFarLeft, + kTitleLeft, + kTitleCenter, + kTitleRight, + kTitleFarRight, + kPageDot0, + kPageDot1, + kPageDot2, + kPageDot3, + kPageDot4, + kLauncherCarouselElementCount, +}; +``` + +数组分成三段: + +| 下标范围 | 对象 | 说明 | +| --- | --- | --- | +| `0..4` | 卡片 panel | 远左、左、中、右、远右 | +| `5..9` | 标题 label | 与卡片槽位对应 | +| `10..14` | 页点 dot | 底部 5 个状态点 | + +辅助访问器: + +```cpp +lv_obj_t *UILaunchPage::panel(size_t slot) +{ + return carousel_elements[kCardFarLeft + slot]; +} + +lv_obj_t *UILaunchPage::label(size_t slot) +{ + return carousel_elements[kTitleFarLeft + slot]; +} +``` + +因此 `panel(2)` 是中心卡片,`label(2)` 是中心标题。 + +## 5. 标准槽位布局 + +`UILaunchPage.cpp` 中用 `CarouselSlot` 描述轮播静态布局: + +```cpp +struct CarouselSlot { + lv_coord_t x; + lv_coord_t y; + lv_coord_t width; + lv_coord_t height; + bool hidden; +}; + +static const CarouselSlot CAROUSEL_SLOTS[] = { + {-177, 4, 61, 61, true}, + {-99, -6, 80, 80, false}, + {0, -16, 100, 100, false}, + {99, -6, 80, 80, false}, + {177, 4, 61, 61, true}, + {-177, LABEL_Y_SIDE, 0, 0, true}, + {-99, LABEL_Y_SIDE, 0, 0, false}, + {0, LABEL_Y_CENTER, 0, 0, false}, + {99, LABEL_Y_SIDE, 0, 0, false}, + {177, LABEL_Y_SIDE, 0, 0, true}, +}; +``` + +槽位语义: + +```text +卡片: far-left(hidden) left center right far-right(hidden) +标题: far-left(hidden) left center right far-right(hidden) +``` + +隐藏的远端槽位是动画缓冲区:切换前先把即将进入的卡片放到远端,动画结束后再把数组顺序旋转。 + +## 6. 首页创建流程 + +`create_screen()` 创建根 screen: + +```cpp +ui_Screen1 = lv_obj_create(NULL); +lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); +lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); + +create_top(ui_Screen1); +create_app_container(ui_Screen1); +``` + +### 6.1 顶部状态栏 + +`create_top()` 包含: + +- 左上角 `ZERO` 文本或 `launcher_brand_logo.png`。 +- `ui_wifiPanel` 与 `ui_wifiBar1..4`,默认隐藏,状态刷新时按信号强度显示。 +- `ui_Panel1` 时间背景图 `status_time_background.png` 与 `ui_timeLabel`。 +- `ui_batteryPanel` 电池背景图 `status_battery_background.png`、`ui_Bar1`、`ui_powerLabel`。 + +状态栏数据刷新不在 `UILaunchPage`,而在 `LaunchImpl::update_home_status_bar()`: + +```cpp +cp0_wifi_status_t wifi = cp0_wifi_get_status(); +cp0_time_str(time_buf, sizeof(time_buf)); +cp0_battery_info_t bat = cp0_battery_read(); +``` + +`LaunchImpl` 构造时创建 5 秒 timer: + +```cpp +status_timer = lv_timer_create(home_status_timer_cb, 5000, this); +``` + +### 6.2 轮播容器 + +`create_app_container()` 创建 `ui_APP_Container`: + +```cpp +ui_APP_Container = lv_obj_create(parent); +lv_obj_remove_style_all(ui_APP_Container); +lv_obj_set_width(ui_APP_Container, 320); +lv_obj_set_height(ui_APP_Container, 150); +lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); +``` + +随后依次创建: + +- 5 个页点:`kPageDot0..kPageDot4`,中心页点默认 10x10、黄色。 +- 5 个标题:中心默认 `CLI`,左右默认 `STORE` / `GAME`,远端隐藏。 +- 5 个卡片:中心 100x100,左右 80x80,远端 61x61 且隐藏。 +- 左右按钮:背景图 `carousel_left_arrow.png` / `carousel_right_arrow.png`。 + +默认标题只是 UI 初始占位,真正内容会由 `LaunchImpl` 初始化应用列表后写入。 + +## 7. 轮播切换流程 + +轮播切换分为 UI 动画和应用数据更新两部分。 + +### 7.1 向右切换 `switch_right()` + +`switch_right()` 的含义是卡片整体向右移动,当前选择变为列表中的前一个应用: + +```cpp +void switch_right(lv_event_t *e) +{ + if (is_animating) { + pending_switch = &switch_right; + return; + } + + is_animating = true; + lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); + launcher_home_animation::animate_right(carousel_elements.data(), snap_all_panels); + + snap_panel_to_slot(carousel_elements[4], 0); + snap_label_to_slot(carousel_elements[9], 5); + + active_launch_page->update_right_slot(carousel_elements[4], carousel_elements[9]); + rotate_carousel_right(0, 4); + rotate_carousel_right(5, 9); +} +``` + +关键步骤: + +1. 如果正在动画中,把本次请求放入 `pending_switch`,等待当前动画结束后执行。 +2. 显示远左隐藏卡片,作为动画进入画面的一侧。 +3. 调用 `launcher_home_animation::animate_right()` 启动动画。 +4. 把远右对象预先吸附到远左槽位,填充即将进入的新应用内容。 +5. 旋转 `carousel_elements[0..4]` 和 `[5..9]`,让数组顺序匹配新的视觉顺序。 +6. 更新页点高亮。 + +### 7.2 向左切换 `switch_left()` + +`switch_left()` 的含义是卡片整体向左移动,当前选择变为列表中的后一个应用: + +```cpp +void switch_left(lv_event_t *e) +{ + if (is_animating) { + pending_switch = &switch_left; + return; + } + + is_animating = true; + lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); + launcher_home_animation::animate_left(carousel_elements.data(), snap_all_panels); + + snap_panel_to_slot(carousel_elements[0], 4); + snap_label_to_slot(carousel_elements[5], 9); + + active_launch_page->update_left_slot(carousel_elements[0], carousel_elements[5]); + rotate_carousel_left(0, 4); + rotate_carousel_left(5, 9); +} +``` + +它和 `switch_right()` 对称:远右进入画面,远左对象被移到远右槽位并填充新内容。 + +## 8. 动画结束后的归位 + +动画结束回调是 `snap_all_panels()`: + +```cpp +static void snap_all_panels() +{ + for (int i = 0; i < 5; i++) + snap_panel_to_slot(carousel_elements[i], i); + + for (int i = 5; i < 10; i++) + snap_label_to_slot(carousel_elements[i], i); + + is_animating = false; + + if (pending_switch) { + switch_cb_t cb = pending_switch; + pending_switch = NULL; + cb(NULL); + } +} +``` + +它解决两个问题: + +- 动画可能有插值误差,结束后强制把对象吸附到标准槽位。 +- 如果动画期间用户连续按方向键,只保留一个待执行切换,动画完成后继续执行。 + +## 9. 应用数据如何写入轮播 + +`LaunchImpl` 维护 `current_app` 和 `app_list`。切换时,`UILaunchPage` 只传入要被复用的 panel/label;具体要显示哪个应用由 `LaunchImpl` 计算。 + +向左切换后填充新右端: + +```cpp +void update_left_slot(lv_obj_t *panel, lv_obj_t *label) +{ + current_app = current_app == app_list.size() - 1 ? 0 : current_app + 1; + int next_app = current_app; + next_app = next_app == app_list.size() - 1 ? 0 : next_app + 1; + next_app = next_app == app_list.size() - 1 ? 0 : next_app + 1; + auto it = std::next(app_list.begin(), next_app); + lv_label_set_text(label, it->Name.c_str()); + panel_set_icon(panel, it->Icon.c_str()); +} +``` + +向右切换后填充新左端: + +```cpp +void update_right_slot(lv_obj_t *panel, lv_obj_t *label) +{ + current_app = current_app == 0 ? app_list.size() - 1 : current_app - 1; + int next_app = current_app; + next_app = next_app == 0 ? app_list.size() - 1 : next_app - 1; + next_app = next_app == 0 ? app_list.size() - 1 : next_app - 1; + auto it = std::next(app_list.begin(), next_app); + lv_label_set_text(label, it->Name.c_str()); + panel_set_icon(panel, it->Icon.c_str()); +} +``` + +图示: + +```text +视觉槽位: [far-left] [left] [center] [right] [far-right] +应用索引: current-2 current-1 current current+1 current+2 + +按右键: + current -> current-1 + 新 far-left 需要显示 current-2 + +按左键: + current -> current+1 + 新 far-right 需要显示 current+2 +``` + +## 10. 输入事件与音效 + +首页键盘事件绑定在 `create_app_container()` 末尾: + +```cpp +lv_obj_add_event_cb(ui_Screen1, main_key_switch, + (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); +``` + +`main_key_switch()` 处理逻辑: + +```text +按下 LEFT/Z + -> audio_play_switch() + -> switch_right() + +按下 RIGHT/C + -> audio_play_switch() + -> switch_left() + +释放 ENTER + -> audio_play_enter() + -> app_launch() + +释放 F12 + -> 开关绿色测试背景 lvping_lock +``` + +代码中先通过 `fzxc_to_arrow()` 把 `F/X/Z/C` 映射成方向键: + +```cpp +KEY_F -> KEY_UP +KEY_X -> KEY_DOWN +KEY_Z -> KEY_LEFT +KEY_C -> KEY_RIGHT +``` + +音效入口: + +```cpp +cp0_signal_system_play_asset("switch.wav"); +cp0_signal_system_play_asset("enter.wav"); +``` + +启动音在 `load_home_screen()` 中播放: + +```cpp +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +## 11. 首页时序文本 + +```text +UILaunchPage::create_screen() + -> create_top() + -> 创建 logo / WiFi / 时间 / 电量对象 + -> create_app_container() + -> 创建 page dots + -> 创建 labels + -> 创建 cards + -> 创建 arrows + -> 绑定 click 和 keyboard callbacks + +用户按 RIGHT + -> main_key_switch() + -> audio_play_switch() + -> switch_left() + -> is_animating=true + -> animate_left() + -> update_left_slot() + -> rotate cards / labels + -> 更新 page dot + -> snap_all_panels() + -> 吸附对象到标准槽位 + -> is_animating=false + -> 如有 pending_switch,继续执行 + +用户按 ENTER + -> main_key_switch() + -> audio_play_enter() + -> app_launch() + -> UILaunchPage::launch_selected_app() + -> Launch::launch_app() +``` + +## 12. 注意事项 + +- `carousel_elements` 保存的是 LVGL 对象指针;轮播切换通过旋转指针数组实现,不是销毁重建对象。 +- `switch_left()` / `switch_right()` 的命名描述动画方向,不一定等同于用户按键方向;当前 `KEY_LEFT` 调用 `switch_right()`,`KEY_RIGHT` 调用 `switch_left()`。 +- 动画期间只记录一个 `pending_switch`,连续快速按键不会无限排队。 +- 首页卡片点击事件都绑定到 `app_launch()`,但正常交互主要通过中心选择 + Enter 启动;如果开放鼠标/触摸,需要确认点击非中心卡片时是否符合预期。 +- 状态栏对象由 `UILaunchPage` 创建,但刷新 timer 在 `LaunchImpl` 构造时创建;如果只创建首页但没有执行 `Launch::bind_ui()`,应用列表和状态栏刷新不会启动。 +- 新增或调整轮播槽位时,要同步修改 `CAROUSEL_SLOTS`、`create_app_container()` 初始位置、动画文件中的槽位定义,避免动画结束跳变。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" new file mode 100644 index 00000000..b9e4bc9a --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" @@ -0,0 +1,497 @@ +# 04 - 应用模型与启动机制 + +本章说明 APPLaunch 如何把内置页面、终端命令、外部独立程序统一成一个应用列表,以及用户按 Enter 后如何启动应用。重点参考 `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/Launch.h`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/components/page_app/*`。 + +## 1. 应用模型概览 + +APPLaunch 将所有首页入口统一抽象为 `app`: + +```text +app +├── Name 显示标题 +├── Icon 图标路径 +├── Exec 外部命令,内置页面可为空 +└── launch(LaunchImpl*) 启动动作 +``` + +统一后,首页轮播不需要关心应用类型,只显示 `Name` 和 `Icon`;按 Enter 时只调用当前 `app.launch()`。 + +```text +首页中心卡片 + -> Launch::launch_app() + -> LaunchImpl::launch_app() + -> app.launch(this) + ├── 内置页面:new PageT + lv_disp_load_scr() + ├── 终端应用:UIConsolePage + PTY exec() + └── 外部应用:cp0_process_exec_blocking() +``` + +## 2. 关键源码路径 + +| 路径 | 说明 | +| --- | --- | +| `projects/APPLaunch/main/ui/Launch.h` | `Launch` 对外门面,隐藏 `LaunchImpl` | +| `projects/APPLaunch/main/ui/Launch.cpp` | `app`、`LaunchImpl`、应用列表、启动逻辑、`.desktop` 扫描 | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Enter / click 事件转发到 `Launch::launch_app()` | +| `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | 终端页面 `UIConsolePage` | +| `projects/APPLaunch/main/ui/components/page_app/*.hpp` | 各内置页面,例如设置、音乐、文件、相机、LoRa | +| `projects/APPLaunch/APPLaunch/applications/` | 运行时 `.desktop` 应用描述目录 | +| `ext_components/cp0_lvgl` | 进程启动、PTY、目录监听、路径解析等底层能力 | + +## 3. `Launch` 与 `LaunchImpl` 分层 + +`Launch.h` 对外暴露很少: + +```cpp +class Launch +{ +public: + void bind_ui(); + void set_launch_page(std::shared_ptr launch_page); + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_app(); + +private: + std::unique_ptr impl_; + std::shared_ptr launch_page_; +}; +``` + +真正逻辑在 `LaunchImpl`: + +```cpp +class LaunchImpl +{ +private: + int current_app = 2; + cp0_watcher_t dir_watcher = NULL; + lv_timer_t *watch_timer = nullptr; + lv_timer_t *status_timer = nullptr; + int fixed_count; + +public: + std::list app_list; + std::shared_ptr app_Page; + std::shared_ptr home_Page; +}; +``` + +字段含义: + +| 字段 | 说明 | +| --- | --- | +| `current_app` | 当前中心卡片对应的应用索引,默认 `2`,即初始中心为 CLI | +| `app_list` | 所有固定应用和动态 `.desktop` 应用 | +| `fixed_count` | 固定应用数量,动态重载时保留此前元素 | +| `app_Page` | 当前内置页面或终端页面的生命周期持有者 | +| `dir_watcher` / `watch_timer` | 监听 `applications/` 目录变化并重载动态应用 | +| `status_timer` | 首页状态栏刷新 timer | + +## 4. `app` 结构与三类启动方式 + +`app` 定义在 `Launch.cpp`: + +```cpp +struct app +{ + std::string Name; + std::string Icon; + std::string Exec; + + std::function launch; + + app(std::string name, std::string icon, std::string exec, bool terminal); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause, bool run_as_root); + + template + app(std::string name, std::string icon, page_t tag); +}; +``` + +三类应用: + +| 类型 | 构造方式 | 启动函数 | 示例 | +| --- | --- | --- | --- | +| 内置页面 | `page_v` | 构造页面并 `lv_disp_load_scr()` | `GAME`、`SETTING`、`MUSIC` | +| 终端命令 | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`、`CLI` | +| 外部进程 | `exec, terminal=false` | `launch_Exec()` | AppStore、Calculator | + +## 5. 固定应用注册 + +`LaunchImpl` 构造函数先注册固定入口: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", + false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +随后把前 5 个应用写入首页 5 个槽位: + +```cpp +lv_label_set_text(UILaunchPage::label(0), it->Name.c_str()); +panel_set_icon(UILaunchPage::panel(0), it->Icon.c_str()); +``` + +初始状态: + +```text +slot 0 far-left : Python +slot 1 left : STORE +slot 2 center : CLI +slot 3 right : GAME +slot 4 far-right: SETTING +current_app : 2 +``` + +之后根据设置项和平台条件继续追加可选应用: + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("Music")) + app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); + +if (APP_ENABLED("Math")) + app_list.emplace_back("MATH", img_path("math_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); + +app_list.emplace_back("Compass", img_path("compass_needle_80.png"), page_v); +``` + +Linux 设备端还会按配置追加 `IP_PANEL`、`FILE`、`SSH`、`MESH`、`REC`、`CAMERA`、`LORA`、`TANK` 等页面。 + +## 6. 内置页面启动机制 + +内置页面通过模板构造: + +```cpp +template +app::app(std::string name, std::string icon, page_t) + : Name(std::move(name)), Icon(std::move(icon)) +{ + launch = [](LaunchImpl *self) + { + ui_loading_show("Loading..."); + lv_refr_now(NULL); + + auto p = std::make_shared(); + self->app_Page = p; + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&LaunchImpl::go_back_home, self); + + ui_loading_hide(); + }; +} +``` + +内置页面需要满足的约定: + +- 页面类可以无参构造。 +- 提供 `screen()` 返回页面根 screen。 +- 提供 `input_group()` 返回页面自己的输入 group。 +- 提供或继承 `navigate_home` 回调,用于返回首页。 + +启动时序: + +```text +Enter + -> app.launch(LaunchImpl*) + -> ui_loading_show("Loading...") + -> lv_refr_now(NULL) + -> make_shared() + -> app_Page = p 保持生命周期 + -> lv_disp_load_scr(p->screen()) + -> 输入设备切换到 p->input_group() + -> p->navigate_home = LaunchImpl::go_back_home + -> ui_loading_hide() +``` + +## 7. 终端应用启动机制 + +终端应用使用 `UIConsolePage`,外部命令在 APPLaunch 进程内的终端页面中执行: + +```cpp +void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) +{ + ui_loading_show("Loading..."); + lv_refr_now(NULL); + + auto p = std::make_shared(); + app_Page = p; + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&LaunchImpl::go_back_home, this); + p->terminal_sysplause = sysplause; + + ui_loading_hide(); + p->exec(exec); +} +``` + +典型入口: + +```text +Python -> exec = "python3", terminal = true +CLI -> exec = "bash", terminal = true +``` + +与内置页面相比,终端应用多一步 `p->exec(exec)`。它通常通过 PTY 与命令交互,用户看到的是 `UIConsolePage`,而不是离开 APPLaunch 的独立界面。 + +## 8. 外部独立应用启动机制 + +外部应用使用 `cp0_process_exec_blocking()`: + +```cpp +void launch_Exec(const std::string &exec, bool keep_root = false) +{ + ui_loading_show("Loading..."); + + lv_disp_t *disp = lv_disp_get_default(); + lv_indev_t *indev = lv_indev_get_next(NULL); + + LVGL_RUN_FLAGE = 0; + if (indev) + lv_indev_set_group(indev, NULL); + lv_timer_enable(false); + lv_refr_now(disp); + + int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, + keep_root ? 1 : 0); + + lv_timer_enable(true); + if (indev) + lv_indev_set_group(indev, UILaunchPage::home_input_group()); + lv_disp_load_scr(ui_Screen1); + ui_loading_hide(); + lv_obj_invalidate(lv_screen_active()); + lv_refr_now(disp); + LVGL_RUN_FLAGE = 1; +} +``` + +关键点: + +- 启动前先显示 Loading 并强刷,让用户立即看到反馈。 +- 关闭 APPLaunch 输入 group,避免外部进程运行时首页继续处理按键。 +- `lv_timer_enable(false)` 暂停 LVGL timer,外部程序接管前台。 +- `cp0_process_exec_blocking()` 阻塞等待外部程序退出。 +- 外部程序退出后恢复 timer、输入 group、首页 screen 和 `LVGL_RUN_FLAGE`。 + +时序文本: + +```text +Enter 外部应用 + -> ui_loading_show() + -> LVGL_RUN_FLAGE=0 + -> lv_indev_set_group(NULL) + -> lv_timer_enable(false) + -> lv_refr_now() + -> cp0_process_exec_blocking() + -> 外部程序运行 + -> APPLaunch 主渲染暂停 + -> 等待外部程序退出 + -> lv_timer_enable(true) + -> 绑定首页 input group + -> lv_disp_load_scr(ui_Screen1) + -> ui_loading_hide() + -> lv_refr_now() + -> LVGL_RUN_FLAGE=1 +``` + +`STORE` 是一个外部应用示例: + +```cpp +app_list.emplace_back("STORE", + img_path("store_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", + false, + true, + true); +``` + +其中 `run_as_root=true` 会传给 `launch_Exec(exec, run_as_root)`,再转成 `keep_root ? 1 : 0`。 + +## 9. 返回首页机制 + +内置页面和终端页面通过 `navigate_home` 回调返回首页: + +```cpp +void go_back_home() +{ + lv_async_call(lv_go_back_home, this); +} + +static void lv_go_back_home(void *arg) +{ + auto self = (LaunchImpl *)arg; + lv_timer_enable(true); + UILaunchPage::bind_home_input_group(); + lv_disp_load_scr(ui_Screen1); + lv_refr_now(NULL); + if (self->app_Page) + self->app_Page.reset(); +} +``` + +为什么使用 `lv_async_call()`: + +- 返回首页可能由页面事件或输入回调触发。 +- 异步执行可以避免在当前 LVGL 事件栈中直接销毁页面对象。 +- `app_Page.reset()` 会释放当前页面,必须确保不再使用该页面对象。 + +外部应用不使用 `navigate_home`,而是由 `cp0_process_exec_blocking()` 返回后恢复首页。 + +## 10. `.desktop` 动态应用扫描 + +动态应用目录: + +```cpp +const std::string app_dir_path = cp0_file_path("applications"); +``` + +设备安装后通常对应: + +```text +/usr/share/APPLaunch/applications/ +``` + +`.desktop` 示例: + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/e-Mail_80.png +``` + +`applications_load()` 只处理扩展名为 `.desktop` 的文件,并在 `[Desktop Entry]` 段读取字段: + +| 字段 | 是否必需 | 说明 | +| --- | --- | --- | +| `Name` | 是 | 首页显示标题 | +| `Exec` | 是 | 启动命令 | +| `Icon` | 否 | 图标路径 | +| `Terminal` | 否 | `true/True/1` 表示用 `UIConsolePage` 启动 | +| `Sysplause` | 否 | 传给终端页面的暂停策略,默认 true | + +注册逻辑: + +```cpp +if (page_title.empty() || app_exec.empty()) + continue; + +for (auto it : app_list) { + if (it.Exec == app_exec) { + in_list = true; + break; + } +} + +if (!in_list) + app_list.emplace_back(page_title, app_icon, app_exec, + app_terminal, app_sysplause); +``` + +注意:动态应用通过 `Exec` 去重;如果 `Exec` 与固定应用或其他 `.desktop` 相同,会跳过。 + +## 11. 动态应用目录监听与重载 + +`LaunchImpl` 构造末尾: + +```cpp +fixed_count = app_list.size(); +applications_load(); +inotify_init_watch(); +watch_timer = lv_timer_create(app_dir_watch_cb, 3000, this); +``` + +监听流程: + +```text +每 3 秒 LVGL timer + -> cp0_dir_watch_poll(dir_watcher) + -> 如果 applications/ 有变化 + -> applications_reload() + -> 删除 fixed_count 之后的动态应用 + -> applications_load() + -> refresh_ui_panels() +``` + +`refresh_ui_panels()` 根据当前 `current_app` 重新写入 5 个可见/隐藏槽位: + +```cpp +app_at(current_app - 2) -> far-left +app_at(current_app - 1) -> left +app_at(current_app) -> center +app_at(current_app + 1) -> right +app_at(current_app + 2) -> far-right +``` + +这保证动态应用增删后,首页不需要重建 LVGL 对象,只更新文字和图标。 + +## 12. 图标设置与资源路径 + +图标写入由 `panel_set_icon()` 完成: + +```cpp +static void panel_set_icon(lv_obj_t *panel, const char *src) +{ + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_obj_set_align(img, LV_ALIGN_CENTER); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, icon_src); +} +``` + +特点: + +- 每个 panel 复用第一个 child image,不重复创建图片对象。 +- 图片被拉伸到 panel 大小。 +- 如果路径为空或不可读,会写日志,但仍调用 `lv_image_set_src()`。 + +固定应用一般使用 `img_path("xxx.png")`,动态 `.desktop` 的 `Icon` 字段当前直接传入 `app_icon`。编写 `.desktop` 时要确认图标路径能被 LVGL 读取。 + +## 13. 从按键到启动的完整流程 + +```text +用户释放 ENTER + -> LV_EVENT_KEYBOARD 投递给 ui_Screen1 + -> main_key_switch() + -> code == KEY_ENTER 且 key_state == 0 + -> audio_play_enter() + -> app_launch(NULL) + -> app_launch() + -> active_launch_page->launch_selected_app() + -> UILaunchPage::launch_selected_app() + -> launch_->launch_app() + -> Launch::launch_app() + -> impl_->launch_app() + -> LaunchImpl::launch_app() + -> auto it = std::next(app_list.begin(), current_app) + -> it->launch(this) + -> 根据 app 类型进入内置页面 / 终端页面 / 外部进程 +``` + +## 14. 注意事项 + +- `Launch::bind_ui()` 必须被调用后才会创建 `LaunchImpl`,否则首页可以显示,但应用列表更新、状态栏 timer、目录监听和启动逻辑都不会工作。 +- `current_app` 默认是 `2`,固定应用前 5 个入口的顺序会影响初始中心卡片;调整固定应用顺序时要同步考虑首页初始体验。 +- 内置页面构造期间如果耗时较长,应保留 `ui_loading_show()` + `lv_refr_now()`,否则用户看不到即时反馈。 +- 外部应用启动时会暂停 APPLaunch 的 LVGL timer 和输入 group;外部程序必须正常退出或响应 HOME 逻辑,否则用户会感觉卡在外部界面。 +- `.desktop` 动态应用至少需要 `Name` 和 `Exec`;`Terminal=true` 适合命令行程序,图形/独占 framebuffer 程序应使用 `Terminal=false`。 +- 动态应用以 `Exec` 去重,不以 `Name` 去重;多个入口如果命令相同,只会保留第一个。 +- 修改 `applications/` 后最多等待 3 秒由 watcher 重载;如果 watcher 未初始化或平台不支持,需要重启 APPLaunch 验证。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" new file mode 100644 index 00000000..06d92a90 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" @@ -0,0 +1,333 @@ +# 05 - 内置页面框架 + +本章说明 APPLaunch 内置页面的类层次、生命周期、页面列表、页面注册方式以及新增页面时应遵守的约定。重点源码在 `projects/APPLaunch/main/ui/components/ui_app_page.hpp`、`projects/APPLaunch/main/ui/components/page_app/*.hpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`。 + +## 1. 内置页面是什么 + +内置页面是编译进 APPLaunch 进程内的 LVGL 页面类。它和 `.desktop` 外部应用不同: + +- 内置页面直接创建 `lv_obj_t *root_screen_`,通过 `lv_disp_load_scr(page->screen())` 切换到自己的 screen。 +- 页面对象保存在 `LaunchImpl::app_Page`,退出时由 `navigate_home` 回调异步释放。 +- 页面与首页共享 APPLaunch 进程、LVGL 主循环、输入线程、资源解析和 `cp0_lvgl_app.h` 系统接口。 +- 页面通常是 header-only,放在 `projects/APPLaunch/main/ui/components/page_app/`,由 `components/page_app.h` 汇总包含。 + +简化关系: + +```text +UILaunchPage 首页轮播 + | + v +LaunchImpl::launch_app() + | + +-- 外部命令: cp0_process_exec_blocking() + +-- 终端命令: UIConsolePage + PTY + +-- 内置页面: std::make_shared() + | + v + lv_disp_load_scr(page->screen()) +``` + +## 2. 页面基类层次 + +### 2.1 `AppPageRoot` + +`AppPageRoot` 是所有内置页面的根基类,位于 `projects/APPLaunch/main/ui/components/ui_app_page.hpp`。它负责创建独立 screen 和 LVGL 输入组。 + +```cpp +class AppPageRoot +{ +public: + std::string page_title_ = "APP"; + lv_group_t *input_group_; + lv_obj_t *root_screen_; + std::function navigate_home; + bool has_bottom_bar_ = false; + int top_bar_height_px_ = 20; + + AppPageRoot() + { + creat_base_UI(); + creat_input_group(); + } + + virtual ~AppPageRoot() + { + lv_obj_del(root_screen_); + lv_group_delete(input_group_); + } +}; +``` + +关键点: + +- `root_screen_` 是页面自己的顶层 screen,不挂在首页 `ui_Screen1` 下。 +- `input_group_` 默认只加入 `root_screen_`,启动页面时会被绑定到当前 `lv_indev_t`。 +- `navigate_home` 由 `LaunchImpl` 注入,页面按 ESC 或完成任务后调用它返回首页。 +- 析构函数删除 `root_screen_` 和 `input_group_`,因此页面内创建的 LVGL 子对象会随 screen 一起释放。 + +### 2.2 顶栏、内容区、底栏区域 + +`ui_app_page.hpp` 把页面拆成几个可复用区域: + +| 类 | 职责 | 默认尺寸 | +| --- | --- | --- | +| `AppTopBarRegion` | 创建状态顶栏,显示标题、WiFi、时间、电量 | 高 `20px` | +| `AppContentRegion` | 创建 `ui_APP_Container` 内容区 | 高 `150px`,有底栏时 `130px` | +| `AppBottomBarRegion` | 创建 `ui_BOTTOM_Container` 底栏 | 高 `20px` | +| `AppPageLayout` | 顶栏 + 内容区 | `320x170` 中的 `20+150` | +| `AppPageWithBottomBarLayout` | 顶栏 + 内容区 + 底栏 | `20+130+20` | +| `home_base` | 首页专用基类,不完全等同于 AppPage | 首页状态栏 + 轮播容器 | + +典型页面直接继承 `AppPage`: + +```cpp +class UIIpPanelPage : public AppPage +{ +public: + UIIpPanelPage() : AppPage() + { + set_page_title("IP INFO"); + creat_UI(); + event_handler_init(); + } +}; +``` + +少数游戏或全屏页面继承 `AppPageRoot`,自行占满 `320x170`,不使用默认顶栏。例如 `UIGamePage`、`UICompassPage`、`UITankBattlePage`。 + +## 3. 顶栏和状态刷新 + +通用顶栏由 `UIAppTopBar` 实现,包含: + +- 左侧标题:`set_page_title()` 最终更新 `top_bar_.set_title()`。 +- WiFi 信号:`cp0_wifi_get_status()`,未连接时隐藏 WiFi panel。 +- 时间:`cp0_time_str()`,默认 5 秒刷新一次。 +- 电量:响应 `LV_EVENT_BATTERY`,使用 `cp0_battery_info_t` 更新百分比和 bar。 + +关键源码路径: + +- `projects/APPLaunch/main/ui/components/ui_app_page.hpp`:`UIAppTopBar`、`AppTopBarRegion`。 +- `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`:`cp0_wifi_get_status()`、`cp0_time_str()`、`cp0_battery_read()` 等接口声明。 + +顶栏资源使用 `cp0_file_path_c()`: + +```cpp +lv_obj_set_style_bg_img_src(time_panel_, + cp0_file_path_c("status_time_background.png"), + LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +注意:普通内置页有自己的状态刷新 timer,页面析构时必须释放页面自建 timer;`AppTopBarRegion` 已负责释放顶栏状态 timer。 + +## 4. 页面生命周期 + +### 4.1 从首页启动内置页面 + +`Launch.cpp` 通过模板构造内置页面 app 描述: + +```cpp +template +app::app(std::string name, std::string icon, page_t) + : Name(std::move(name)), Icon(std::move(icon)) +{ + launch = [](LaunchImpl *ctx) { + auto p = std::make_shared(); + ctx->app_Page = p; + p->navigate_home = std::bind(&LaunchImpl::go_back_home, ctx); + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + }; +} +``` + +实际代码在 `projects/APPLaunch/main/ui/Launch.cpp` 中,核心逻辑是: + +1. 首页按 ENTER 后调用 `cpp_app_launch()`。 +2. `UILaunchPage::launch_selected_app()` 转到 `Launch::launch_app()`。 +3. `LaunchImpl::launch_app()` 找到当前 app,并执行该 app 的 `launch` 函数。 +4. 内置页面创建对象、加载 screen、切换输入组。 +5. 页面内部按 ESC 或业务完成后调用 `navigate_home()`。 +6. `LaunchImpl::go_back_home()` 使用 `lv_async_call()` 回到首页,重新绑定首页输入组并 reset `app_Page`。 + +### 4.2 返回首页 + +返回首页统一走 `navigate_home`,不要在页面中直接删除自己。 + +```cpp +if (navigate_home) + navigate_home(); +``` + +`LaunchImpl::lv_go_back_home()` 会: + +- `lv_timer_enable(true)` 恢复 LVGL timer。 +- `UILaunchPage::bind_home_input_group()` 绑定首页输入组。 +- `lv_disp_load_scr(ui_Screen1)` 加载首页 screen。 +- `app_Page.reset()` 释放当前页面对象。 + +注意事项: + +- 页面析构时必须停止自建 `lv_timer_t`、后台线程、文件 watcher、PTY 或音频资源。 +- 不要在键盘事件回调栈中直接 `delete this`,用 `navigate_home` 交给 `LaunchImpl` 异步处理。 +- 如果页面临时切换到子页面或嵌套页面,要恢复正确的输入组。 + +## 5. 当前内置页面列表 + +页面实现集中在 `projects/APPLaunch/main/ui/components/page_app/`。 + +| 页面类 | 文件 | 启动器名称 | 继承 | 说明 | +| --- | --- | --- | --- | --- | +| `UIConsolePage` | `ui_app_console.hpp` | `CLI` 或终端外部命令 | `AppPage` | 终端模拟器,PTY 读写,支持 ANSI/VT 序列和键盘转义序列 | +| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | 贪吃蛇游戏,全屏自绘,使用 LVGL timer 驱动 | +| `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | 系统设置、应用开关、亮度、音量、WiFi、相机分辨率等 | +| `UIMusicPage` | `ui_app_music.hpp` | `MUSIC` | `AppPage` | 音乐播放器,目录浏览、播放列表、音频回调 | +| `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | 指南针页面,传感器线程 + UI timer | +| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | `IP_PANEL` | `AppPage` | 网络接口/IP 信息列表,每秒刷新 | +| `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | 文件浏览器,目录列表和进入/返回 | +| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH 参数输入,连接后嵌入 `UIConsolePage` | +| `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh 消息列表、输入弹层、发送/刷新 | +| `UIRecPage` | `ui_app_rec.hpp` | `REC` | 自定义 `rec_page` | 录音/播放/文件列表,含异步资源管理 | +| `UICameraPage` | `ui_app_camera.hpp` | `CAMERA` | `AppPage` | 相机预览、图库、拍照、状态页 | +| `UILoraPage` | `ui_app_lora.hpp` | `LORA` | `AppPage` | LoRa 业务页面,内部还有 C 风格创建/销毁接口 | +| `UITankBattlePage` | `ui_app_tank_battle.hpp` | `TANK` | `AppPageRoot` | 坦克小游戏,全屏、固定按键映射 | + +`Python`、`STORE`、`MATH` 不是内置页面:它们分别通过命令或外部进程启动。 + +## 6. 页面注册和显示顺序 + +内置页面在 `LaunchImpl::LaunchImpl()` 中插入 `app_list`。固定前 5 个应用先初始化首页 5 个轮播槽: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +随后根据设置开关添加可选内置页: + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("Music")) + app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); + +if (APP_ENABLED("IP_Panel")) + app_list.emplace_back("IP_PANEL", img_path("ip_panel_100.png"), page_v); +``` + +约定: + +- `Store`、`CLI`、`Game`、`Setting` 在设置页中是 always-on,不能禁用。 +- `Compass` 当前在 `Launch.cpp` 中无条件加入,不受 `UISetupPage` 的 Launcher 开关列表控制。 +- Linux 设备构建下才加入 `IP_PANEL`、`FILE`、`SSH`、`MESH`、`REC`、`CAMERA`、`LORA`、`TANK` 等页面;SDL 构建会受 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 限制。 +- `.desktop` 动态应用在内置页之后扫描加入,目录变化由 watcher 每 3 秒检查。 + +## 7. 页面代码骨架 + +新增普通页面建议继承 `AppPage`: + +```cpp +#pragma once +#include "../ui_app_page.hpp" +#include "compat/input_keys.h" + +class UINewPage : public AppPage +{ +public: + UINewPage() : AppPage() + { + set_page_title("NEW"); + create_ui(); + event_handler_init(); + } + + ~UINewPage() + { + if (timer_) { + lv_timer_delete(timer_); + timer_ = nullptr; + } + } + +private: + lv_timer_t *timer_ = nullptr; + + void create_ui() + { + lv_obj_t *bg = lv_obj_create(ui_APP_Container); + lv_obj_set_size(bg, 320, 150); + lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); + } + + void event_handler_init() + { + lv_obj_add_event_cb(root_screen_, &UINewPage::event_cb, LV_EVENT_ALL, this); + } + + static void event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self || !IS_KEY_RELEASED(e)) + return; + + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + if (key == KEY_ESC && self->navigate_home) + self->navigate_home(); + } +}; +``` + +新增全屏页面可继承 `AppPageRoot`,但要自己处理 `320x170` 布局、状态提示和返回键。 + +## 8. 页面 UI 约定 + +- 分辨率按 `320x170` 设计;通用页面内容区是 `320x150`,顶部 `20px` 已被顶栏占用。 +- 页面内部对象通常保存在 `std::unordered_map ui_obj_`,便于重绘/删除。 +- 对列表页,优先使用固定行高 + 虚拟滚动,而不是让 LVGL 容器自由滚动,避免小屏输入焦点混乱。 +- 对频繁刷新页面,使用 `lv_timer_create()`,析构中 `lv_timer_delete()`。 +- 对后台线程或异步回调,使用 `std::atomic` alive 标记,析构时停止线程,避免页面释放后回调访问悬空对象。 +- 图片、音频、字体路径不要硬编码相对路径;使用 `img_path()`、`audio_path()` 或 `cp0_file_path_c()`。 + +## 9. 嵌套页面和特殊页面 + +`UISSHPage` 是一个典型的嵌套页面:输入 SSH 参数时由 `UISSHPage` 处理键盘;连接后创建 `UIConsolePage`,切换 screen 和输入组。 + +```cpp +console_page_ = std::make_shared(); +console_page_->navigate_home = [this]() { + console_page_.reset(); + view_state_ = ViewState::INPUT; + lv_disp_load_scr(this->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); +}; + +lv_disp_load_scr(console_page_->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); +``` + +这种页面要特别注意: + +- 子页面退出不一定等于回首页,可能只是回父页面。 +- 输入组必须随当前 screen 切换,否则按键会发给不可见页面。 +- 父页面析构时要先释放子页面对象。 + +## 10. 与首页轮播的关系 + +首页轮播本身由 `UILaunchPage.cpp` 管理: + +- `carousel_elements` 保存 5 张卡片、5 个标题和 5 个页点。 +- 左右切换时调用 `switch_left()` / `switch_right()`,动画完成后旋转数组并让 `LaunchImpl` 更新远端槽位内容。 +- ENTER 触发 `app_launch()`,最终调用当前 app 的 `launch()`。 + +内置页面不直接操作首页轮播;返回首页后轮播状态由 `LaunchImpl` 保持。 + +## 11. 常见注意事项 + +- 不要在页面构造函数中执行长耗时阻塞操作;先显示页面或 loading,再启动任务。 +- 不要假设 `lv_indev_get_next(NULL)` 一定非空,切换输入组前最好判空。 +- 不要在页面中直接访问首页全局对象,除非是明确的首页功能。 +- 页面标题请调用 `set_page_title()`,不要直接改顶栏内部 label。 +- 所有可退出页面都应支持 `KEY_ESC`,并调用 `navigate_home` 或返回上一级视图。 +- 页面开关 key 必须和 `UISetupPage::save_app_toggle()`、`Launch.cpp` 的 `APP_ENABLED()` 保持一致。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" new file mode 100644 index 00000000..bbbc2443 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" @@ -0,0 +1,361 @@ +# 06 - 资源与配置系统 + +本章说明 APPLaunch 的运行时资源目录、路径解析规则、`.desktop` 动态应用文件、配置 API、设置页配置 key 以及资源使用注意事项。重点源码在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`。 + +## 1. 资源系统概览 + +APPLaunch 不建议页面直接拼接运行时路径,而是通过 `cp0_file_path()` / `cp0_file_path_c()` 做统一解析。 + +```text +页面代码 / Launch.cpp + | + v +img_path(), audio_path(), cp0_file_path_c() + | + v +cp0_lvgl_file.cpp / sdl_lvgl_file.cpp + | + +-- 图片: share/images/... + +-- 音频: /usr/share/APPLaunch/share/audio/... + +-- 字体: /usr/share/APPLaunch/share/font/... + +-- applications: /usr/share/APPLaunch/applications + +-- keyboard_device / keyboard_map / lock_file 等特殊路径 +``` + +页面常用包装函数位于 `projects/APPLaunch/main/ui/components/ui_app_page.hpp`: + +```cpp +static inline std::string img_path(const char *name) +{ + return cp0_file_path(name); +} + +static inline std::string audio_path(const char *name) +{ + return cp0_file_path(name); +} +``` + +## 2. 运行时资源树 + +源码资源树位于: + +```text +projects/APPLaunch/APPLaunch/ +├── applications/ +├── bin/ +├── lib/ +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +设备端安装后通常对应: + +```text +/usr/share/APPLaunch/ +├── applications/ +├── bin/ +├── lib/ +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +| 目录 | 内容 | 使用者 | +| --- | --- | --- | +| `applications/` | `.desktop` 应用描述 | `LaunchImpl::applications_load()` | +| `share/images/` | 图标、状态栏背景、页面图片、GIF | 首页、顶栏、各内置页面 | +| `share/audio/` | `startup.mp3`、`switch.wav`、`enter.wav`、页面按键音 | 首页音效、设置页、页面音效 | +| `share/font/` | TTF/OTF 字体 | `LauncherFonts`、页面自定义字体 | +| `bin/` | 随包脚本和外部程序 | Store、更新脚本、动态应用 | +| `lib/` | 随包动态库 | 外部程序或平台能力 | + +## 3. 路径解析规则 + +### 3.1 设备端 `cp0_lvgl_file.cpp` + +设备端实现位于 `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`,根目录固定为: + +```cpp +constexpr const char *kAppRoot = "/usr/share/APPLaunch"; +``` + +核心规则: + +| 输入 | 输出 | +| --- | --- | +| `applications` | `/usr/share/APPLaunch/applications` | +| `lock_file` | `/tmp/M5CardputerZero-APPLaunch_fcntl.lock` | +| `keyboard_device` | `/dev/input/by-path/platform-3f804000.i2c-event` | +| `keyboard_map` | `/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map` | +| `store_sync_cmd` | `python /usr/share/APPLaunch/bin/store_cache_sync.py` | +| `*.png` / `*.gif` / `*.jpg` / `*.jpeg` / `*.svg` | `share/images/` | +| `*.wav` / `*.mp3` / `*.ogg` | `/usr/share/APPLaunch/share/audio/` | +| `*.ttf` / `*.otf` | `/usr/share/APPLaunch/share/font/` | +| 其它字符串 | 原样返回 | + +设备端图片规则目前返回 `share/images/` 这样的相对路径,而音频和字体返回 `/usr/share/APPLaunch/...` 绝对路径。页面写法要遵循现有 `img_path("xxx.png")` 约定,不要混用多个根目录。 + +### 3.2 SDL 实现 `sdl_lvgl_file.cpp` + +SDL 实现位于 `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`。规则和设备端基本一致,但 root 由 `get_app_root_path()` 决定,并通过 `app_relative_path(root_path, file, "share/images/")` 等函数适配开发机运行目录。 + +SDL 下仍保留特殊名: + +```cpp +if (file == "applications") return root_path + "/applications"; +if (file == "keyboard_device") return "/dev/input/by-path/platform-3f804000.i2c-event"; +if (file == "keyboard_map") return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; +``` + +注意:SDL 模式只是让 APPLaunch 可在开发机运行,并不代表所有设备资源都存在。例如相机、LoRa、某些 Linux 设备页可能被编译条件排除或运行时不可用。 + +### 3.3 `cp0_file_path_c()` 缓存 + +C 接口声明在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: + +```c +const char *cp0_file_path_c(const char *file); +``` + +实现使用 `thread_local std::unordered_map` 缓存: + +```cpp +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::unordered_map paths; + std::string key = file ? std::string(file) : std::string(); + auto it = paths.find(key); + if (it == paths.end()) { + it = paths.emplace(key, cp0_file_path(key)).first; + } + return it->second.c_str(); +} +``` + +因此返回的 `const char *` 在线程内稳定,可直接传给 LVGL 样式或图片接口;如果跨线程保存指针,建议保存 `std::string`。 + +## 4. 图片、音频、字体使用示例 + +### 4.1 图片 + +首页和内置页常用: + +```cpp +app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); + +lv_obj_set_style_bg_img_src(time_panel_, + cp0_file_path_c("status_time_background.png"), + LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +首页卡片图标由 `Launch.cpp::panel_set_icon()` 设置: + +```cpp +static void panel_set_icon(lv_obj_t *panel, const char *src) +{ + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, src); +} +``` + +注意:`panel_set_icon()` 会检查 `access(icon_src, R_OK)` 并记录日志。若设备端图片路径是相对路径,运行目录必须正确,否则日志会提示 missing/unreadable。 + +### 4.2 音频 + +首页音效通过系统音频信号播放资源名: + +```cpp +static void audio_play_ui_asset(const char *name) +{ + cp0_signal_system_play_asset(name); +} + +static void audio_play_switch(void) { audio_play_ui_asset("switch.wav"); } +static void audio_play_enter(void) { audio_play_ui_asset("enter.wav"); } +``` + +启动音在首页加载后播放: + +```cpp +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +页面内如需文件路径,可使用 `audio_path("key_enter.wav")`。如果底层 API 接收的是资源名而不是路径,应直接传资源名,避免重复拼路径。 + +### 4.3 字体 + +首页和顶栏使用 `LauncherFonts` 管理 freetype 字体: + +```cpp +launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD) +``` + +字体路径最终经 `cp0_file_path()` 解析到 `share/font/`。如果 freetype 字体加载失败,`LauncherFonts` 会回退到 LVGL 内置 Montserrat 字体。 + +## 5. `.desktop` 动态应用 + +动态应用文件放在 `cp0_file_path("applications")` 指向的目录。`LaunchImpl::applications_load()` 只处理 `*.desktop` 文件,并解析 `[Desktop Entry]` 段。 + +支持 key: + +| key | 必填 | 说明 | +| --- | --- | --- | +| `Name` | 是 | 轮播显示名称 | +| `Exec` | 是 | 启动命令或可执行路径 | +| `Icon` | 否 | 图标路径;可以是 `share/images/...` 或可被 LVGL 读取的路径 | +| `Terminal` | 否 | `true`/`True`/`1` 表示在 `UIConsolePage` 中运行 | +| `Sysplause` | 否 | 终端命令结束后是否暂停等待用户确认,默认 `true` | + +示例: + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/e-Mail_80.png +Sysplause=true +``` + +加载规则: + +- 只读取 `[Desktop Entry]` 段内的键值。 +- 空行、`#`、`;` 注释会跳过。 +- `Name` 和 `Exec` 缺一则跳过。 +- 如果 `Exec` 和已有 app 重复,会跳过。 +- `TryExec` 当前没有被 `applications_load()` 使用。 +- `applications/` 目录被 watcher 监听,每 3 秒轮询,有变化时清除动态应用并重新扫描。 + +## 6. 配置 API + +配置接口声明在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: + +```c +void cp0_config_init(void); +int cp0_config_get_int(const char *key, int default_val); +void cp0_config_set_int(const char *key, int val); +const char *cp0_config_get_str(const char *key, const char *default_val); +void cp0_config_set_str(const char *key, const char *val); +void cp0_config_save(void); +``` + +使用约定: + +- 读取时必须提供默认值,保证配置缺失时页面仍能运行。 +- 写入后需要调用 `cp0_config_save()` 持久化。 +- 设备端实现位于 `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`。 +- SDL 兼容实现位于 `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp`。 + +典型写法: + +```cpp +int volume = cp0_config_get_int("volume", cp0_volume_read()); +cp0_volume_write(new_val); +cp0_config_set_int("volume", new_val); +cp0_config_save(); +``` + +## 7. 配置 key 清单 + +### 7.1 启动器应用开关 + +设置页 `UISetupPage` 的 `Launcher` 菜单保存 `app_`: + +| 配置 key | 默认值 | 含义 | 备注 | +| --- | --- | --- | --- | +| `app_Python` | `1` | Python 入口显示开关 | 设置页可见,但 Python 在 `Launch.cpp` 固定加入,当前开关不影响固定项 | +| `app_Store` | `1` | Store 入口 | always-on,不能关闭 | +| `app_CLI` | `1` | CLI 入口 | always-on,不能关闭 | +| `app_Game` | `1` | GAME 入口 | always-on,不能关闭 | +| `app_Setting` | `1` | SETTING 入口 | always-on,不能关闭 | +| `app_Music` | `1` | MUSIC 内置页 | `Launch.cpp` 读取 | +| `app_Math` | `1` | Calculator 外部应用 | `Launch.cpp` 读取 | +| `app_IP_Panel` | `1` | IP_PANEL 内置页 | Linux 非 SDL 下读取 | +| `app_File` | `1` | FILE 内置页 | Linux 非 SDL 下读取 | +| `app_SSH` | `1` | SSH 内置页 | Linux 非 SDL 下读取 | +| `app_Mesh` | `1` | MESH 内置页 | Linux 非 SDL 下读取 | +| `app_Rec` | `1` | REC 内置页 | Linux 非 SDL 下读取 | +| `app_Camera` | `1` | CAMERA 内置页 | Linux 非 SDL 下读取 | +| `app_LoRa` | `1` | LORA 内置页 | Linux 非 SDL 下读取 | +| `app_Tank` | `1` | TANK 内置页 | Linux 非 SDL 下读取 | + +注意:`Compass` 当前无对应 `app_Compass` 设置项,`Launch.cpp` 无条件加入。 + +### 7.2 系统与页面配置 + +| 配置 key | 读写位置 | 含义 | +| --- | --- | --- | +| `brightness` | `UISetupPage`、`ext_components/cp0_lvgl/src/commount.c` | 背光亮度值,启动时恢复,设置页写入 | +| `volume` | `UISetupPage`、`commount.c` | 系统音量,启动时恢复,设置页写入 | +| `dark_time` | `UISetupPage` | 熄屏时间,选项为 `0/10/30/60/300` 秒 | +| `cam_resolution` | `UISetupPage`、相机页可读取 | 相机分辨率选项索引 | +| `startup_mode` | `UISetupPage` | 启动模式,当前选项为 `Launcher` / `CLI` | +| `extport_usb` | `UISetupPage` | 扩展口 USB 开关 | +| `extport_5vout` | `UISetupPage` | 扩展口 5V 输出开关 | +| `run_as_user` | `cp0_app_process.cpp`、`cp0_app_pty.cpp` | 外部进程/PTY 降权用户配置 | + +### 7.3 业务临时输入 + +以下内容多为页面内存状态,默认不持久化: + +- `UISSHPage` 的 Host/Port/User 默认值在构造函数中初始化,没有写入配置。 +- `UIMeshPage` 消息输入缓冲区只存在页面内存中。 +- `UIFilePage` 当前路径和选中行只存在页面内存中。 +- `UIIpPanelPage` 网络接口列表每秒从 `cp0_network_list()` 刷新。 + +## 8. 设置页配置写入路径 + +`UISetupPage` 是配置 key 最集中的位置。典型函数: + +- `menu_init()`:构建设置菜单,读取 `app_*`、`extport_*`。 +- `save_app_toggle()`:保存启动器应用开关。 +- `enter_brightness_adjust()` / `apply_value_selection()`:应用亮度、音量、熄屏、分辨率、启动模式。 +- `apply_volume()`:写系统音量并保存 `volume`。 + +示例: + +```cpp +void save_app_toggle(int idx) +{ + char cfg_key[64]; + snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); + bool enabled = menu_items_[0].sub_items[idx].toggle_state; + cp0_config_set_int(cfg_key, enabled ? 1 : 0); + cp0_config_save(); +} +``` + +修改配置 key 时必须同步检查: + +- `UISetupPage::menu_init()` 的 `app_keys` / `app_labels`。 +- `UISetupPage::save_app_toggle()` 的 `app_keys` 和 always-on 列表。 +- `Launch.cpp` 的 `APP_ENABLED("...")`。 +- 文档和默认配置。 + +## 9. 资源命名建议 + +- 首页图标按 `_100.png` 命名,如 `music_100.png`、`setting_100.png`。 +- 小图标或状态背景按功能命名,如 `status_time_background.png`、`status_battery_background.png`。 +- 页面专用资源使用页面前缀,如 `setting_ok.png`、`setting_cross.png`。 +- 音效用短名,如 `switch.wav`、`enter.wav`、`key_back.wav`。 +- 字体使用真实文件名,如 `Montserrat-Bold.ttf`,通过 `launcher_fonts().get()` 加载。 + +## 10. 常见问题和注意事项 + +- 图片和音频扩展名大小写会被转小写判断,但文件系统大小写仍敏感,文件名本身要匹配。 +- `cp0_file_path()` 只按扩展名分类,不检查文件是否存在。 +- `.desktop` 的 `Icon` 不会自动调用 `cp0_file_path()`;建议写 LVGL 能直接读取的路径,或与现有模板保持一致。 +- 如果新增资源用于设备端,确认打包脚本会把 `projects/APPLaunch/APPLaunch/share/...` 带入安装包。 +- 配置写入后如果忘记 `cp0_config_save()`,重启后会丢失。 +- `app_*` 开关影响的是下次构造 `LaunchImpl` 时的列表;运行中修改后不一定立即改变首页固定列表,视是否触发重建/重启而定。 +- `run_as_user` 会影响外部进程和 PTY 命令执行身份,调试权限问题时要检查该配置。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" new file mode 100644 index 00000000..459369b6 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" @@ -0,0 +1,420 @@ +# 07 - 输入系统与按键映射 + +本章说明 APPLaunch 的键盘输入线程、`key_item` 事件结构、LVGL 事件投递、首页和内置页面按键映射、终端输入转义以及调试注意事项。重点源码在 `projects/APPLaunch/main/include/keyboard_input.h`、`projects/APPLaunch/main/ui/ui.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`、`projects/APPLaunch/main/ui/UILaunchPage.cpp` 和 `projects/APPLaunch/main/ui/components/page_app/*.hpp`。 + +## 1. 输入系统概览 + +APPLaunch 的输入有两条路径: + +1. 自定义 `LV_EVENT_KEYBOARD`:携带完整 `struct key_item`,页面大多直接监听它。 +2. LVGL indev key:`cp0_keypad_read_cb()` 同时把 evdev key 转成 `LV_KEY_*`,供 LVGL group/focus 机制使用。 + +数据流: + +```text +物理键盘 / SDL 键盘 + | + v +libinput / SDL keyboard backend + | + v +keyboard_read_thread() + | + v +enqueue_key(struct key_item) + | + v +keyboard_queue + keyboard_mutex + | + v +cp0_keypad_read_cb() + | + +-- lv_obj_send_event(lv_screen_active(), LV_EVENT_KEYBOARD, key_item) + +-- ui_global_hint_on_key(key_item) + +-- data->key = cp0_evdev_process_key(key_code) +``` + +`LV_EVENT_KEYBOARD` 是 APPLaunch 自定义事件,不是 LVGL 内置键事件。`main.cpp` 启动时注册: + +```cpp +if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); +``` + +## 2. `key_item` 数据结构 + +`projects/APPLaunch/main/include/keyboard_input.h` 定义输入事件: + +```c +struct key_item { + uint32_t key_code; // Linux evdev key code + uint32_t keysym; // primary XKB keysym + uint32_t codepoint; // Unicode code point, 无字符则为 0 + uint32_t mods; // KBD_MOD_* 修饰键位图 + int key_state; // 0=released, 1=pressed, 2=repeat + char sym_name[65]; // XKB keysym name + char utf8[16]; // UTF-8 字符 + char flage; + STAILQ_ENTRY(key_item) entries; +}; +``` + +常量: + +| 常量 | 值/含义 | +| --- | --- | +| `KBD_KEY_RELEASED` | `0`,释放 | +| `KBD_KEY_PRESSED` | `1`,按下 | +| `KBD_KEY_REPEATED` | `2`,长按重复 | +| `KBD_MOD_SHIFT` | Shift 修饰 | +| `KBD_MOD_CTRL` | Ctrl 修饰 | +| `KBD_MOD_ALT` | Alt 修饰 | +| `KBD_MOD_LOGO` | Logo 修饰 | +| `KBD_MOD_CAPS` | CapsLock 状态 | +| `KBD_MOD_NUM` | NumLock 状态 | + +页面可用 `key_code` 做物理按键判断,也可用 `utf8` / `codepoint` 获取文本输入。 + +## 3. 事件宏和页面读取方式 + +`projects/APPLaunch/main/ui/ui.h` 提供常用宏: + +```c +#define LV_EVENT_KEYBOARD_GET_KEY(e) \ + ((struct key_item *)lv_event_get_param(e))->key_code + +#define LV_EVENT_KEYBOARD_GET_KEY_STATE(e) \ + ((struct key_item *)lv_event_get_param(e))->key_state + +#define IS_KEY_PRESSED(e) \ + ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && \ + (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) + +#define IS_KEY_RELEASED(e) \ + ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && \ + (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) +``` + +典型页面事件绑定: + +```cpp +void event_handler_init() +{ + lv_obj_add_event_cb(root_screen_, UIIpPanelPage::static_lvgl_handler, + LV_EVENT_ALL, this); +} + +static void static_lvgl_handler(lv_event_t *e) +{ + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self || !IS_KEY_RELEASED(e)) + return; + + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + self->handle_key(key); +} +``` + +注意:多数菜单页只在 release 时处理,避免 press 和 repeat 重复触发;游戏类页面可能在 press/repeat 时处理移动和射击。 + +## 4. 设备端输入线程 + +设备端实现在 `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`。 + +### 4.1 初始化 + +`init_input()` 做三件事: + +```c +if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + +pthread_create(&keyboard_read_thread_id, NULL, + keyboard_read_thread, (void *)keyboard_device); + +cp0_create_lvgl_input_devices(); +``` + +键盘设备默认来自: + +```c +const char *keyboard_device = getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); +``` + +如果环境变量为空,`keyboard_read_thread()` 使用默认: + +```text +/dev/input/by-path/platform-3f804000.i2c-event +``` + +这个路径也可通过 `cp0_file_path("keyboard_device")` 查到。 + +### 4.2 读取与入队 + +`keyboard_read_thread()` 使用 libinput 监听键盘事件,使用 xkbcommon 生成 `keysym`、`codepoint`、`utf8`,并使用 timerfd 生成 repeat 事件。 + +入队函数 `enqueue_key()`: + +```c +static void enqueue_key(const struct key_item *src) { + struct key_item *elm = malloc(sizeof(*elm)); + *elm = *src; + + if (elm->key_code == KEY_ESC) { + LVGL_HOME_KEY_FLAG = elm->key_state; + } + + if (LVGL_RUN_FLAGE) { + pthread_mutex_lock(&keyboard_mutex); + STAILQ_INSERT_TAIL(&keyboard_queue, elm, entries); + pthread_mutex_unlock(&keyboard_mutex); + } else { + free(elm); + } +} +``` + +关键全局状态: + +| 变量 | 含义 | +| --- | --- | +| `keyboard_queue` | 待 LVGL 消费的 `key_item` 队列 | +| `keyboard_mutex` | 队列锁 | +| `LVGL_HOME_KEY_FLAG` | ESC 当前状态;外部应用运行时用于长按返回/杀进程逻辑 | +| `LVGL_RUN_FLAGE` | LVGL 是否接收输入;外部应用运行时可置 0 | +| `LV_EVENT_KEYBOARD` | 自定义 LVGL 事件 id | + +### 4.3 出队与投递 + +`cp0_keypad_read_cb()` 从队列取出事件,并投递给当前 active screen: + +```c +lv_obj_t *root = lv_screen_active(); +if (root) + lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); + +ui_global_hint_on_key(elm); + +data->key = cp0_evdev_process_key(elm->key_code); +if (data->key) { + data->state = (lv_indev_state_t)elm->key_state; + data->continue_reading = !STAILQ_EMPTY(&keyboard_queue); +} +free(elm); +``` + +注意:`elm` 在回调结束后被释放,因此页面不能长期保存 `lv_event_get_param(e)` 指针。需要跨异步使用时,应复制字段。 + +## 5. evdev 到 LVGL key 的转换 + +`cp0_evdev_process_key()` 把部分 Linux evdev key 转为 LVGL 导航 key: + +| evdev key | LVGL key | +| --- | --- | +| `KEY_UP` | `LV_KEY_UP` | +| `KEY_DOWN` | `LV_KEY_DOWN` | +| `KEY_LEFT` | `LV_KEY_LEFT` | +| `KEY_RIGHT` | `LV_KEY_RIGHT` | +| `KEY_ESC` | `LV_KEY_ESC` | +| `KEY_DELETE` | `LV_KEY_DEL` | +| `KEY_BACKSPACE` | `LV_KEY_BACKSPACE` | +| `KEY_ENTER` | `LV_KEY_ENTER` | +| `KEY_NEXT` | `LV_KEY_NEXT` | +| `KEY_PREVIOUS` | `LV_KEY_PREV` | +| `KEY_HOME` | `LV_KEY_HOME` | +| `KEY_END` | `LV_KEY_END` | + +如果页面直接处理 `LV_EVENT_KEYBOARD`,通常使用原始 `KEY_*`;如果页面交给 LVGL 控件焦点机制,则依赖 `data->key`。 + +`projects/APPLaunch/main/include/compat/input_keys.h` 在 Linux 下包含 ``,在非 Linux 平台提供常见 `KEY_*` 兼容定义,保证 SDL/桌面构建也能编译页面代码。 + +## 6. 首页按键映射 + +首页按键处理在 `projects/APPLaunch/main/ui/UILaunchPage.cpp::main_key_switch()`。 + +先将 CardputerZero 上常用的 `F/X/Z/C` 映射为方向键: + +```cpp +static uint32_t fzxc_to_arrow(uint32_t key) +{ + switch (key) { + case KEY_F: return KEY_UP; + case KEY_X: return KEY_DOWN; + case KEY_Z: return KEY_LEFT; + case KEY_C: return KEY_RIGHT; + default: return key; + } +} +``` + +首页行为: + +| 输入 | 触发时机 | 行为 | +| --- | --- | --- | +| `KEY_LEFT` 或 `Z` | pressed/repeat | 播放 `switch.wav`,调用 `switch_right()`,轮播向右取下一项 | +| `KEY_RIGHT` 或 `C` | pressed/repeat | 播放 `switch.wav`,调用 `switch_left()`,轮播向左取下一项 | +| `KEY_ENTER` | released | 播放 `enter.wav`,启动当前 app | +| `KEY_F12` | released | 开关绿色全屏调试遮罩,设置 `lvping_lock` | +| `KEY_UP` / `KEY_DOWN` 或 `F` / `X` | pressed/repeat | 当前首页未定义动作 | + +注意:`main_key_switch()` 对左右键在 press 阶段处理,因此长按可能产生 repeat 并连续切换;ENTER 在 release 阶段启动,避免按下期间重复启动。 + +## 7. 内置页面按键映射总览 + +各页面独立绑定 `root_screen_` 的 `LV_EVENT_KEYBOARD`。常见约定如下: + +| 页面 | 文件 | 主要按键 | +| --- | --- | --- | +| `UIConsolePage` | `ui_app_console.hpp` | ESC/方向/回车/退格转 PTY 控制序列;HOME 相关状态用于退出/外部锁 | +| `UIGamePage` | `ui_app_game.hpp` | 方向键移动,ENTER 开始/重开,ESC 返回 | +| `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN 或 F/X 选择,ENTER/RIGHT 或 C 进入/确认,ESC/LEFT 或 Z 返回,部分页面支持 R/D | +| `UIMusicPage` | `ui_app_music.hpp` | F/X/Z/C 映射到 LV_KEY_UP/DOWN/LEFT/RIGHT;ENTER 播放/载入;ESC 返回 | +| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | F/X/Z/C 映射到 LV_KEY_*;UP/DOWN 选择;ESC 返回 | +| `UIFilePage` | `ui_app_file.hpp` | UP/DOWN 选择;RIGHT/ENTER 进入;LEFT 返回上级;ESC 返回首页或上级 | +| `UISSHPage` | `ui_app_ssh.hpp` | UP/DOWN 切换 Host/Port/User;字符输入;BACKSPACE 删除;ENTER 连接;ESC 返回 | +| `UIMeshPage` | `ui_app_mesh.hpp` | S 打开输入;R 刷新;UP/DOWN 浏览;ENTER 发送;BACKSPACE 删除;ESC 取消/返回 | +| `UICameraPage` | `ui_app_camera.hpp` | ESC 返回/退出页面;ENTER 拍照/确认;UP/DOWN/LEFT/RIGHT 导航;1-5 快捷按钮 | +| `UIRecPage` | `ui_app_rec.hpp` | 根据录音/列表状态处理导航、确认、返回 | +| `UICompassPage` | `ui_app_compass.hpp` | F4/F6 校准或切换,ESC 返回 | +| `UILoraPage` | `ui_app_lora.hpp` | 将 KEY_UP/DOWN/LEFT/RIGHT/ENTER/ESC/BACKSPACE/DELETE 转为 LV_KEY_* 后交给业务处理 | +| `UITankBattlePage` | `ui_app_tank_battle.hpp` | `33(F)` 上、`45(X)` 下、`44(Z)` 左、`46(C)` 右、`57(SPACE)` 发射、ESC 返回 | + +## 8. F/X/Z/C 方向键约定 + +CardputerZero 键盘上常用 `F/X/Z/C` 作为方向替代。代码中有三种用法: + +1. 首页 `UILaunchPage.cpp`:`fzxc_to_arrow()` 把 `F/X/Z/C` 转为 `KEY_UP/DOWN/LEFT/RIGHT`。 +2. 页面内部转换为 LVGL key,如 `UIMusicPage`、`UIIpPanelPage`: + +```cpp +switch (key) { +case KEY_F: return LV_KEY_UP; +case KEY_X: return LV_KEY_DOWN; +case KEY_Z: return LV_KEY_LEFT; +case KEY_C: return LV_KEY_RIGHT; +} +``` + +3. 游戏直接使用 evdev 数字:`UITankBattlePage` 中 `KEY_MOVE_UP = 33`、`KEY_MOVE_DOWN = 45`、`KEY_MOVE_LEFT = 44`、`KEY_MOVE_RIGHT = 46`。 + +新增页面建议优先使用 `KEY_F` 等符号名,不要写裸数字;如果为了兼容历史提示写数字,应在注释中说明对应键名。 + +## 9. 文本输入 + +有些页面需要输入字符,例如 SSH、Mesh、WiFi 密码、终端。 + +### 9.1 简单 ASCII 映射 + +`UISSHPage` 和 `UIMeshPage` 使用 `keycode_to_char()` 将 `KEY_1`、`KEY_Q` 等转成小写字符: + +```cpp +static char keycode_to_char(uint32_t key) +{ + if (key >= KEY_1 && key <= KEY_9) return '1' + (key - KEY_1); + if (key == KEY_0) return '0'; + if (key >= KEY_Q && key <= KEY_P) return qwerty[key - KEY_Q]; + if (key == KEY_SPACE) return ' '; + if (key == 52) return '.'; // KEY_DOT + if (key == 12) return '-'; // KEY_MINUS + return 0; +} +``` + +这种方式简单,但不支持 Shift 大写、输入法、多字节字符。需要完整文本能力时,应读取 `key_item::utf8` 或 `codepoint`。 + +### 9.2 终端输入 + +`UIConsolePage` 直接读取 `struct key_item`,把物理键和 UTF-8 文本转为 PTY 字节流: + +- `KEY_ENTER` -> `\r` +- `KEY_BACKSPACE` -> `0x7f` +- `KEY_ESC` -> `0x1b` +- 方向键 -> `\033[A/B/C/D` 或 application cursor mode 下的 `\033OA/OB/OC/OD` +- 普通字符 -> `key_item::utf8` + +终端页面要同时处理子进程退出、屏幕刷新、光标闪烁和 ESC/Home 返回语义,因此比普通页面更复杂。 + +## 10. 外部应用运行时的输入处理 + +外部应用通过 `LaunchImpl::launch_Exec()` 启动: + +```cpp +LVGL_RUN_FLAGE = 0; +lv_indev_set_group(indev, NULL); +lv_timer_enable(false); + +int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); + +lv_timer_enable(true); +lv_indev_set_group(indev, UILaunchPage::home_input_group()); +lv_disp_load_scr(ui_Screen1); +LVGL_RUN_FLAGE = 1; +``` + +含义: + +- 外部进程运行时,APPLaunch 暂停 LVGL timer,并停止接收普通键盘队列事件。 +- ESC 状态仍会更新 `LVGL_HOME_KEY_FLAG`,供 `APPLaunch_lock()` 或外部进程返回逻辑判断。 +- 外部进程退出后,恢复首页 screen、输入组和 LVGL timer。 + +`main.cpp::APPLaunch_lock()` 还会检查锁文件持有者;如果外部应用持锁且 ESC 长按达到约 5 秒,会调用 `cp0_process_kill(holder_pid, 3000)` 尝试结束外部应用。 + +## 11. 输入组切换 + +首页和页面各有自己的 LVGL group: + +- 首页:`UILaunchPage::home_input_group()`。 +- 内置页:`AppPageRoot::input_group()`。 +- 嵌套终端:`UIConsolePage::input_group()`。 + +切换页面时必须同步切换输入组: + +```cpp +lv_disp_load_scr(p->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); +``` + +返回首页: + +```cpp +UILaunchPage::bind_home_input_group(); +lv_disp_load_scr(ui_Screen1); +``` + +如果 screen 已切换但 group 仍指向旧页面,可能出现: + +- 页面看得见但按键无效。 +- 不可见页面仍响应按键。 +- 嵌套页面退出后 ESC 行为异常。 + +## 12. 调试输入问题 + +设备端键盘层已有日志: + +```text +[KBD] enqueue code=... state=... sym=... utf8=... cp=... mods=... run=... home_flag=... +[INDEV] dequeue code=... state=... sym=... utf8=... cp=... active_screen=... +[LAUNCHER] main_key_switch raw=...->code=... state=... sym=... +``` + +排查顺序: + +1. 确认 `keyboard_read_thread()` 是否启动,设备路径是否正确。 +2. 看 `[KBD] enqueue` 是否出现;没有则是 libinput/device/xkb 层问题。 +3. 看 `[INDEV] dequeue` 是否出现;没有则可能队列未被 LVGL indev 消费。 +4. 看 `active_screen` 是否为当前页面 screen。 +5. 看页面是否绑定了 `root_screen_` 的 `LV_EVENT_KEYBOARD`。 +6. 看页面是处理 press、release 还是 repeat,是否触发时机不一致。 +7. 看 `LVGL_RUN_FLAGE` 是否为 0;外部应用运行时普通事件会被丢弃。 + +## 13. 新增页面按键建议 + +新增页面建议遵守这些规则: + +- 列表和菜单页:在 `IS_KEY_RELEASED(e)` 时处理 `KEY_UP/DOWN/LEFT/RIGHT/ENTER/ESC`。 +- 游戏页:在 `IS_KEY_PRESSED(e)` 时处理持续动作,必要时接受 repeat。 +- 文本输入页:优先使用 `key_item::utf8`,简单场景才写 `keycode_to_char()`。 +- 返回键:ESC 必须可退出当前页面或当前弹层;如果有多级视图,先退回上一级,再回首页。 +- 方向替代键:如支持设备键盘,统一支持 `F/X/Z/C`。 +- 不要保存 `struct key_item *` 指针;需要异步处理时复制 `key_code`、`utf8` 等字段。 +- 对可长按的键,明确区分 `KBD_KEY_PRESSED`、`KBD_KEY_REPEATED`、`KBD_KEY_RELEASED`,避免重复确认或重复启动。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" new file mode 100644 index 00000000..c33d1a3f --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" @@ -0,0 +1,907 @@ +# 08 - 构建与编译指南 + +本章说明 `projects/APPLaunch` 的完整构建方法,覆盖 Linux SDL2 本机仿真、设备端原生编译、Linux x86 交叉编译、macOS 交叉编译、依赖安装、环境变量、SCons 关键逻辑和常见错误处理。 + +所有命令默认从仓库根目录开始: + +```bash +cd /home/nihao/w2T/github/launcher +``` + +## 1. 构建目标速览 + +APPLaunch 可以编译成多种形态。核心差异由 `CONFIG_DEFAULT_FILE` 指向的配置文件决定。 + +| 构建目标 | 运行位置 | 配置文件 | 显示/输入后端 | 典型用途 | +| --- | --- | --- | --- | --- | +| Linux SDL2 本机仿真 | Linux x86_64 开发机 | `linux_x86_sdl2_config_defaults.mk` | SDL2 窗口 + SDL 输入 | 日常 UI 调试、快速开发 | +| 设备端原生编译 | M5CardputerZero AArch64 Linux | `config_defaults.mk` | Linux framebuffer + evdev | 在设备上直接构建运行 | +| Linux x86 交叉编译 | Linux x86_64 开发机,产物运行在设备 | `linux_x86_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | 推荐的正式设备产物构建方式 | +| macOS 交叉编译 | macOS 开发机,产物运行在设备 | `mac_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | 在 macOS 上生成 arm64 设备产物 | +| macOS SDL/Darwin 配置 | macOS 开发机 | `darwin_config_defaults.mk` | SDL 相关配置 | 本机 SDL 方向的配置基础 | + +构建产物通常出现在: + +```text +projects/APPLaunch/dist/ +├── M5CardputerZero-APPLaunch +├── APPLaunch/ +└── store_cache_sync.py +``` + +其中: + +- `M5CardputerZero-APPLaunch` 是主可执行文件。 +- `APPLaunch/` 是运行时资源树,会被复制到 `dist/APPLaunch`。 +- `store_cache_sync.py` 来自仓库 `doc/store_cache_sync.py`,由 `STATIC_FILES` 一起复制。 + +## 2. 前置条件 + +### 2.1 子模块和目录结构 + +首次克隆推荐使用: + +```bash +git clone --recursive https://github.com/CardputerZero/launcher.git +cd launcher +``` + +如果已经克隆但子模块未初始化: + +```bash +git submodule update --init --recursive +``` + +APPLaunch 的顶层 `SConstruct` 假设目录关系如下: + +```text +launcher/ +├── SDK/ +├── ext_components/ +└── projects/ + └── APPLaunch/ + ├── SConstruct + └── main/SConstruct +``` + +进入 APPLaunch 工程目录后构建: + +```bash +cd projects/APPLaunch +``` + +不要在仓库根目录直接执行 APPLaunch 的 `scons`,因为 `PROJECT_PATH`、`SDK_PATH` 和 `EXT_COMPONENTS_PATH` 都以当前工程目录为基础推导。 + +### 2.2 Python 依赖 + +SCons 和 Kconfig 工具需要 Python 3.8 或更高版本。 + +```bash +python3 --version +``` + +通用 Python 包: + +```bash +python3 -m pip install --user parse scons requests tqdm +python3 -m pip install --user setuptools-rust paramiko scp +``` + +作用说明: + +| 包 | 用途 | +| --- | --- | +| `scons` | 主构建入口 | +| `parse` | SCons 脚本和 SDK 构建工具解析配置/命令输出 | +| `requests`、`tqdm` | SDK 工具下载依赖源码或 sysroot 包时使用 | +| `paramiko`、`scp` | `scons push` 通过 SSH 上传 `dist` 时使用 | +| `setuptools-rust` | 某些 Python 依赖构建时可能需要 | + +如果使用虚拟环境: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +## 3. Linux 开发机依赖安装 + +### 3.1 基础依赖 + +Debian/Ubuntu 示例: + +```bash +sudo apt update +sudo apt install -y \ + python3 python3-pip python3-venv \ + build-essential pkg-config git \ + libffi-dev +``` + +### 3.2 SDL2 仿真依赖 + +Linux SDL2 构建会在 `main/SConstruct` 中调用: + +```python +pkg_config_cflags("freetype2") +pkg_config_cflags("sdl2") +pkg_config_ldflags("sdl2") +``` + +因此本机需要安装 SDL2、FreeType 以及输入相关库: + +```bash +sudo apt install -y \ + libsdl2-dev libfreetype6-dev \ + libinput-dev libxkbcommon-dev libudev-dev +``` + +建议先确认 `pkg-config` 能找到库: + +```bash +pkg-config --cflags sdl2 +pkg-config --libs sdl2 +pkg-config --cflags freetype2 +pkg-config --libs freetype2 +``` + +### 3.3 Linux x86 交叉编译依赖 + +Linux x86_64 交叉编译到 M5CardputerZero AArch64 需要 GNU AArch64 交叉工具链: + +```bash +sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +``` + +验证: + +```bash +aarch64-linux-gnu-gcc --version +aarch64-linux-gnu-g++ --version +``` + +交叉编译还需要设备侧头文件和库。APPLaunch 的顶层 `SConstruct` 会在交叉编译时自动准备 SDK 静态 sysroot: + +```text +SDK/github_source/static_lib_v0.0.4 +``` + +如果该目录不存在或其中 `version` 文件不匹配 `v0.0.4`,构建脚本会从发布包下载: + +```text +https://github.com/CardputerZero/M5CardputerZero-UserDemo/releases/download/v0.0.4/sdk_bsp.tar.gz +``` + +因此首次交叉编译需要网络可用;离线环境需要提前准备好 `SDK/github_source/static_lib_v0.0.4`。 + +## 4. macOS 依赖安装 + +### 4.1 Python 环境 + +推荐使用虚拟环境: + +```bash +python3 -m venv launcher-python-venv +source launcher-python-venv/bin/activate +pip3 install parse scons requests tqdm setuptools-rust paramiko scp +``` + +### 4.2 macOS 交叉工具链 + +`mac_cross_cp0_config_defaults.mk` 指定: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-unknown-linux-gnu-" +``` + +安装方式: + +```bash +brew tap messense/macos-cross-toolchains +brew install aarch64-unknown-linux-gnu +``` + +验证: + +```bash +aarch64-unknown-linux-gnu-gcc --version +aarch64-unknown-linux-gnu-g++ --version +``` + +### 4.3 macOS SDL/Darwin 相关依赖 + +如果使用 `darwin_config_defaults.mk` 方向做本机 SDL 调试,需要准备 SDL2 和 FreeType。常见安装方式: + +```bash +brew install sdl2 freetype pkg-config +``` + +确认: + +```bash +pkg-config --cflags sdl2 +pkg-config --cflags freetype2 +``` + +## 5. 关键环境变量 + +### 5.1 `CONFIG_DEFAULT_FILE` + +`CONFIG_DEFAULT_FILE` 是最重要的构建选择变量。 + +示例: + +```bash +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +``` + +SCons 会把它传给 Kconfig 生成: + +```text +build/config/global_config.mk +build/config/global_config.h +``` + +如果不设置,`projects/APPLaunch/SConstruct` 有自动逻辑: + +- 当 `platform.machine()` 是 `x86_64` 时,默认使用 `linux_x86_sdl2_config_defaults.mk`。 +- 如果环境变量 `CardputerZero=y`,强制使用 `linux_x86_cross_cp0_config_defaults.mk`。 +- 设备端原生编译通常需要显式指定 `CONFIG_DEFAULT_FILE=config_defaults.mk`,避免默认逻辑误判。 + +### 5.2 `CardputerZero` + +快捷选择交叉编译配置: + +```bash +export CardputerZero=y +``` + +等价于让顶层 `SConstruct` 设置: + +```text +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +``` + +建议在自动化脚本中仍然显式写 `CONFIG_DEFAULT_FILE`,这样更容易排查构建目标。 + +### 5.3 `SDK_PATH` 和 `EXT_COMPONENTS_PATH` + +APPLaunch 顶层 `SConstruct` 会自动设置: + +```python +os.environ["SDK_PATH"] = str(sdk_path) +os.environ["EXT_COMPONENTS_PATH"] = str(sdk_path.parent / "ext_components") +``` + +含义: + +| 变量 | 默认值 | 作用 | +| --- | --- | --- | +| `SDK_PATH` | 仓库根目录下的 `SDK` | 让 SDK 构建系统找到 Kconfig、SCons 工具和内置组件 | +| `EXT_COMPONENTS_PATH` | 仓库根目录下的 `ext_components` | 让构建系统加载 `cp0_lvgl`、`Miniaudio`、`Sigslot` 等扩展组件 | + +通常不要手工覆盖这两个变量,除非你确实在测试外部 SDK 或组件目录。 + +### 5.4 `CONFIG_TOOLCHAIN_SYSROOT` + +交叉编译时由顶层 `SConstruct` 自动写入临时配置: + +```text +build/config/config_tmp.mk +``` + +内容类似: + +```make +CONFIG_TOOLCHAIN_SYSROOT="/path/to/launcher/SDK/github_source/static_lib_v0.0.4" +CONFIG_TOOLCHAIN_FLAGS="-I/path/to/launcher/SDK/github_source/static_lib_v0.0.4/usr/include/aarch64-linux-gnu" +``` + +SDK 构建系统读取后会追加: + +```text +--sysroot=$CONFIG_TOOLCHAIN_SYSROOT +-I$CONFIG_TOOLCHAIN_SYSROOT/usr/include +-I$CONFIG_TOOLCHAIN_SYSROOT/usr/include/ +-L$CONFIG_TOOLCHAIN_SYSROOT/lib/ +-L$CONFIG_TOOLCHAIN_SYSROOT/usr/lib/ +``` + +`main/SConstruct` 也会使用它追加 FreeType、libpng、libcamera 的 include 和链接路径。 + +### 5.5 `APPLAUNCH_STARTUP_ANIMATION` + +启动动画是可选编译宏: + +```bash +export APPLAUNCH_STARTUP_ANIMATION=1 +``` + +当该变量为 `1` 时,`main/SConstruct` 会添加: + +```text +-DAPPLAUNCH_STARTUP_ANIMATION +``` + +如果未设置,启动动画相关代码不会启用。 + +### 5.6 调试构建输出 + +SDK 构建系统在没有 `CONFIG_COMMPILE_DEBUG` 时会使用简洁输出,例如 `CXX ...`、`Linking ...`。如果需要看完整编译命令,可以尝试: + +```bash +export CONFIG_COMMPILE_DEBUG=y +scons -j8 +``` + +## 6. Linux SDL2 本机编译和运行 + +这是开发 UI 时最常用的方式,产物运行在 Linux x86_64 开发机的 SDL2 窗口中。 + +### 6.1 清理旧配置 + +切换构建目标前必须清理。尤其是从交叉编译切回 SDL2 时,旧的 `build/config/global_config.mk` 会保留原目标配置。 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +``` + +### 6.2 编译 + +```bash +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons -j8 +``` + +如果当前机器是 `x86_64`,也可以不设置 `CONFIG_DEFAULT_FILE`,因为顶层 `SConstruct` 会默认选择 SDL2 配置。但文档和脚本中建议显式设置。 + +### 6.3 运行 + +```bash +cd dist +./M5CardputerZero-APPLaunch +``` + +SDL2 配置启用: + +```make +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_V9_5_LV_OS_PTHREAD=y +``` + +因此在 `dist` 目录运行时,LVGL 的 POSIX 文件系统根路径是当前目录,资源路径可以通过 `./APPLaunch/...` 解析。若在 `projects/APPLaunch` 目录直接运行 `dist/M5CardputerZero-APPLaunch`,资源相对路径可能不同,建议优先进入 `dist` 运行。 + +### 6.4 SDL2 构建会链接的库 + +当配置文件包含 `linux_x86_sdl2_config_defaults.mk` 时,`main/SConstruct` 会额外处理: + +- 给 LVGL 组件添加 FreeType 编译和链接参数。 +- 给 APPLaunch 添加 SDL2 编译和链接参数。 +- 链接 `input`、`xkbcommon`、`udev`。 +- 过滤掉 LVGL 组件中的 `lv_sdl_keyboard.c`,避免和工程自定义键盘输入路径冲突。 + +## 7. 设备端原生编译 + +设备端原生编译指在 M5CardputerZero 的 AArch64 Linux 系统上直接构建 APPLaunch。优点是工具链和运行库天然匹配;缺点是设备性能和存储空间有限,编译速度较慢。 + +### 7.1 设备端安装依赖 + +在设备上执行: + +```bash +sudo apt update +sudo apt install -y \ + python3 python3-pip python3-venv \ + build-essential pkg-config git \ + libffi-dev libfreetype6-dev \ + libinput-dev libxkbcommon-dev libudev-dev \ + libcamera-dev libjpeg-dev +python3 -m pip install --user parse scons requests tqdm setuptools-rust paramiko scp +``` + +设备镜像的包名可能略有差异。如果 `libcamera-dev` 不存在,先确认镜像源是否启用,或使用系统中已经提供的 libcamera 头文件和库。 + +### 7.2 编译 + +```bash +cd /home/pi/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=config_defaults.mk +scons -j2 +``` + +设备端建议 `-j2` 或 `-j4`,避免内存不足。`config_defaults.mk` 启用: + +```make +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +### 7.3 运行 + +设备端配置的资源根路径是 `/usr/share/APPLaunch/`,所以仅从 `dist` 目录直接运行可能找不到正式部署路径下的资源。临时测试可以二选一: + +1. 复制资源到正式位置: + +```bash +sudo mkdir -p /usr/share/APPLaunch +sudo cp -a dist/APPLaunch/. /usr/share/APPLaunch/ +sudo install -m 0755 dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +2. 用 SDL2 配置做本机资源路径调试,不用于设备正式运行。 + +正式设备部署建议使用第 09 章的 `.deb` 打包和 systemd 服务。 + +## 8. Linux x86 交叉编译到设备 + +这是推荐的正式构建方式:在 Linux x86_64 开发机上生成 arm64 产物,然后打包或上传到设备。 + +### 8.1 清理并选择配置 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +也可以使用: + +```bash +export CardputerZero=y +scons -j8 +``` + +但推荐显式设置 `CONFIG_DEFAULT_FILE`。 + +### 8.2 交叉编译配置说明 + +`linux_x86_cross_cp0_config_defaults.mk` 关键项: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_LINUX_FBDEV_RENDER_MODE_FULL=y +CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +含义: + +- 使用 `aarch64-linux-gnu-gcc/g++`。 +- 使用设备 framebuffer,不创建 SDL2 窗口。 +- 使用 evdev 读取键盘/输入事件。 +- 资源路径固定为 `/usr/share/APPLaunch/`。 +- 开启 NEON 汇编优化。 +- 使用 full render mode,适合设备屏幕完整刷新策略。 + +### 8.3 自动 sysroot 逻辑 + +顶层 `SConstruct` 判断 `CONFIG_DEFAULT_FILE` 中包含 `cross` 时,会启用 `cross_package_enabled`: + +```python +if "cross" in os.environ.get("CONFIG_DEFAULT_FILE", ''): + cross_package_enabled = True +``` + +然后生成 `build/config/config_tmp.mk`,写入: + +```text +CONFIG_TOOLCHAIN_SYSROOT="SDK/github_source/static_lib_v0.0.4" +CONFIG_TOOLCHAIN_FLAGS="-I.../usr/include/aarch64-linux-gnu" +``` + +如果 `SDK/github_source/static_lib_v0.0.4` 缺失或版本不匹配,会下载 `sdk_bsp.tar.gz`。这个 sysroot 给交叉编译提供: + +- 设备侧系统库。 +- FreeType、libpng、libcamera、libjpeg 等头文件和库。 +- 交叉链接所需的 `libstdc++.so.6` 等运行库参照。 + +### 8.4 验证产物架构 + +编译完成后: + +```bash +file dist/M5CardputerZero-APPLaunch +``` + +期望看到类似: + +```text +ELF 64-bit LSB executable, ARM aarch64 +``` + +检查动态依赖名称: + +```bash +aarch64-linux-gnu-readelf -d dist/M5CardputerZero-APPLaunch | grep NEEDED +``` + +如果需要在开发机查看符号或段信息: + +```bash +aarch64-linux-gnu-readelf -h dist/M5CardputerZero-APPLaunch +aarch64-linux-gnu-objdump -p dist/M5CardputerZero-APPLaunch | grep NEEDED +``` + +## 9. macOS 交叉编译到设备 + +### 9.1 编译命令 + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk +scons -j8 +``` + +`mac_cross_cp0_config_defaults.mk` 使用: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-unknown-linux-gnu-" +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +### 9.2 macOS 链接路径补充 + +`main/SConstruct` 对 `mac_cross_cp0_config_defaults.mk` 做了额外处理: + +- 添加 FreeType 和 libpng include:`$CONFIG_TOOLCHAIN_SYSROOT/usr/include/freetype2`、`libpng16`。 +- 添加 libcamera include,优先使用 `pkg-config --cflags libcamera`,失败时回退到 `$CONFIG_TOOLCHAIN_SYSROOT/usr/include/libcamera`。 +- 链接 `$CONFIG_TOOLCHAIN_SYSROOT/usr/lib/aarch64-linux-gnu/libstdc++.so.6`。 +- 追加 `-Wl,-rpath-link,...` 和 `-B...`,帮助 macOS 上的交叉 linker 找到 sysroot 内的 Linux 库。 + +### 9.3 macOS 常见注意事项 + +- Homebrew 在 Apple Silicon 上通常位于 `/opt/homebrew`,在 Intel Mac 上通常位于 `/usr/local`。如果工具链不在 PATH 中,需要手动追加。 +- 如果 `pkg-config` 找不到 `libcamera`,脚本会 fallback,但 sysroot 内仍必须存在实际头文件和库。 +- 生成的文件是 Linux arm64 ELF,不能在 macOS 本机直接运行。 + +验证: + +```bash +file dist/M5CardputerZero-APPLaunch +``` + +期望是 `ARM aarch64` Linux ELF,而不是 Mach-O。 + +## 10. SCons 关键逻辑 + +### 10.1 顶层 `projects/APPLaunch/SConstruct` + +该文件负责构建入口和全局环境准备: + +1. 定义 SDK 路径: + +```text +sdk_path = projects/APPLaunch/../../SDK +``` + +2. 根据环境变量选择默认配置: + +```text +CardputerZero=y -> linux_x86_cross_cp0_config_defaults.mk +x86_64 且未设置 CONFIG_DEFAULT_FILE -> linux_x86_sdl2_config_defaults.mk +``` + +3. 交叉编译时生成 `build/config/config_tmp.mk`,补充 sysroot。 + +4. 设置: + +```text +SDK_PATH +EXT_COMPONENTS_PATH +``` + +5. 调用 SDK 构建系统: + +```python +SConscript(str(sdk_path / "tools" / "scons" / "project.py"), variant_dir=os.getcwd(), duplicate=0) +``` + +6. 交叉编译时检查并下载 `static_lib_v0.0.4`。 + +### 10.2 SDK `project.py` + +SDK 构建系统做以下事情: + +1. 处理特殊命令:`menuconfig`、`clean`、`distclean`、`save`、`SET_CROSS`、`push`。 +2. 调用 Kconfig 工具生成 `global_config.mk` 和 `global_config.h`。 +3. 从 `global_config.mk` 把 `CONFIG_...` 变量加载进环境变量。 +4. 建立 SCons 编译环境和工具链前缀。 +5. 扫描 SDK 组件目录和 `ext_components` 目录。 +6. 加载 `projects/APPLaunch/main/SConstruct` 注册主工程组件。 +7. 编译静态库、共享库和可执行文件。 +8. 把可执行文件和 `STATIC_FILES` 复制到 `dist`。 + +### 10.3 `projects/APPLaunch/main/SConstruct` + +该文件注册 APPLaunch 主程序组件: + +- 执行 `ui/components/generate_page_app_includes.py`,生成内置页面 include 聚合文件。 +- 读取当前 git 短 hash,注入编译宏 `LAUNCHER_GIT_COMMIT_RAW`。 +- 收集 `src/*.c*` 和整个 `ui` 目录源码。 +- 添加 include:`main`、`main/include`、`ext_components/cp0_lvgl/include`、`SDK/components/utilities/include`。 +- 依赖组件:`cp0_lvgl`、`eventpp`、`lvgl_component`、`pthread`、`Miniaudio`。 +- 可选依赖:`Backward_cpp`。 +- 按不同配置文件追加 SDL2、FreeType、libinput、xkbcommon、udev、libcamera、jpeg 等依赖。 +- 通过 `wget_github('https://github.com/jgromes/RadioLib.git')` 拉取 RadioLib,并直接编译 SX1262 相关源码。 +- 把 `../APPLaunch` 资源树和 `doc/store_cache_sync.py` 加入 `STATIC_FILES`。 +- 注册项目 target:`M5CardputerZero-APPLaunch`。 + +## 11. 常用 SCons 命令 + +| 命令 | 作用 | +| --- | --- | +| `scons -j8` | 使用 8 个并行任务构建 | +| `scons -c` | 清理 SCons 已知目标产物 | +| `scons distclean` | 删除 `build`、`dist`、`.sconsign.dblite`、`.config*` 等配置和产物 | +| `scons menuconfig` | 打开 Kconfig 菜单并重新生成配置 | +| `scons save` | 把当前 `build/config/global_config.mk` 保存回 `CONFIG_DEFAULT_FILE` 指向的配置文件 | +| `scons push` | 按 `setup.ini` 通过 SSH 上传 `dist` | + +切换目标时推荐流程: + +```bash +scons distclean +export CONFIG_DEFAULT_FILE=目标配置文件 +scons -j8 +``` + +不要只改 `CONFIG_DEFAULT_FILE` 后直接 `scons -j8`,因为旧的 `build/config/global_config.mk` 可能已经存在,SDK 构建系统不会自动重新生成配置。 + +## 12. `menuconfig` 使用建议 + +运行: + +```bash +cd projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons menuconfig +``` + +`menuconfig` 会基于 `CONFIG_DEFAULT_FILE` 和临时配置生成最终配置。修改后会输出到: + +```text +build/config/global_config.mk +build/config/global_config.h +``` + +如果确认要固化修改: + +```bash +scons save +``` + +注意:`scons save` 会写回配置文件。多人协作时不要随意保存到共享的 `*_config_defaults.mk`,除非这是本次任务明确要求的变更。 + +## 13. 常见错误和处理 + +### 13.1 `scons: command not found` + +原因:未安装 SCons 或 Python 用户 bin 目录不在 PATH。 + +处理: + +```bash +python3 -m pip install --user scons +python3 -m scons --version +``` + +如果 `python3 -m scons` 可用,也可以这样构建: + +```bash +python3 -m scons -j8 +``` + +### 13.2 `ModuleNotFoundError: No module named 'parse'` + +原因:缺少 Python 包。 + +处理: + +```bash +python3 -m pip install --user parse requests tqdm paramiko scp +``` + +虚拟环境中请先 `source .venv/bin/activate`。 + +### 13.3 `Package sdl2 was not found in the pkg-config search path` + +原因:Linux SDL2 仿真依赖未安装,或 `PKG_CONFIG_PATH` 未包含 SDL2 `.pc` 文件目录。 + +处理: + +```bash +sudo apt install -y libsdl2-dev pkg-config +pkg-config --cflags sdl2 +``` + +macOS: + +```bash +brew install sdl2 pkg-config +pkg-config --cflags sdl2 +``` + +### 13.4 `Package freetype2 was not found` + +处理: + +```bash +sudo apt install -y libfreetype6-dev +pkg-config --cflags freetype2 +``` + +macOS: + +```bash +brew install freetype pkg-config +pkg-config --cflags freetype2 +``` + +### 13.5 `aarch64-linux-gnu-gcc: not found` + +原因:Linux 交叉工具链未安装,或 PATH 不包含工具链。 + +处理: + +```bash +sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +aarch64-linux-gnu-gcc --version +``` + +macOS 交叉编译应使用 `aarch64-unknown-linux-gnu-gcc`,对应配置文件是 `mac_cross_cp0_config_defaults.mk`。 + +### 13.6 下载 `sdk_bsp.tar.gz` 失败 + +原因:首次交叉编译需要下载 `static_lib_v0.0.4`,网络不可用或 GitHub 访问失败。 + +处理: + +1. 确认网络能访问 GitHub release。 +2. 重新执行 `scons -j8`。 +3. 离线环境手工准备: + +```text +SDK/github_source/static_lib_v0.0.4/ +└── version # 内容应为 v0.0.4 +``` + +如果目录存在但版本不匹配,顶层 `SConstruct` 仍会尝试更新。 + +### 13.7 `libcamera` 头文件或库找不到 + +交叉编译配置中,`main/SConstruct` 会添加: + +```text +$CONFIG_TOOLCHAIN_SYSROOT/usr/include/libcamera +-lcamera -lcamera-base -ljpeg +``` + +处理: + +```bash +ls SDK/github_source/static_lib_v0.0.4/usr/include/libcamera +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu | grep camera +``` + +如果缺失,需要更新 sysroot 包或安装设备侧开发库后重新制作 sysroot。 + +### 13.8 链接时报 `cannot find -linput`、`-lxkbcommon`、`-ludev` + +本机 SDL2 构建:安装开发包。 + +```bash +sudo apt install -y libinput-dev libxkbcommon-dev libudev-dev +``` + +交叉编译:检查 sysroot: + +```bash +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libinput.* +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libxkbcommon.* +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libudev.* +``` + +### 13.9 切换配置后仍然使用旧后端 + +原因:`build/config/global_config.mk` 已经存在,构建系统不会因为你改了环境变量就自动重建配置。 + +处理: + +```bash +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +检查最终配置: + +```bash +grep -E 'LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk +``` + +### 13.10 SDL2 运行黑屏或资源缺失 + +常见原因:没有从 `dist` 目录运行,导致 `CONFIG_V9_5_LV_FS_POSIX_PATH="./"` 指向错误位置。 + +处理: + +```bash +cd projects/APPLaunch/dist +ls APPLaunch/share/images +./M5CardputerZero-APPLaunch +``` + +### 13.11 设备运行提示资源文件不存在 + +设备配置资源路径是: + +```text +/usr/share/APPLaunch/ +``` + +检查: + +```bash +ls /usr/share/APPLaunch/share/images +ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +如果是手动部署,请确保复制的是 `dist/APPLaunch` 的内容,而不是只复制可执行文件。 + +### 13.12 RadioLib 下载失败 + +`main/SConstruct` 使用 `wget_github('https://github.com/jgromes/RadioLib.git')` 获取 RadioLib。首次构建可能需要网络。 + +处理: + +- 确认网络可访问 GitHub。 +- 检查 `SDK/github_source` 下是否已有 RadioLib 缓存。 +- 在离线环境提前准备对应源码缓存。 + +## 14. 推荐构建流程 + +### 14.1 日常 UI 开发 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch +``` + +### 14.2 生成设备正式产物 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +然后进入第 09 章执行 `.deb` 打包、安装和 systemd 验证。 + +### 14.3 快速确认构建目标 + +```bash +grep CONFIG_DEFAULT_FILE /proc/$$/environ 2>/dev/null || true +grep -E 'CONFIG_TOOLCHAIN_PREFIX|LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk +file dist/M5CardputerZero-APPLaunch +``` diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" new file mode 100644 index 00000000..c936be4f --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" @@ -0,0 +1,901 @@ +# 09 - 打包部署与 systemd + +本章说明 APPLaunch 如何从 `dist` 目录打包为 Debian `.deb`,如何部署到 M5CardputerZero,如何通过 systemd 自启动,以及如何验证和排查部署问题。 + +所有命令默认从仓库根目录开始: + +```bash +cd /home/nihao/w2T/github/launcher +``` + +## 1. 部署形态总览 + +APPLaunch 的设备端运行依赖两类文件: + +1. 主程序:`M5CardputerZero-APPLaunch`。 +2. 运行时资源树:`APPLaunch/`,包含应用描述、字体、图片、音频、脚本和可选子应用。 + +正式安装后的目标路径是: + +```text +/usr/share/APPLaunch/ +├── applications/ +├── bin/ +│ ├── M5CardputerZero-APPLaunch +│ ├── M5CardputerZero-AppStore # 如果 dist/bin 中存在则打包 +│ ├── M5CardputerZero-Calculator # 如果 dist/bin 中存在则打包 +│ └── appstore.py # 如果 dist/bin 中存在则打包 +├── lib/ +├── share/ +│ ├── font/ +│ └── images/ +└── cache -> /var/cache/APPLaunch # postinst 创建 +``` + +systemd 服务文件安装到: + +```text +/lib/systemd/system/APPLaunch.service +``` + +服务启动命令是: + +```text +/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +工作目录是: + +```text +/usr/share/APPLaunch +``` + +## 2. 打包前必须先完成设备目标构建 + +`.deb` 应该使用 arm64 设备产物,而不是 Linux SDL2 x86_64 仿真产物。 + +推荐在 Linux x86_64 开发机交叉编译: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +`file` 结果应包含: + +```text +ARM aarch64 +``` + +如果看到 `x86-64`,说明你打包的是 SDL2 本机产物,不能安装到设备作为正式 launcher。 + +设备端原生编译也可以用于打包: + +```bash +cd /home/pi/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=config_defaults.mk +scons -j2 +file dist/M5CardputerZero-APPLaunch +``` + +## 3. `llm_pack.py` 打包脚本说明 + +打包脚本位于: + +```text +projects/APPLaunch/tools/llm_pack.py +``` + +核心常量: + +| 常量 | 值 | 说明 | +| --- | --- | --- | +| `PACKAGE_NAME` | `applaunch` | Debian 包名 | +| `APP_NAME` | `APPLaunch` | 应用名和服务名基础 | +| `BIN_NAME` | `M5CardputerZero-APPLaunch` | 主可执行文件名 | +| `INSTALL_PPREFIX` | `usr/share` | 安装前缀上层目录 | +| `INSTALL_PREFIX` | `usr/share/APPLaunch` | 应用安装根目录 | +| `BIN_PATH` | `usr/share/APPLaunch/bin` | 可执行文件目录 | +| `LIB_PATH` | `usr/share/APPLaunch/lib` | 动态库目录 | +| `SHARE_PATH` | `usr/share/APPLaunch/share` | 共享资源目录 | +| `APP_PATH` | `usr/share/APPLaunch/applications` | `.desktop` 应用描述目录 | +| `SERVICE_PATH` | `lib/systemd/system` | systemd 服务目录 | + +默认版本信息在脚本入口处: + +```python +version = '0.2.1' +src_folder = '../dist' +revision = 'm5stack1' +``` + +生成的包文件名格式: + +```text +applaunch_0.2.1-m5stack1_arm64.deb +``` + +## 4. `.deb` 包目录结构 + +运行脚本后会在 `projects/APPLaunch/tools` 下生成临时目录: + +```text +projects/APPLaunch/tools/debian-APPLaunch/ +├── DEBIAN/ +│ ├── control +│ ├── postinst +│ └── prerm +├── lib/ +│ └── systemd/ +│ └── system/ +│ └── APPLaunch.service +└── usr/ + └── share/ + └── APPLaunch/ + ├── applications/ + ├── bin/ + │ └── M5CardputerZero-APPLaunch + ├── lib/ + └── share/ + ├── font/ + └── images/ +``` + +最终 `.deb` 文件位于: + +```text +projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +``` + +## 5. 打包命令 + +### 5.1 安装打包工具 + +Linux 开发机: + +```bash +sudo apt update +sudo apt install -y dpkg-dev fakeroot +``` + +只要有 `dpkg-deb` 即可: + +```bash +dpkg-deb --version +``` + +### 5.2 执行打包 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +python3 llm_pack.py +``` + +成功时会看到类似: + +```text +Creating Debian package applaunch 0.2.1 ... +Debian package created: .../applaunch_0.2.1-m5stack1_arm64.deb +applaunch create success! +``` + +### 5.3 指定自定义版本 + +当前脚本入口固定使用 `0.2.1` 和 `m5stack1`。如果需要临时打自定义版本,可以直接从 Python 调用函数,不修改仓库文件: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +python3 - <<'PY' +from llm_pack import create_applaunch_deb +print(create_applaunch_deb(version='0.2.1', src_folder='../dist', revision='m5stack1')) +PY +``` + +如果要长期改变版本号,应通过正式代码变更修改 `llm_pack.py`,并同步记录发布说明。 + +### 5.4 清理打包产物 + +脚本支持: + +```bash +python3 llm_pack.py clean +python3 llm_pack.py distclean +``` + +差异: + +| 命令 | 行为 | +| --- | --- | +| `clean` | 删除当前目录下 `*.deb`,并删除当前目录一级子目录 | +| `distclean` | 删除当前目录下 `*.deb` 和 `m5stack_*` | + +注意:`clean` 会删除 `tools` 目录下的一级目录,包括 `debian-APPLaunch` 这类临时目录。不要在放有重要子目录的非预期目录执行。 + +## 6. 打包脚本复制规则 + +### 6.1 主程序查找 + +脚本从 `src_folder` 查找主程序,默认 `src_folder='../dist'`。 + +查找顺序: + +1. `../dist/M5CardputerZero-APPLaunch` +2. `../dist/bin/M5CardputerZero-APPLaunch` + +如果两个位置都不存在,会抛出: + +```text +FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist +``` + +### 6.2 附加应用和后端 + +脚本会尝试包含以下可选文件: + +```text +../dist/bin/M5CardputerZero-AppStore +../dist/bin/appstore.py +../dist/bin/M5CardputerZero-Calculator +``` + +如果存在则复制到: + +```text +/usr/share/APPLaunch/bin/ +``` + +其中非 `.py` 文件会设置为 `0755`。 + +### 6.3 资源树复制 + +脚本优先复制源码中的资源树: + +```text +projects/APPLaunch/APPLaunch +``` + +目标是包内: + +```text +usr/share/APPLaunch +``` + +如果源码资源树不存在,则尝试使用: + +```text +../dist/APPLaunch +``` + +这意味着打包时通常不只依赖 `dist/APPLaunch`,也会把工程源码目录中的 `APPLaunch/` 资源树复制进去。 + +### 6.4 AppStore 图片补充 + +如果存在: + +```text +projects/AppStore/share/images +``` + +脚本会把以下图片复制到包内 `usr/share/APPLaunch/share/images`: + +```text +store_wordmark.png +store_arrow_*.png +``` + +## 7. Debian 控制脚本 + +### 7.1 `DEBIAN/control` + +打包脚本生成的 control 包含: + +```text +Package: applaunch +Version: 0.2.1 +Architecture: arm64 +Maintainer: dianjixz +Original-Maintainer: m5stack +Section: APPLaunch +Priority: optional +Homepage: https://www.m5stack.com +Packaged-Date: <打包时间> +Description: M5CardputerZero APPLaunch +``` + +重要点: + +- `Architecture` 固定为 `arm64`。 +- 脚本不自动声明 `Depends`,因此依赖库需要由基础镜像提供,或在后续版本中补充依赖声明。 + +### 7.2 `DEBIAN/postinst` + +安装后脚本执行: + +```sh +mkdir -p /var/cache/APPLaunch +ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl enable APPLaunch.service +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl start APPLaunch.service +exit 0 +``` + +作用: + +- 创建可写缓存目录 `/var/cache/APPLaunch`。 +- 在只读/系统资源目录下建立 `cache` 软链接。 +- 启用并启动 systemd 服务。 + +注意:如果 `/usr/share/APPLaunch/cache` 已存在,`ln -s` 可能报错。当前脚本没有使用 `ln -sfn`,重复安装时需要留意安装日志。 + +### 7.3 `DEBIAN/prerm` + +卸载前脚本执行: + +```sh +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl stop APPLaunch.service +[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl disable APPLaunch.service +rm -rf /var/cache/APPLaunch +exit 0 +``` + +作用: + +- 停止服务。 +- 禁用开机自启动。 +- 删除缓存目录。 + +注意:卸载会删除 `/var/cache/APPLaunch`,其中若存有运行时缓存或应用商店缓存,会一并清除。 + +## 8. systemd 服务文件 + +脚本生成: + +```ini +[Unit] +Description=APPLaunch Service + +[Service] +ExecStart=/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +WorkingDirectory=/usr/share/APPLaunch +Restart=always +RestartSec=1 +StartLimitInterval=0 + +[Install] +WantedBy=multi-user.target +``` + +字段说明: + +| 字段 | 说明 | +| --- | --- | +| `ExecStart` | 启动 APPLaunch 主程序 | +| `WorkingDirectory` | 设置当前目录为 `/usr/share/APPLaunch`,方便相对路径访问 | +| `Restart=always` | 进程退出后总是重启 | +| `RestartSec=1` | 退出 1 秒后重启 | +| `StartLimitInterval=0` | 关闭默认启动频率限制,避免频繁崩溃后 systemd 停止重启 | +| `WantedBy=multi-user.target` | enable 后随多用户目标启动 | + +当前服务文件没有显式设置用户,默认以 systemd system service 的 root 身份运行。这通常有利于访问 framebuffer、evdev、GPIO、音频和相机设备,但也意味着程序权限较高。 + +## 9. 安装到设备 + +### 9.1 复制 `.deb` 到设备 + +假设设备 IP 是 `192.168.28.177`,用户名是 `pi`: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +``` + +### 9.2 在设备上安装 + +```bash +ssh pi@192.168.28.177 +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +如果安装过程中提示缺少依赖,先修复依赖: + +```bash +sudo apt-get -f install +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +### 9.3 覆盖安装 + +再次安装同名或更高版本包: + +```bash +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +如果服务正在运行,`postinst` 会尝试 enable/start。为了减少安装期间 framebuffer 或输入设备占用问题,可以手动先停服务: + +```bash +sudo systemctl stop APPLaunch.service || true +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +sudo systemctl restart APPLaunch.service +``` + +## 10. 使用 `scons push` 快速部署 + +除了 `.deb`,工程还支持通过 `setup.ini` 上传 `dist` 目录。 + +配置文件: + +```text +projects/APPLaunch/setup.ini +``` + +默认内容示例: + +```ini +[ssh] +local_file_path = dist +remote_file_path = /home/pi/dist +remote_host = 192.168.28.177 +remote_port = 22 +username = pi +password = pi +; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' +; after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +``` + +执行: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons push +``` + +`SDK/tools/scons/push.py` 会: + +1. 读取 `setup.ini`。 +2. 遍历 `local_file_path` 下所有文件。 +3. 计算本地 MD5。 +4. 通过 SSH 获取远端文件 MD5。 +5. 只上传有变化的文件。 +6. 可选执行 `before_cmd` 和 `after_cmd`。 + +适用场景: + +- 开发阶段快速替换 `dist`。 +- 快速上传单次编译结果。 +- 不需要测试 Debian 安装脚本时。 + +不适用场景: + +- 验证正式安装路径。 +- 验证 `postinst`、`prerm`。 +- 验证 systemd enable/install 行为。 +- 需要生成可分发安装包。 + +## 11. 手动部署方式 + +当不想使用 `.deb`,也不想使用 `scons push`,可以手动复制。 + +在开发机上传: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scp dist/M5CardputerZero-APPLaunch pi@192.168.28.177:/home/pi/ +scp -r dist/APPLaunch pi@192.168.28.177:/home/pi/APPLaunch-new +``` + +在设备上安装: + +```bash +sudo systemctl stop APPLaunch.service || true +sudo mkdir -p /usr/share/APPLaunch/bin +sudo install -m 0755 /home/pi/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +sudo rsync -a --delete /home/pi/APPLaunch-new/ /usr/share/APPLaunch/ +sudo mkdir -p /var/cache/APPLaunch +sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +sudo systemctl daemon-reload +sudo systemctl restart APPLaunch.service +``` + +如果服务文件尚未安装,可以手动创建 `/lib/systemd/system/APPLaunch.service`,内容参考第 8 节。 + +## 12. 部署验证命令 + +### 12.1 包状态 + +```bash +dpkg -l | grep applaunch +dpkg -s applaunch +``` + +查看包安装了哪些文件: + +```bash +dpkg -L applaunch +``` + +查看 `.deb` 包内容但不安装: + +```bash +dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb +``` + +查看 `.deb` 元信息: + +```bash +dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb +``` + +### 12.2 文件和权限 + +```bash +ls -l /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +file /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +ls -ld /usr/share/APPLaunch +ls -l /usr/share/APPLaunch/cache +ls -l /var/cache/APPLaunch +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | head +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | head +``` + +期望: + +- 主程序有执行权限。 +- 主程序架构为 `ARM aarch64`。 +- `/usr/share/APPLaunch/cache` 指向 `/var/cache/APPLaunch`。 +- 图片和字体资源存在。 + +### 12.3 动态库依赖 + +在设备上: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +检查是否有缺失: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +``` + +如果缺少库,需要安装对应系统包,或补充打包规则把私有库放入 `/usr/share/APPLaunch/lib` 并配置运行时搜索路径。 + +### 12.4 systemd 状态 + +```bash +systemctl status APPLaunch.service --no-pager +systemctl is-enabled APPLaunch.service +systemctl is-active APPLaunch.service +``` + +查看日志: + +```bash +journalctl -u APPLaunch.service -b --no-pager +journalctl -u APPLaunch.service -b -f +``` + +重启: + +```bash +sudo systemctl restart APPLaunch.service +``` + +停止: + +```bash +sudo systemctl stop APPLaunch.service +``` + +开机自启动: + +```bash +sudo systemctl enable APPLaunch.service +``` + +取消开机自启动: + +```bash +sudo systemctl disable APPLaunch.service +``` + +重新读取服务文件: + +```bash +sudo systemctl daemon-reload +``` + +### 12.5 手动前台运行 + +排查 systemd 前,建议先前台运行: + +```bash +sudo systemctl stop APPLaunch.service || true +cd /usr/share/APPLaunch +sudo ./bin/M5CardputerZero-APPLaunch +``` + +这样可以直接看到标准输出和崩溃信息。如果前台运行正常但 systemd 不正常,再检查服务文件、权限和工作目录。 + +### 12.6 framebuffer 和输入设备 + +检查 framebuffer: + +```bash +ls -l /dev/fb* +cat /sys/class/graphics/fb0/name 2>/dev/null || true +``` + +检查输入设备: + +```bash +ls -l /dev/input/ +cat /proc/bus/input/devices +``` + +检查当前谁占用 framebuffer 或输入设备: + +```bash +sudo fuser -v /dev/fb0 2>/dev/null || true +sudo fuser -v /dev/input/event* 2>/dev/null || true +``` + +如果另一个图形程序正在运行,APPLaunch 可能无法正确显示或读取输入。 + +## 13. 卸载和回滚 + +### 13.1 卸载 + +```bash +sudo dpkg -r applaunch +``` + +这会触发 `prerm`:停止服务、disable 服务、删除 `/var/cache/APPLaunch`。 + +如果要同时清理配置文件: + +```bash +sudo dpkg -P applaunch +``` + +### 13.2 安装旧包回滚 + +```bash +sudo systemctl stop APPLaunch.service || true +sudo dpkg -i /home/pi/applaunch_旧版本-m5stack1_arm64.deb +sudo systemctl restart APPLaunch.service +``` + +验证: + +```bash +dpkg -s applaunch | grep Version +systemctl status APPLaunch.service --no-pager +``` + +### 13.3 临时禁用 launcher + +```bash +sudo systemctl disable --now APPLaunch.service +``` + +恢复: + +```bash +sudo systemctl enable --now APPLaunch.service +``` + +## 14. 常见部署错误 + +### 14.1 安装时报 `package architecture (arm64) does not match system` + +原因:设备系统不是 arm64,或在 x86_64 开发机上直接安装了 arm64 包。 + +处理: + +```bash +uname -m +dpkg --print-architecture +``` + +`.deb` 应安装在 M5CardputerZero 设备上,而不是 Linux x86_64 开发机上。 + +### 14.2 设备运行时报 `Exec format error` + +原因:主程序架构错误。常见情况是把 Linux SDL2 x86_64 产物打进了 arm64 包。 + +检查: + +```bash +file /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +正确处理:重新交叉编译: + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +然后重新打包安装。 + +### 14.3 服务反复重启 + +检查: + +```bash +systemctl status APPLaunch.service --no-pager +journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +常见原因: + +- 缺少动态库。 +- 资源路径不存在。 +- framebuffer 或输入设备不可用。 +- 程序启动即崩溃。 +- 安装的是错误架构产物。 + +进一步检查: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +ls /usr/share/APPLaunch/share/images +ls /dev/fb0 +``` + +### 14.4 `ln: failed to create symbolic link '/usr/share/APPLaunch/cache': File exists` + +原因:重复安装时 `postinst` 使用 `ln -s`,目标已存在。 + +处理: + +```bash +sudo rm -rf /usr/share/APPLaunch/cache +sudo mkdir -p /var/cache/APPLaunch +sudo ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache +sudo systemctl restart APPLaunch.service +``` + +如果要从根本上修复,应修改打包脚本为 `ln -sfn`,但这属于代码变更。 + +### 14.5 `dpkg-deb: error: failed to open package info file .../DEBIAN/control` + +原因:打包目录结构不完整,或脚本中途失败后残留目录异常。 + +处理: + +```bash +cd projects/APPLaunch/tools +python3 llm_pack.py clean +python3 llm_pack.py +``` + +### 14.6 `FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist` + +原因:未构建,或构建目录不是 `projects/APPLaunch/dist`,或打包脚本不是从 `tools` 目录运行。 + +处理: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +ls -l dist/M5CardputerZero-APPLaunch +cd tools +python3 llm_pack.py +``` + +### 14.7 服务启动后黑屏 + +排查顺序: + +1. 确认可执行文件能前台运行。 +2. 确认 framebuffer 存在。 +3. 确认没有其它进程占用显示。 +4. 确认资源路径存在。 +5. 查看 journal 日志。 + +命令: + +```bash +sudo systemctl stop APPLaunch.service || true +cd /usr/share/APPLaunch +sudo ./bin/M5CardputerZero-APPLaunch +ls -l /dev/fb0 +sudo fuser -v /dev/fb0 2>/dev/null || true +journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +### 14.8 外部应用无法启动 + +APPLaunch 会从资源树和 `.desktop` 描述中找到外部应用。先检查: + +```bash +find /usr/share/APPLaunch/applications -maxdepth 1 -type f -print +find /usr/share/APPLaunch/bin -maxdepth 1 -type f -print +``` + +确认外部应用有执行权限: + +```bash +ls -l /usr/share/APPLaunch/bin +``` + +如果 `.desktop` 中的 Exec 指向不存在的路径,需要修正资源树或重新打包。 + +## 15. 发布前检查清单 + +打包前: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +打包后: + +```bash +cd tools +python3 llm_pack.py +dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb +dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 +``` + +安装后: + +```bash +dpkg -s applaunch | grep -E 'Package|Version|Architecture' +systemctl status APPLaunch.service --no-pager +systemctl is-enabled APPLaunch.service +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +ls -l /usr/share/APPLaunch/cache +journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +功能验证: + +- 设备开机后 APPLaunch 自动显示首页。 +- 键盘/按键输入可用。 +- 首页应用轮播可切换。 +- 资源图片和字体正常显示。 +- 内置页面可进入和返回。 +- 外部应用启动后能退出并回到 APPLaunch。 +- AppStore/Calculator 等可选子应用如已打包,能从 launcher 正常启动。 + +## 16. 推荐部署流程 + +正式发布建议使用: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +cd tools +python3 llm_pack.py +scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl status APPLaunch.service --no-pager' +``` + +开发阶段快速替换建议使用: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +scons push +``` + +两者区别:`.deb` 验证完整安装和 systemd 生命周期;`scons push` 更快,但不能替代正式打包验证。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" new file mode 100644 index 00000000..a380591d --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -0,0 +1,420 @@ +# 10 - 扩展开发指南 + +本章说明如何在 APPLaunch 中扩展功能,重点覆盖 4 类常见改动:新增内置页面、新增外部 `.desktop` 应用、新增图片/音频/字体资源、修改设置开关。涉及的核心代码位于 `projects/APPLaunch/main/ui`,平台适配和路径解析位于 `ext_components/cp0_lvgl`。 + +## 1. 扩展前先理解的几个入口 + +| 入口 | 作用 | +| --- | --- | +| `projects/APPLaunch/main/ui/Launch.cpp` | 固定应用列表、动态 `.desktop` 扫描、启动内置页面或外部进程 | +| `projects/APPLaunch/main/ui/components/page_app/` | 内置页面实现目录,页面通常为 header-only `.hpp` | +| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | `AppPage`、顶部栏、`img_path()`、`audio_path()` 等页面公共能力 | +| `projects/APPLaunch/main/ui/components/generate_page_app_includes.py` | 构建前自动生成 `page_app.h`,把 `page_app/*.hpp` 全部 include 进来 | +| `projects/APPLaunch/APPLaunch/` | 运行时资源树,打包后对应设备端 `/usr/share/APPLaunch/` | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | 设备端 `cp0_file_path()` 路径规则 | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 开发机 `cp0_file_path()` 路径规则 | +| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 设备端设置持久化,保存到 `/var/lib/applaunch/settings` | + +APPLaunch 的应用来源分两类: + +- **内置页面**:编译进 APPLaunch 进程,由 `app("NAME", icon, page_v)` 注册,进入时创建 `PageT` 对象并切换到它的 screen。 +- **外部应用**:通过固定 `Exec` 或 `.desktop` 描述启动独立进程,非终端应用会暂停 Launcher 的 LVGL 定时器,等待子进程退出后回到首页。 + +## 2. 新增内置页面 + +内置页面适合实现“和 Launcher 同进程、直接使用 LVGL、需要共享输入组/顶部栏/状态栏”的功能,例如设置、音乐、文件、相机、LoRa 页面。 + +### 2.1 新建页面文件 + +在 `projects/APPLaunch/main/ui/components/page_app/` 下新增一个 `.hpp`,命名建议使用 `ui_app_xxx.hpp`。页面类继承 `AppPage`,构造函数中设置标题、创建 UI、绑定按键事件。 + +最小骨架: + +```cpp +#pragma once + +#include "../ui_app_page.hpp" + +class UIMyToolPage : public AppPage +{ +public: + UIMyToolPage() : AppPage() + { + set_page_title("MY TOOL"); + create_ui(); + event_handler_init(); + } + +private: + lv_obj_t *title_ = nullptr; + + void create_ui() + { + lv_obj_t *root = screen(); + lv_obj_set_style_bg_color(root, lv_color_hex(0x101820), LV_PART_MAIN | LV_STATE_DEFAULT); + + UIAppTopBar top("MY TOOL"); + top.create(root); + + title_ = lv_label_create(root); + lv_label_set_text(title_, "Hello APPLaunch"); + lv_obj_center(title_); + } + + void event_handler_init() + { + lv_obj_add_event_cb(screen(), &UIMyToolPage::key_event_cb, LV_EVENT_KEY, this); + } + + static void key_event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + uint32_t key = lv_event_get_key(e); + if (key == LV_KEY_ESC && self->navigate_home) { + self->navigate_home(); + } + } +}; +``` + +注意事项: + +- 页面必须继承 `AppPage`,这样才能复用 `screen()`、`input_group()`、`navigate_home` 等机制。 +- 返回首页优先调用 `navigate_home()`,不要直接 `lv_disp_load_scr(ui_Screen1)`,否则 `LaunchImpl` 无法正确释放当前页面对象。 +- 页面内如果创建 LVGL timer、文件描述符、线程或外设句柄,要在析构函数里释放。 +- 页面尺寸以 320x170 为基准;常见布局是顶部栏 20px,正文 320x150。 +- 资源路径不要硬编码绝对路径;图片用 `img_path("xxx.png")`,音频用 `audio_path("xxx.wav")`。 + +### 2.2 确认页面会被 include + +`projects/APPLaunch/main/SConstruct` 在构建前运行: + +```python +ui/components/generate_page_app_includes.py +``` + +该脚本会扫描 `projects/APPLaunch/main/ui/components/page_app/*.hpp`,生成 `projects/APPLaunch/main/ui/components/page_app.h`。通常只要文件后缀是 `.hpp`,构建时就会自动被 include。 + +如果你手动检查,`page_app.h` 中应出现: + +```cpp +#include "page_app/ui_app_my_tool.hpp" +``` + +### 2.3 在首页应用列表注册 + +打开 `projects/APPLaunch/main/ui/Launch.cpp`,找到 `LaunchImpl::LaunchImpl()`。内置页面注册方式如下: + +```cpp +app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); +``` + +推荐放在 `APP_ENABLED` 控制区,方便后续通过设置页开关控制是否显示: + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("MyTool")) + app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); + +#undef APP_ENABLED +``` + +注册规则: + +- 第一个参数是首页轮播显示名,建议短一些,避免在小屏上被截断。 +- 第二个参数是图标路径,通常使用 `img_path("xxx_100.png")`。 +- 第三个参数 `page_v` 表示点击时创建内置页面。 +- 如果页面只支持设备端硬件,可以放在 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 内,避免 SDL2 构建失败。 + +### 2.4 添加设置页开关 + +如果希望设置页的 `Launcher` 菜单可以控制新页面显示,需要修改 `UISetupPage::menu_init()` 中的 `app_keys` 和 `app_labels`。 + +示例: + +```cpp +static const char *app_keys[] = { + "Python", "Store", "CLI", "Game", "Setting", + "Music", "Math", "MyTool" +}; + +static const char *app_labels[] = { + "Python", "Store", "CLI", "Game", "Setting", + "Music", "Math", "My Tool" +}; +``` + +`save_app_toggle()` 会把开关保存为 `app_`,例如 `app_MyTool=0`。`Launch.cpp` 里用相同 key 读取: + +```cpp +cp0_config_get_int("app_MyTool", 1) +``` + +设备端持久化文件是: + +```text +/var/lib/applaunch/settings +``` + +### 2.5 构建验证 + +SDL2 本机验证: + +```bash +cd projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed +./dist/M5CardputerZero-APPLaunch +``` + +设备交叉编译验证: + +```bash +cd projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed +``` + +如果是设备端专用页面,至少要跑交叉编译;如果页面也能在开发机显示,建议先用 SDL2 快速验证 UI 和按键。 + +## 3. 新增外部 `.desktop` 应用 + +外部 `.desktop` 适合“独立可执行程序、脚本或终端命令”。它不需要改 C++ 应用列表,APPLaunch 会扫描 `applications` 目录并动态加入首页。 + +### 3.1 放置 `.desktop` 文件 + +开发树路径: + +```text +projects/APPLaunch/APPLaunch/applications/ +``` + +设备安装后路径: + +```text +/usr/share/APPLaunch/applications/ +``` + +已有模板: + +```text +projects/APPLaunch/APPLaunch/applications/vim.desktop.temple +``` + +注意当前扫描逻辑只处理文件名以 `.desktop` 结尾的文件,`.desktop.temple` 只是模板,不会被加载。 + +### 3.2 `.desktop` 字段格式 + +最小示例: + +```ini +[Desktop Entry] +Name=Vim +Exec=vim +Terminal=true +Icon=share/images/email.png +Type=Application +``` + +APPLaunch 当前解析的字段: + +| 字段 | 是否必需 | 说明 | +| --- | --- | --- | +| `Name` | 是 | 首页显示名 | +| `Exec` | 是 | 要执行的命令,可为绝对路径或 shell 命令 | +| `Icon` | 否 | 图标路径,建议写 `share/images/xxx.png` 或可被 LVGL 读取的路径 | +| `Terminal` | 否 | `true`/`True`/`1` 表示在内置 `UIConsolePage` 中运行 | +| `Sysplause` | 否 | 仅终端应用使用,控制终端页命令结束后的暂停行为,默认 true | +| `Type` | 否 | 兼容桌面文件习惯,当前 APPLaunch 不依赖该字段 | +| `TryExec` | 否 | 当前 APPLaunch 不解析,只能作为说明字段 | + +示例 1:启动终端命令。 + +```ini +[Desktop Entry] +Name=TOP +Exec=top +Terminal=true +Sysplause=false +Icon=share/images/cli_100.png +Type=Application +``` + +示例 2:启动独立程序。 + +```ini +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/my_app +Terminal=false +Icon=share/images/my_app_100.png +Type=Application +``` + +示例 3:启动脚本。 + +```ini +[Desktop Entry] +Name=NetInfo +Exec=/bin/sh /usr/share/APPLaunch/bin/netinfo.sh +Terminal=true +Icon=share/images/ip_panel_100.png +Type=Application +``` + +### 3.3 外部应用启动行为 + +`Launch.cpp` 对外部应用有两种启动方式: + +- `Terminal=true`:创建 `UIConsolePage`,在 APPLaunch 进程内显示 PTY 终端,执行 `Exec`。 +- `Terminal=false`:调用 `cp0_process_exec_blocking()` 启动外部进程,APPLaunch 暂停 LVGL timer 和输入组,等子进程退出后恢复首页。 + +非终端外部应用返回首页依赖以下行为: + +- 子进程正常退出,APPLaunch 会恢复 `ui_Screen1`。 +- 设备端长按 ESC 约 3 秒会向外部应用进程组发送 SIGTERM,若 3 秒未退出再 SIGKILL。 +- `cp0_process_exec_blocking()` 会暂停 Launcher 键盘线程,让外部程序直接读取 evdev 输入。 + +### 3.4 动态刷新 + +APPLaunch 启动时会调用 `applications_load()` 扫描 `.desktop` 文件;之后 `inotify`/SDL 目录监听会每 3 秒检查一次应用目录变化。新增、删除或修改 `.desktop` 后,正常情况下无需重启 Launcher,轮播列表会自动刷新。 + +如果没有刷新: + +```bash +# 设备端 +ls -l /usr/share/APPLaunch/applications +journalctl -u APPLaunch.service -f + +# SDL2 开发机,在运行目录附近确认 APPLaunch/applications 是否存在 +find projects/APPLaunch -path '*APPLaunch/applications*' -maxdepth 5 -type f +``` + +### 3.5 去重规则 + +动态应用按 `Exec` 去重。如果两个 `.desktop` 的 `Exec` 完全相同,后扫描到的文件会被跳过,并打印: + +```text +applications_load: skip ... (duplicate Exec) +``` + +## 4. 新增资源 + +资源分为图片、音频、字体和外部程序/脚本。开发树统一放在 `projects/APPLaunch/APPLaunch/` 下,构建时 `main/SConstruct` 通过 `STATIC_FILES += [ADir('../APPLaunch')]` 把它复制到输出/安装包。 + +### 4.1 资源目录 + +| 类型 | 开发树路径 | 设备端路径 | 推荐访问方式 | +| --- | --- | --- | --- | +| 图片 | `projects/APPLaunch/APPLaunch/share/images/` | `/usr/share/APPLaunch/share/images/` | `img_path("xxx.png")` 或 `.desktop` 的 `Icon=share/images/xxx.png` | +| 音频 | `projects/APPLaunch/APPLaunch/share/audio/` | `/usr/share/APPLaunch/share/audio/` | `audio_path("xxx.wav")` | +| 字体 | `projects/APPLaunch/APPLaunch/share/font/` | `/usr/share/APPLaunch/share/font/` | `launcher_fonts().get("xxx.ttf", size, style)` | +| 外部应用 | `projects/APPLaunch/APPLaunch/bin/` | `/usr/share/APPLaunch/bin/` | `.desktop` 的 `Exec=/usr/share/APPLaunch/bin/xxx` | +| `.desktop` | `projects/APPLaunch/APPLaunch/applications/` | `/usr/share/APPLaunch/applications/` | 自动扫描 | + +如果新增 `bin/` 目录或脚本,确保脚本有执行权限,或在 `.desktop` 中通过 `/bin/sh script.sh` 调用。 + +### 4.2 `cp0_file_path()` 路径规则 + +设备端 `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` 的关键规则: + +- `cp0_file_path("applications")` -> `/usr/share/APPLaunch/applications` +- `cp0_file_path("lock_file")` -> `/tmp/M5CardputerZero-APPLaunch_fcntl.lock` +- 图片扩展名 `png/gif/jpg/jpeg/svg` -> `share/images/` +- 音频扩展名 `wav/mp3/ogg` -> `/usr/share/APPLaunch/share/audio/` +- 字体扩展名 `ttf/otf` -> `/usr/share/APPLaunch/share/font/` + +SDL2 端 `sdl_lvgl_file.cpp` 会根据可执行文件目录、当前工作目录及 `APPLaunch/share` 自动推断资源根目录,并把路径转换为相对当前工作目录的路径,便于开发机运行。 + +### 4.3 图片资源建议 + +- 首页轮播图标建议提供 100px 版本,例如 `mytool_100.png`。 +- 如果页面内部需要小图标,可提供 80px 或更小版本。 +- LVGL 读取图片时对路径和格式比较敏感;先复用仓库现有 PNG 命名和尺寸风格最稳妥。 +- 如果 `panel_set_icon()` 打印 `missing/unreadable`,先检查文件是否存在于运行时资源树,而不是只存在于源码目录。 + +### 4.4 音频资源建议 + +页面内播放按键音可参考 `UISetupPage`: + +```cpp +std::string snd_enter_ = audio_path("key_enter.wav"); +cp0_signal_audio_api({"PlayFile", snd_enter_}, nullptr); +``` + +设备端音频通常使用 `/usr/share/APPLaunch/share/audio/xxx.wav`,SDL2 端由路径适配层解析。 + +### 4.5 字体资源建议 + +字体放在 `share/font/`,页面里优先通过公共字体缓存获取,避免重复创建: + +```cpp +lv_font_t *font = launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD); +lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +新增字体后要同时验证 SDL2 和设备端 FreeType 配置是否开启。构建配置中 SDL2 和交叉编译都会为 LVGL 加入 FreeType 相关 include/link 参数。 + +## 5. 修改设置开关 + +设置页集中在 `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`。当前设置包括 Launcher 应用显示开关、Boot、Screen、WiFi、Speaker、Camera、Info、About、Help、ExtPort 等。 + +### 5.1 新增 Launcher 应用开关 + +步骤: + +1. 在 `UISetupPage::menu_init()` 的 `app_keys` 增加内部 key,例如 `MyTool`。 +2. 在相同位置的 `app_labels` 增加显示名,例如 `My Tool`。 +3. 在 `Launch.cpp` 注册应用时使用相同 key:`APP_ENABLED("MyTool")`。 +4. 运行设置页,进入 `Launcher` 菜单,切换 O/X。 +5. 返回首页后如列表未刷新,可重启 APPLaunch;当前固定/内置列表是在 `LaunchImpl` 构造时读取配置。 + +### 5.2 新增普通设置项 + +在 `menu_init()` 中找到对应分组,向 `sub_items` 加一项: + +```cpp +{"My Option", true, cp0_config_get_int("my_option", 1) != 0, [this]() { + bool en = cp0_config_get_int("my_option", 1) == 0; + cp0_config_set_int("my_option", en ? 1 : 0); + cp0_config_save(); +}}, +``` + +如果是进入二级/三级页面选择值,可参考现有实现: + +- `enter_brightness_adjust()`:亮度选择。 +- `enter_darktime_adjust()`:息屏时间选择。 +- `enter_volume_adjust()` 和 `apply_volume()`:音量保存并应用。 +- `enter_camera_resolution()`:相机分辨率。 +- `enter_startup_mode()`:启动模式。 + +### 5.3 配置持久化位置 + +设备端配置实现:`ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`。 + +- 配置目录:`/var/lib/applaunch` +- 配置文件:`/var/lib/applaunch/settings` +- 格式:一行一个 `key=value` +- 最大条目:`MAX_ENTRIES=32` + +常用命令: + +```bash +sudo cat /var/lib/applaunch/settings +sudo sed -i 's/^app_Music=.*/app_Music=1/' /var/lib/applaunch/settings +sudo systemctl restart APPLaunch.service +``` + +如果新增大量配置,注意当前最大条目数是 32;超过后 `cp0_config_set_*` 会直接返回,设置不会保存。 + +## 6. 扩展时的验证清单 + +| 检查项 | 方法 | +| --- | --- | +| 文件只放在正确目录 | 内置页面放 `main/ui/components/page_app/`,资源放 `APPLaunch/share/`,`.desktop` 放 `APPLaunch/applications/` | +| SDL2 能编译 | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | +| 设备交叉编译能过 | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | +| 图标能显示 | 观察日志是否有 `set panel icon missing/unreadable` | +| 页面能返回首页 | 内置页按 ESC 调用 `navigate_home()`;外部页按 ESC 长按或自身退出 | +| `.desktop` 被加载 | 文件名以 `.desktop` 结尾,包含 `[Desktop Entry]`、`Name`、`Exec` | +| 设置能保存 | 检查 `/var/lib/applaunch/settings` 是否写入对应 key | diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" new file mode 100644 index 00000000..76b5687f --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" @@ -0,0 +1,513 @@ +# 11 - 调试与故障排查 + +本章面向 APPLaunch 开发和设备部署过程中的常见问题。建议先用 SDL2 版本复现 UI/资源/输入逻辑,再用设备端日志定位 framebuffer、evdev、权限和 systemd 问题。 + +## 1. 常用调试命令 + +### 1.1 查看仓库和构建状态 + +```bash +cd /home/nihao/w2T/github/launcher + +git status --short +find docs/launcher工程详细说明 -maxdepth 1 -type f | sort +find projects/APPLaunch/APPLaunch -maxdepth 3 -type f | sort | sed -n '1,160p' +``` + +### 1.2 SDL2 本机编译运行 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed +./dist/M5CardputerZero-APPLaunch +``` + +用途: + +- 快速验证首页、内置页面、轮播动画、`.desktop` 扫描。 +- 检查 LVGL 对象创建和资源路径。 +- 避开设备端 framebuffer、evdev、systemd 权限问题。 + +### 1.3 设备端/交叉编译 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed +``` + +如果在设备上原生构建: + +```bash +cd /path/to/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed +``` + +### 1.4 查看 APPLaunch 运行日志 + +如果通过 systemd 启动: + +```bash +sudo systemctl status APPLaunch.service --no-pager +sudo journalctl -u APPLaunch.service -b --no-pager +sudo journalctl -u APPLaunch.service -f +``` + +如果手动运行设备端二进制: + +```bash +sudo systemctl stop APPLaunch.service +cd /usr/share/APPLaunch +sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch 2>&1 | tee /tmp/applaunch.log +``` + +实际二进制路径取决于打包脚本和安装方式,也可能位于构建输出 `projects/APPLaunch/dist/M5CardputerZero-APPLaunch`。 + +### 1.5 检查运行时资源 + +```bash +ls -l /usr/share/APPLaunch +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | sort | sed -n '1,120p' +find /usr/share/APPLaunch/share/audio -maxdepth 1 -type f | sort +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort +find /usr/share/APPLaunch/applications -maxdepth 1 -type f | sort +``` + +### 1.6 检查输入设备 + +```bash +ls -l /dev/input/by-path/ +ls -l /dev/input/event* +sudo evtest +``` + +代码默认键盘设备: + +```text +/dev/input/by-path/platform-3f804000.i2c-event +``` + +可通过环境变量覆盖: + +```bash +APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +### 1.7 检查配置文件 + +```bash +sudo ls -l /var/lib/applaunch +sudo cat /var/lib/applaunch/settings +``` + +常见配置 key: + +- `app_Music`、`app_Math`、`app_File`、`app_Camera` 等:Launcher 页面显示开关。 +- `brightness`:亮度。 +- `volume`:音量。 +- `dark_time`:息屏时间。 +- `cam_resolution`:相机分辨率。 +- `startup_mode`:启动模式。 +- `extport_usb`、`extport_5vout`:扩展口设置。 +- `run_as_user`:外部进程降权运行用户。 + +## 2. 日志关键词速查 + +| 关键词 | 位置 | 含义 | +| --- | --- | --- | +| `[BOOT] lv_init() done` | `main.cpp` | LVGL 初始化完成 | +| `[BOOT] cp0_lvgl_init() starting...` | `main.cpp` | 开始初始化平台适配层、显示、输入、音频等 | +| `[BOOT] First frame flushed to fb0.` | `main.cpp` | 首帧已强制刷新到显示设备 | +| `Entering main loop` | `main.cpp` | 主循环已进入 | +| `[LAUNCHER] set panel icon` | `Launch.cpp` | 首页图标设置成功 | +| `set panel icon missing/unreadable` | `Launch.cpp` | 图标路径不存在或不可读 | +| `applications_load: opendir failed` | `Launch.cpp` | applications 目录不存在或不可读 | +| `missing Name or Exec` | `Launch.cpp` | `.desktop` 缺少必需字段 | +| `duplicate Exec` | `Launch.cpp` | `.desktop` 与已有应用 Exec 重复 | +| `Launching terminal app` | `Launch.cpp` | 进入内置终端页面运行命令 | +| `Launching external app` | `Launch.cpp` | 启动非终端外部程序 | +| `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | 外部应用运行期间父进程读到 ESC | +| `[cp0] Returned to launcher` | `cp0_app_process.cpp` | 外部应用退出,准备回到首页 | +| `[HOME_STATUS] connected=` | `Launch.cpp` | 首页状态栏刷新 WiFi/电量 | + +## 3. 黑屏排查 + +黑屏要先判断是“进程没起来”“LVGL 没刷首帧”“资源/页面构造崩溃”“外部应用占住 framebuffer”还是“背光/亮度问题”。 + +### 3.1 快速判断进程状态 + +```bash +pgrep -a M5CardputerZero-APPLaunch +sudo systemctl status APPLaunch.service --no-pager +sudo journalctl -u APPLaunch.service -b --no-pager | tail -120 +``` + +如果没有进程: + +- 检查 systemd unit 的 `ExecStart` 路径是否存在。 +- 检查二进制是否有执行权限。 +- 手动运行二进制查看 stderr。 + +如果进程反复重启: + +```bash +sudo journalctl -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' +``` + +### 3.2 检查启动日志阶段 + +日志停在不同阶段,处理方向不同: + +| 停止位置 | 可能原因 | 排查方向 | +| --- | --- | --- | +| 没有 `[BOOT] lv_init()` | 程序未执行或很早崩溃 | systemd、二进制路径、动态库、权限 | +| 停在 `cp0_lvgl_init() starting` | 显示/输入/音频/硬件初始化卡住 | framebuffer、evdev、音频设备、硬件 HAL | +| 有 `ui_init done` 但黑屏 | 首帧刷新失败、背光为 0、资源导致对象不可见 | framebuffer、背光、资源路径 | +| 进入 main loop 后黑屏 | 页面绘制异常或外部应用锁住显示 | 日志、锁文件、外部进程 | + +### 3.3 检查 framebuffer 和背光 + +```bash +ls -l /dev/fb0 +id +sudo cat /sys/class/backlight/backlight/brightness +sudo cat /sys/class/backlight/backlight/max_brightness +``` + +尝试提高亮度: + +```bash +echo 80 | sudo tee /sys/class/backlight/backlight/brightness +``` + +如果设置页曾把亮度保存为很低,检查: + +```bash +sudo grep '^brightness=' /var/lib/applaunch/settings +``` + +### 3.4 检查是否被外部应用占住 + +APPLaunch 启动非终端外部应用时会暂停自身 LVGL timer,并等待子进程退出。若外部应用卡死,可能看起来像 Launcher 黑屏或无法响应。 + +```bash +ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|Calculator|AppStore|my_app' | grep -v grep +``` + +可先温和结束外部应用进程组,或长按 ESC 约 3 秒让 `cp0_process_exec_blocking()` 触发 SIGTERM。 + +### 3.5 用 SDL2 缩小范围 + +如果 SDL2 版本正常、设备端黑屏,优先查设备 HAL、framebuffer、背光、evdev、权限和 systemd。若 SDL2 也黑屏,优先查 UI 构造、资源路径和 LVGL 对象样式。 + +## 4. 资源缺失排查 + +资源缺失常见表现:图标空白、背景不显示、字体回退、音效没有声音、日志出现 `missing/unreadable`。 + +### 4.1 检查源目录和运行目录是否都有资源 + +```bash +# 源码树 +find projects/APPLaunch/APPLaunch/share/images -maxdepth 1 -type f | sort | grep my_icon + +# 设备端 +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | sort | grep my_icon +``` + +如果源码树有、设备端没有: + +- 重新构建/打包/安装。 +- 检查 `projects/APPLaunch/main/SConstruct` 中 `STATIC_FILES += [ADir('../APPLaunch')]` 是否仍然存在。 +- 检查打包脚本是否把 `APPLaunch/` 资源树复制进包。 + +### 4.2 检查路径写法 + +内置页面推荐: + +```cpp +img_path("my_icon_100.png") +audio_path("key_enter.wav") +``` + +`.desktop` 推荐: + +```ini +Icon=share/images/my_icon_100.png +``` + +不要在设备端页面里直接写开发机绝对路径,例如 `/home/nihao/.../projects/APPLaunch/...`。设备安装后资源根目录是 `/usr/share/APPLaunch/`。 + +### 4.3 理解图片路径的特殊性 + +设备端 `cp0_file_path("xxx.png")` 返回 `share/images/xxx.png`,这是相对当前运行目录的路径;如果手动从非预期目录启动设备端二进制,图片可能找不到。推荐在 `/usr/share/APPLaunch` 作为工作目录运行,或使用 systemd 的正确 `WorkingDirectory`。 + +SDL2 端会自动探测 `APPLaunch/share`,但仍建议从 `projects/APPLaunch` 目录运行构建产物。 + +### 4.4 字体缺失 + +检查字体文件: + +```bash +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort +``` + +如果新增字体导致崩溃或不显示: + +- 确认扩展名是 `.ttf` 或 `.otf`。 +- 确认 FreeType 构建依赖可用。 +- 先用已有字体 `Montserrat-Bold.ttf` 或 `AlibabaPuHuiTi-3-55-Regular.ttf` 验证页面逻辑。 + +## 5. 输入无效排查 + +输入无效分为首页不响应、内置页面不响应、外部应用不响应、按键码映射错误。 + +### 5.1 首页或内置页面不响应 + +检查是否绑定了正确输入组: + +- 首页:`UILaunchPage::bind_home_input_group()`。 +- 内置页面:`Launch.cpp` 创建页面后调用 `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())`。 +- 返回首页:`LaunchImpl::lv_go_back_home()` 会重新绑定首页 input group。 + +内置页面新增事件时,要确保事件挂在正确对象上,并且对象属于页面 input group。可参考已有页面的 `event_handler_init()`。 + +### 5.2 设备端 evdev 是否有事件 + +```bash +ls -l /dev/input/by-path/platform-3f804000.i2c-event +sudo evtest /dev/input/by-path/platform-3f804000.i2c-event +``` + +如果默认路径不存在,临时覆盖: + +```bash +APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +### 5.3 按键码映射问题 + +相关文件: + +| 文件 | 作用 | +| --- | --- | +| `ext_components/cp0_lvgl/include/compat/input_keys.h` | 兼容输入 key 定义 | +| `projects/APPLaunch/main/include/keyboard_input.h` | APPLaunch 私有输入头 | +| `ext_components/cp0_lvgl/include/keyboard_input.h` | cp0_lvgl 输入接口 | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | 设备端键盘输入实现 | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | SDL2 键盘输入实现 | + +排查方法: + +- SDL2 中先确认方向键、Enter、Esc 是否触发预期。 +- 设备端用 `evtest` 读原始 key code。 +- 对照 `LV_KEY_*` 和工程里的自定义键值。 +- 外部应用期间查看 `[CP0-APP] evdev code=... value=...` 日志。 + +### 5.4 外部应用输入无效 + +非终端外部应用启动后,APPLaunch 会调用 `keyboard_pause()` 暂停自身键盘线程,但不会 EVIOCGRAB 设备;父进程和子进程都可以读同一个 evdev。若外部应用没有输入: + +- 确认外部应用读取的是同一个 `/dev/input/event*`。 +- 确认运行用户有权限读该 input 设备;外部应用默认可能从 root 降权到普通用户。 +- 检查 `run_as_user` 配置或使用内置固定应用的 `run_as_root` 注册方式。 +- 用 `Terminal=true` 先验证命令是否能在 PTY 里接收键盘输入。 + +## 6. 外部应用无法返回排查 + +外部应用无法返回通常由子进程不退出、进程组未被杀掉、输入设备无法读到 ESC、或应用本身接管显示后未恢复导致。 + +### 6.1 正常返回路径 + +`Launch.cpp` 的 `launch_Exec()`: + +1. 显示 Loading。 +2. `LVGL_RUN_FLAGE = 0`。 +3. 解绑 LVGL 输入组。 +4. `lv_timer_enable(false)` 暂停 LVGL timer。 +5. 调用 `cp0_process_exec_blocking(exec, &LVGL_HOME_KEY_FLAG, keep_root)`。 +6. 子进程退出后重新启用 timer、绑定首页 input group、加载 `ui_Screen1`、隐藏 Loading。 + +### 6.2 先确认子进程是否还在 + +```bash +ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|my_app|sh -c' | grep -v grep +``` + +如果子进程还在: + +- 让应用自身退出。 +- 长按 ESC 约 3 秒。 +- 查看日志是否出现 `[cp0] ESC held ... SIGTERM pgid ...`。 + +### 6.3 ESC 长按无效 + +检查: + +```bash +sudo journalctl -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' +``` + +如果没有 `[CP0-APP] evdev` 日志: + +- 默认键盘路径可能错误。 +- 父进程没有 input 设备权限。 +- 外部应用或其它进程独占了输入设备。 + +如果有 ESC DOWN 但没有 SIGTERM: + +- 按住时长不足,当前阈值是 3 秒。 +- key code 不是 `KEY_ESC`,需要检查键盘映射。 + +### 6.4 子进程退出但首页没恢复 + +查日志是否有: + +```text +[cp0] Returned to launcher +App ... exited with code ... +``` + +如果有返回日志但屏幕仍异常: + +- 外部应用可能改了 framebuffer 状态或终端模式。 +- 尝试切回首页后强制刷新已由 `lv_obj_invalidate()` 和 `lv_refr_now()` 执行;如果仍不显示,检查 framebuffer/背光。 +- 检查外部应用是否留下 lock 或后台进程继续占用显示。 + +## 7. 构建失败排查 + +### 7.1 SCons 找不到 SDK 或组件 + +现象:找不到 `project.py`、组件、头文件。 + +排查: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +python3 - <<'PY' +from pathlib import Path +p = Path.cwd() +print('cwd=', p) +print('SDK=', p.parent.parent / 'SDK') +print('ext=', p.parent.parent / 'ext_components') +PY +ls ../../SDK/tools/scons/project.py +ls ../../ext_components/cp0_lvgl +``` + +APPLaunch 的 `SConstruct` 会自动设置: + +```python +SDK_PATH = ../../SDK +EXT_COMPONENTS_PATH = ../../ext_components +``` + +### 7.2 SDL2 / FreeType / libinput 依赖缺失 + +SDL2 配置会通过 `pkg-config` 查找 `sdl2`、`freetype2`,并链接 `input`、`xkbcommon`、`udev`。 + +检查: + +```bash +pkg-config --cflags --libs sdl2 freetype2 +ldconfig -p | grep -E 'libinput|libxkbcommon|libudev' +``` + +Ubuntu/Debian 常见依赖名: + +```bash +sudo apt-get install scons pkg-config libsdl2-dev libfreetype-dev libinput-dev libxkbcommon-dev libudev-dev +``` + +### 7.3 交叉编译 sysroot 缺失 + +`linux_x86_cross_cp0_config_defaults.mk` 会使用 `SDK/github_source/static_lib_v0.0.4` 作为 sysroot。如果不存在,`SConstruct` 会尝试下载 `sdk_bsp.tar.gz`。网络受限时会失败。 + +检查: + +```bash +ls -l ../../SDK/github_source/static_lib_v0.0.4 +cat ../../SDK/github_source/static_lib_v0.0.4/version 2>/dev/null || true +``` + +如果下载失败,需要提前准备 sysroot,或在有网络环境完成一次构建缓存。 + +### 7.4 新增页面后编译失败 + +常见原因: + +| 现象 | 原因 | 处理 | +| --- | --- | --- | +| `PageT not declared` | 页面类名和注册名不一致,或 `.hpp` 未被 `page_app.h` include | 检查 `page_app.h`,重新运行 scons | +| SDL2 构建找不到 Linux 头 | 页面直接 include 设备端专用头 | 用 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 包住设备端代码 | +| 链接找不到符号 | 新页面调用的函数未加入组件依赖 | 检查 `main/SConstruct` 的 `REQUIREMENTS`/`LDFLAGS` | +| 重复定义 | header-only 页面里定义了非 inline 全局变量/函数 | 改成类成员、`static`、`inline`,或移入 `.cpp` | + +### 7.5 `page_app.h` 自动生成导致工作区变化 + +`generate_page_app_includes.py` 会按文件名排序生成 `page_app.h`。新增/删除 `page_app/*.hpp` 后,构建可能修改该文件。这是预期行为,但提交前要确认 diff 是否只包含预期 include 列表变化。 + +## 8. `.desktop` 加载失败排查 + +### 8.1 文件未被扫描 + +检查: + +```bash +ls -l /usr/share/APPLaunch/applications +``` + +要求: + +- 文件名必须以 `.desktop` 结尾。 +- 内容必须包含 `[Desktop Entry]`。 +- 至少有 `Name=` 和 `Exec=`。 +- 空行、`#`、`;` 注释会被跳过。 + +### 8.2 应用被去重跳过 + +如果日志出现: + +```text +applications_load: skip ... (duplicate Exec) +``` + +说明已有固定应用或另一个 `.desktop` 使用了相同 `Exec`。修改 `Exec` 为唯一命令即可。 + +### 8.3 图标不显示 + +`.desktop` 的 `Icon` 不会自动调用 `img_path()`,它按字段原样传给 `panel_set_icon()`。因此建议写: + +```ini +Icon=share/images/my_app_100.png +``` + +如果写绝对路径,也必须确保设备端文件存在且可读。 + +### 8.4 命令执行失败 + +终端应用先用命令行验证: + +```bash +which vim +vim --version +``` + +非终端应用检查: + +```bash +ls -l /usr/share/APPLaunch/bin/my_app +ldd /usr/share/APPLaunch/bin/my_app +sudo -u pi /usr/share/APPLaunch/bin/my_app +``` + +如果 APPLaunch 以 root 启动,外部应用默认会尝试降权到普通用户。需要 root 权限的应用要么通过固定内置注册使用 `run_as_root`,要么调整程序权限/组权限,避免无必要 root。 + +## 9. 故障定位推荐顺序 + +1. `git status --short` 确认当前改动范围。 +2. SDL2 构建运行,排除基础 UI/语法问题。 +3. 检查资源是否存在于 `projects/APPLaunch/APPLaunch` 和设备 `/usr/share/APPLaunch`。 +4. 查看 `journalctl -u APPLaunch.service -f`,定位启动阶段。 +5. 用 `evtest` 验证输入设备和按键码。 +6. 用 `ps` 检查外部应用和进程组。 +7. 检查 `/var/lib/applaunch/settings`,排除设置开关、亮度、运行用户导致的问题。 +8. 最后再看 HAL 层:`ext_components/cp0_lvgl/src/cp0/` 中 framebuffer、keyboard、process、settings、audio 等实现。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" new file mode 100644 index 00000000..624546c0 --- /dev/null +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" @@ -0,0 +1,215 @@ +# 12 - 常用修改入口速查 + +本章按“我要改什么”整理 APPLaunch 的常用入口。修改前请先确认当前工作区状态,避免覆盖其他 agent 的改动: + +```bash +cd /home/nihao/w2T/github/launcher +git status --short +``` + +## 1. 高频任务入口表 + +| 任务 | 主要文件/目录 | 关键点 | 验证方式 | +| --- | --- | --- | --- | +| 新增内置页面 | `projects/APPLaunch/main/ui/components/page_app/` | 新建 `ui_app_xxx.hpp`,继承 `AppPage` | SDL2 编译并打开页面 | +| 注册内置页面到首页 | `projects/APPLaunch/main/ui/Launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | 首页轮播出现图标 | +| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/Launch.cpp` | 设置页写 `app_Key`,Launcher 读 `APP_ENABLED("Key")` | 切换设置后重启或刷新首页 | +| 新增外部 `.desktop` 应用 | `projects/APPLaunch/APPLaunch/applications/` | 文件名必须以 `.desktop` 结尾,包含 `Name` 和 `Exec` | 日志无 skip,首页出现应用 | +| 新增图标 | `projects/APPLaunch/APPLaunch/share/images/` | 内置页用 `img_path()`,`.desktop` 用 `Icon=share/images/xxx.png` | 日志无 `missing/unreadable` | +| 新增音效 | `projects/APPLaunch/APPLaunch/share/audio/` | 页面用 `audio_path()` 和 `cp0_signal_audio_api()` | 设备端播放声音 | +| 新增字体 | `projects/APPLaunch/APPLaunch/share/font/` | 用 `launcher_fonts().get()`,确认 FreeType 依赖 | 页面文字使用新字体 | +| 修改首页轮播布局 | `projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.h` | 5 个 slot、左右切换、中心卡片 | SDL2 查看动画和输入 | +| 修改轮播动画 | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | 卡片移动、缩放、透明度等动画 | SDL2 连续左右切换 | +| 修改首页状态栏 | `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/ui.c` | `update_home_status_bar()` 刷新 WiFi/时间/电量 | 看 `[HOME_STATUS]` 日志 | +| 修改设置页菜单 | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | `menu_init()` 增加 `MenuItem`/`SubItem` | 进入 SETTING 页面测试 | +| 修改配置保存逻辑 | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 当前保存到 `/var/lib/applaunch/settings`,最多 32 项 | 查看 settings 文件 | +| 修改资源路径规则 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | 设备端和 SDL2 要同步考虑 | SDL2 + 设备端都检查资源 | +| 修改外部应用启动/返回 | `projects/APPLaunch/main/ui/Launch.cpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`、`cp0_process_exec_blocking()` | 外部应用启动、ESC 返回 | +| 修改终端应用 | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、命令执行、输入输出 | `Terminal=true` 应用验证 | +| 修改输入映射 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | 设备端和 SDL2 输入差异 | `evtest` + SDL2 键盘 | +| 修改启动流程 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、主循环 | 看 `[BOOT]` 日志 | +| 修改构建依赖 | `projects/APPLaunch/main/SConstruct` | `SRCS`、`INCLUDE`、`REQUIREMENTS`、`STATIC_FILES` | scons 编译 | +| 修改构建配置 | `projects/APPLaunch/*.mk` | SDL2、设备端、交叉编译不同配置 | 指定 `CONFIG_DEFAULT_FILE` 构建 | +| 修改打包内容 | `projects/APPLaunch/tools/llm_pack.py`、`projects/APPLaunch/APPLaunch/` | 资源树和安装路径 | 构建包后检查文件列表 | +| 修改平台 HAL | `ext_components/cp0_lvgl/src/cp0/`、`ext_components/cp0_lvgl/include/hal/` | framebuffer、音频、网络、设置、进程等 | 设备端实测 | + +## 2. 源码目录速查 + +| 路径 | 用途 | +| --- | --- | +| `projects/APPLaunch/main/src/main.cpp` | APPLaunch 进程入口、初始化顺序、主循环、外部应用锁检测 | +| `projects/APPLaunch/main/ui/ui.c` | LVGL 全局 UI 对象创建,多数 `ui_*` 全局对象来源于这里 | +| `projects/APPLaunch/main/ui/ui.cpp` | C++ UI 初始化桥接 | +| `projects/APPLaunch/main/ui/ui.h` | UI 全局声明、C/C++ 共享接口 | +| `projects/APPLaunch/main/ui/Launch.cpp` | 应用模型、应用列表、启动逻辑、动态 `.desktop` 加载、状态栏刷新 | +| `projects/APPLaunch/main/ui/Launch.h` | `Launch` 对外包装类 | +| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 screen、轮播 slot、输入事件、首页页面行为 | +| `projects/APPLaunch/main/ui/UILaunchPage.h` | 首页类接口,包含 panel/label/input group 访问函数 | +| `projects/APPLaunch/main/ui/ui_loading.cpp` | Loading 遮罩显示和隐藏 | +| `projects/APPLaunch/main/ui/ui_global_hint.cpp` | 全局提示浮层 | +| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | LVGL OS/线程辅助 | +| `projects/APPLaunch/main/ui/Animation/` | 首页轮播动画实现 | +| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | 内置页面基类、顶部栏、公共资源路径辅助 | +| `projects/APPLaunch/main/ui/components/page_app.h` | 自动生成的内置页面 include 汇总 | +| `projects/APPLaunch/main/ui/components/page_app/` | 内置页面实现目录 | +| `projects/APPLaunch/main/include/` | APPLaunch 私有头和兼容输入头 | + +## 3. 内置页面入口表 + +| 页面/功能 | 文件 | 注册名或图标 | 说明 | +| --- | --- | --- | --- | +| GAME | `projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | +| SETTING | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | 设置页,包含应用开关、亮度、音量、WiFi、相机等 | +| MUSIC | `projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | 音乐页 | +| Compass | `projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | 指南针页 | +| IP_PANEL | `projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP 信息面板,设备端启用 | +| FILE | `projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | 文件页,设备端启用 | +| SSH | `projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH 页面,设备端启用 | +| MESH | `projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh 页面,设备端启用 | +| REC | `projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | 录音页,设备端启用 | +| CAMERA | `projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | 相机页,设备端启用 | +| LORA | `projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa 页面,设备端启用 | +| TANK | `projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | 坦克游戏,设备端启用 | +| CLI/终端 | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`,用于 bash、python、Terminal=true 应用 | + +固定注册入口在 `LaunchImpl::LaunchImpl()`: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +## 4. 外部应用入口表 + +| 项目 | 路径/函数 | 说明 | +| --- | --- | --- | +| `.desktop` 目录 | `projects/APPLaunch/APPLaunch/applications/` | 开发树,打包后为 `/usr/share/APPLaunch/applications/` | +| 模板 | `projects/APPLaunch/APPLaunch/applications/vim.desktop.temple` | 示例模板;因后缀不是 `.desktop`,不会被扫描 | +| 扫描函数 | `LaunchImpl::applications_load()` in `projects/APPLaunch/main/ui/Launch.cpp` | 解析 `[Desktop Entry]`、`Name`、`Icon`、`Exec`、`Terminal`、`Sysplause` | +| 目录监听 | `LaunchImpl::inotify_init_watch()`、`app_dir_watch_cb()` | 监听 applications 变化并刷新动态应用列表 | +| 动态刷新 | `LaunchImpl::applications_reload()` | 保留固定应用,删除动态应用后重扫 | +| 终端启动 | `LaunchImpl::launch_Exec_in_terminal()` | 创建 `UIConsolePage` 并执行命令 | +| 非终端启动 | `LaunchImpl::launch_Exec()` | 暂停 LVGL,调用 `cp0_process_exec_blocking()` | +| 设备端进程执行 | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | fork、降权、ESC 长按退出、恢复键盘 | +| PTY 执行 | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | 终端页命令执行和用户选择 | + +`.desktop` 最小模板: + +```ini +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/my_app +Terminal=false +Icon=share/images/my_app_100.png +Type=Application +``` + +## 5. 资源入口表 + +| 资源 | 开发路径 | 访问方式 | 常见问题 | +| --- | --- | --- | --- | +| 首页/应用图标 | `projects/APPLaunch/APPLaunch/share/images/` | `img_path("xxx.png")` | 设备端相对路径依赖工作目录;`.desktop` 建议写 `share/images/xxx.png` | +| 页面图片 | `projects/APPLaunch/APPLaunch/share/images/` | `img_path("xxx.png")` 或 `cp0_file_path_c("xxx.png")` | 文件名大小写必须一致 | +| 音频 | `projects/APPLaunch/APPLaunch/share/audio/` | `audio_path("xxx.wav")` | 设备端音频路径为绝对 `/usr/share/APPLaunch/share/audio/` | +| 字体 | `projects/APPLaunch/APPLaunch/share/font/` | `launcher_fonts().get("xxx.ttf", size, style)` | 需要 FreeType,字体对象要缓存复用 | +| 外部二进制/脚本 | `projects/APPLaunch/APPLaunch/bin/` | `.desktop` 的 `Exec=/usr/share/APPLaunch/bin/xxx` | 注意执行权限和动态库依赖 | +| 外部应用描述 | `projects/APPLaunch/APPLaunch/applications/` | 自动扫描 `.desktop` | `.desktop.temple` 不会被扫描 | +| 随包库文件 | `projects/APPLaunch/APPLaunch/lib/` | 由程序或脚本自行加载 | 注意运行时 `LD_LIBRARY_PATH` 或 rpath | + +路径解析代码: + +| 平台 | 文件 | 重点 | +| --- | --- | --- | +| 设备端 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | 资源根固定为 `/usr/share/APPLaunch`,图片返回相对 `share/images/` | +| SDL2 | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | 从可执行文件目录、当前目录、父目录推断 `APPLaunch/share` | +| C 接口 | `cp0_file_path_c()` | 返回 thread-local 缓存的 `const char *`,适合 LVGL style API | +| C++ 接口 | `cp0_file_path()` | 返回 `std::string`,页面内推荐使用 | + +## 6. 设置和持久化入口表 + +| 设置项 | UI 入口 | 配置 key | 实现位置 | +| --- | --- | --- | --- | +| 应用显示开关 | SETTING -> Launcher | `app_` | `ui_app_setup.hpp` 的 `save_app_toggle()`,`Launch.cpp` 的 `APP_ENABLED()` | +| 亮度 | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | +| 息屏时间 | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | +| 音量 | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`、`cp0_volume_read/write()` | +| 相机分辨率 | SETTING -> Camera -> Resolution | `cam_resolution` | `ui_app_setup.hpp`、相机页面读取 | +| 启动模式 | 设置页相关选择 | `startup_mode` | `ui_app_setup.hpp` | +| USB 扩展口 | SETTING -> ExtPort | `extport_usb` | `ui_app_setup.hpp` | +| 5V 输出 | SETTING -> ExtPort | `extport_5vout` | `ui_app_setup.hpp` | +| 外部应用运行用户 | 手动配置 | `run_as_user` | `cp0_app_process.cpp`、`cp0_app_pty.cpp` | + +配置实现: + +| 文件 | 说明 | +| --- | --- | +| `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` | `cp0_config_get_int/set_int/get_str/set_str/save` 声明 | +| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 设备端配置读写,保存到 `/var/lib/applaunch/settings` | +| `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` | SDL2 兼容实现 | +| `ext_components/cp0_lvgl/src/commount.c` | 启动时应用保存的亮度和音量 | + +## 7. 构建入口表 + +| 场景 | 命令/文件 | 说明 | +| --- | --- | --- | +| 默认 SDL2 构建 | `projects/APPLaunch/SConstruct` 自动选择 `linux_x86_sdl2_config_defaults.mk` | x86_64 开发机默认配置 | +| 显式 SDL2 构建 | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | 推荐本机开发验证 | +| 交叉编译 | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | x86 Linux 到 AArch64 设备 | +| 设备原生构建 | `CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed` | 设备端 framebuffer/evdev 配置 | +| macOS 交叉 | `CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk scons ...` | macOS 到设备 | +| macOS/Darwin | `darwin_config_defaults.mk` | Darwin/SDL 相关配置 | +| 主构建脚本 | `projects/APPLaunch/SConstruct` | 设置 SDK、EXT_COMPONENTS、sysroot 下载 | +| 组件构建脚本 | `projects/APPLaunch/main/SConstruct` | 源码、依赖、静态文件、git commit 宏 | +| APPLaunch 配置 | `projects/APPLaunch/main/Kconfig` | 主工程 Kconfig | +| cp0_lvgl 配置 | `ext_components/cp0_lvgl/Kconfig` | 平台适配组件配置 | + +## 8. 平台适配入口表 + +| 能力 | 头文件 | 设备端实现 | SDL2/兼容实现 | +| --- | --- | --- | --- | +| LVGL 初始化 | `ext_components/cp0_lvgl/include/hal_lvgl_bsp.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c` | +| framebuffer/display | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c` | +| 键盘输入 | `ext_components/cp0_lvgl/include/keyboard_input.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | +| 文件路径 | `ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | +| 进程 | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp` | +| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp` | +| 音频 | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c` | +| 设置/亮度/音量 | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp` | +| 网络/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp` | +| 截图 | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp` | +| 相机 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp` | 设备端相机 | `ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp` | + +## 9. 调试命令速查 + +| 目的 | 命令 | +| --- | --- | +| 查看当前改动 | `git status --short` | +| SDL2 构建 | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | +| SDL2 运行 | `cd projects/APPLaunch && ./dist/M5CardputerZero-APPLaunch` | +| 交叉编译 | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | +| 查看 systemd 状态 | `sudo systemctl status APPLaunch.service --no-pager` | +| 跟踪日志 | `sudo journalctl -u APPLaunch.service -f` | +| 查看启动日志 | `sudo journalctl -u APPLaunch.service -b --no-pager` | +| 查资源 | `find /usr/share/APPLaunch -maxdepth 3 -type f | sort` | +| 查 `.desktop` | `find /usr/share/APPLaunch/applications -maxdepth 1 -type f -name '*.desktop' -print -exec sed -n '1,80p' {} \;` | +| 查设置 | `sudo cat /var/lib/applaunch/settings` | +| 查输入设备 | `ls -l /dev/input/by-path/ && sudo evtest` | +| 查外部应用进程 | `ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|sh -c|M5CardputerZero'` | +| 查动态库 | `ldd /usr/share/APPLaunch/bin/my_app` | +| 查图标日志 | `sudo journalctl -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | + +## 10. 改动前后检查清单 + +| 阶段 | 检查项 | +| --- | --- | +| 改动前 | `git status --short`,确认哪些文件已有他人改动 | +| 新增页面后 | 确认 `.hpp` 文件在 `page_app/`,类名与 `Launch.cpp` 注册一致 | +| 新增资源后 | 源码树和设备端 `/usr/share/APPLaunch` 都能找到文件 | +| 新增 `.desktop` 后 | 文件后缀是 `.desktop`,有 `[Desktop Entry]`、`Name`、`Exec` | +| 修改设置后 | `/var/lib/applaunch/settings` 写入正确 key,未超过配置条目上限 | +| 构建后 | SDL2 或交叉编译通过,没有非预期自动生成 diff | +| 设备运行后 | `journalctl` 中无 missing、skip、segfault、permission denied | +| 外部应用后 | 能正常退出或长按 ESC 返回首页 | From af4e863a179389fa6692d1bfc2f9190d8a922d68 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:34:43 +0800 Subject: [PATCH 32/70] Refactor launcher home to reuse app page base --- projects/APPLaunch/main/ui/UILaunchPage.cpp | 176 ++-------------- projects/APPLaunch/main/ui/UILaunchPage.h | 1 - .../main/ui/components/ui_app_page.hpp | 191 ++++++++++++++---- 3 files changed, 170 insertions(+), 198 deletions(-) diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index e090ac7d..69726931 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -493,6 +493,8 @@ std::string LauncherFonts::key(const char *ttf_name, uint16_t size, lv_freetype_ lv_group_t *UILaunchPage::home_input_group() { + if (active_launch_page) + return active_launch_page->input_group(); return ::home_input_group; } @@ -510,19 +512,13 @@ void UILaunchPage::bind_home_input_group() { lv_indev_t *indev = lv_indev_get_next(NULL); if (indev) { - lv_indev_set_group(indev, ::home_input_group); + lv_indev_set_group(indev, home_input_group()); } } void UILaunchPage::init_input_group() { - if (::home_input_group) { - bind_home_input_group(); - return; - } - - ::home_input_group = lv_group_create(); - lv_group_add_obj(::home_input_group, ui_Screen1); + ::home_input_group = input_group(); bind_home_input_group(); } @@ -591,164 +587,26 @@ void UILaunchPage::launch_selected_app() void UILaunchPage::create_screen() { - if (ui_Screen1) + if (!ui_Screen1) + ui_Screen1 = screen(); + if (!::ui_APP_Container) + ::ui_APP_Container = content_container(); + + if (carousel_elements[kCardCenter]) return; - ui_Screen1 = lv_obj_create(NULL); - lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Screen1, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - create_top(ui_Screen1); - create_app_container(ui_Screen1); - -} - -void UILaunchPage::create_top(lv_obj_t *parent) -{ -#ifdef APPLAUNCH_LOGO_USE_PNG - ui_Image1 = lv_img_create(parent); - lv_img_set_src(ui_Image1, cp0_file_path_c("launcher_brand_logo.png")); - lv_obj_set_width(ui_Image1, LV_SIZE_CONTENT); - lv_obj_set_height(ui_Image1, LV_SIZE_CONTENT); - lv_obj_set_x(ui_Image1, 5); - lv_obj_set_y(ui_Image1, 5); - lv_obj_add_flag(ui_Image1, LV_OBJ_FLAG_ADV_HITTEST); - lv_obj_clear_flag(ui_Image1, LV_OBJ_FLAG_SCROLLABLE); -#else - ui_Image1 = lv_label_create(parent); - lv_label_set_text(ui_Image1, "ZERO"); - lv_obj_set_x(ui_Image1, 5); - lv_obj_set_y(ui_Image1, 2); - lv_obj_set_style_text_font(ui_Image1, launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(ui_Image1, lv_color_hex(0xCCAA00), LV_PART_MAIN | LV_STATE_DEFAULT); -#endif - - // --- WiFi signal strength bars (4 bars, hidden when disconnected) --- - ui_wifiPanel = lv_obj_create(parent); - lv_obj_set_width(ui_wifiPanel, 24); - lv_obj_set_height(ui_wifiPanel, 15); - lv_obj_set_x(ui_wifiPanel, 210); - lv_obj_set_y(ui_wifiPanel, 4); - lv_obj_clear_flag(ui_wifiPanel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_flag(ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); - - ui_wifiBar1 = lv_obj_create(ui_wifiPanel); - lv_obj_set_width(ui_wifiBar1, 4); - lv_obj_set_height(ui_wifiBar1, 6); - lv_obj_set_align(ui_wifiBar1, LV_ALIGN_BOTTOM_LEFT); - lv_obj_set_x(ui_wifiBar1, 0); - lv_obj_clear_flag(ui_wifiBar1, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_wifiBar1, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar1, lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_wifiBar1, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_wifiBar1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_wifiBar2 = lv_obj_create(ui_wifiPanel); - lv_obj_set_width(ui_wifiBar2, 4); - lv_obj_set_height(ui_wifiBar2, 9); - lv_obj_set_align(ui_wifiBar2, LV_ALIGN_BOTTOM_LEFT); - lv_obj_set_x(ui_wifiBar2, 6); - lv_obj_clear_flag(ui_wifiBar2, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_wifiBar2, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar2, lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_wifiBar2, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_wifiBar2, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_wifiBar3 = lv_obj_create(ui_wifiPanel); - lv_obj_set_width(ui_wifiBar3, 4); - lv_obj_set_height(ui_wifiBar3, 12); - lv_obj_set_align(ui_wifiBar3, LV_ALIGN_BOTTOM_LEFT); - lv_obj_set_x(ui_wifiBar3, 12); - lv_obj_clear_flag(ui_wifiBar3, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_wifiBar3, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar3, lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_wifiBar3, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_wifiBar3, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_wifiBar4 = lv_obj_create(ui_wifiPanel); - lv_obj_set_width(ui_wifiBar4, 4); - lv_obj_set_height(ui_wifiBar4, 15); - lv_obj_set_align(ui_wifiBar4, LV_ALIGN_BOTTOM_LEFT); - lv_obj_set_x(ui_wifiBar4, 18); - lv_obj_clear_flag(ui_wifiBar4, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_wifiBar4, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar4, lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_wifiBar4, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_wifiBar4, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - // --- Time status icon --- - ui_Panel1 = lv_obj_create(parent); - lv_obj_set_width(ui_Panel1, 40); - lv_obj_set_height(ui_Panel1, 16); - lv_obj_set_x(ui_Panel1, 236); - lv_obj_set_y(ui_Panel1, 4); - lv_obj_clear_flag(ui_Panel1, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_Panel1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_Panel1, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Panel1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_Panel1, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_Panel1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_timeLabel = lv_label_create(ui_Panel1); - lv_obj_set_width(ui_timeLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_timeLabel, LV_SIZE_CONTENT); - lv_obj_set_align(ui_timeLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_timeLabel, "15:21"); - lv_obj_set_style_text_color(ui_timeLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_timeLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // --- Battery status icon --- - ui_batteryPanel = lv_obj_create(parent); - lv_obj_set_width(ui_batteryPanel, 36); - lv_obj_set_height(ui_batteryPanel, 16); - lv_obj_set_x(ui_batteryPanel, 280); - lv_obj_set_y(ui_batteryPanel, 4); - lv_obj_clear_flag(ui_batteryPanel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_batteryPanel, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_batteryPanel, cp0_file_path_c("status_battery_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_Bar1 = lv_bar_create(ui_batteryPanel); - lv_bar_set_value(ui_Bar1, 96, LV_ANIM_OFF); - lv_bar_set_start_value(ui_Bar1, 0, LV_ANIM_OFF); - lv_obj_set_width(ui_Bar1, 33); - lv_obj_set_height(ui_Bar1, 14); - lv_obj_set_align(ui_Bar1, LV_ALIGN_CENTER); - lv_obj_set_style_radius(ui_Bar1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_Bar1, lv_color_hex(0x484847), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Bar1, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_set_style_radius(ui_Bar1, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_Bar1, lv_color_hex(0x666633), LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_Bar1, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); - - ui_powerLabel = lv_label_create(ui_batteryPanel); - lv_obj_set_width(ui_powerLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_powerLabel, LV_SIZE_CONTENT); - lv_obj_set_align(ui_powerLabel, LV_ALIGN_CENTER); - lv_label_set_text(ui_powerLabel, "96%"); - lv_obj_set_style_text_color(ui_powerLabel, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(ui_powerLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + create_app_container(content_container()); } + void UILaunchPage::create_app_container(lv_obj_t *parent) { - ::ui_APP_Container = lv_obj_create(parent); - lv_obj_remove_style_all(::ui_APP_Container); - lv_obj_set_width(::ui_APP_Container, 320); - lv_obj_set_height(::ui_APP_Container, 150); - lv_obj_set_x(::ui_APP_Container, 0); - lv_obj_set_y(::ui_APP_Container, 10); - lv_obj_set_align(::ui_APP_Container, LV_ALIGN_CENTER); + ::ui_APP_Container = parent; + if (!::ui_APP_Container) + return; + + lv_obj_set_size(::ui_APP_Container, 320, 150); lv_obj_clear_flag(::ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); carousel_elements[kPageDot0] = lv_obj_create(::ui_APP_Container); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index c26af1e9..e3623a86 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -47,7 +47,6 @@ class UILaunchPage : public home_base static std::array carousel_elements; private: - static void create_top(lv_obj_t *parent); static void create_app_container(lv_obj_t *parent); std::shared_ptr launch_; diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index e8f59f1d..90420e37 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -293,8 +293,8 @@ class AppPageRoot public: std::string page_title_ = "APP"; - lv_group_t *input_group_; - lv_obj_t *root_screen_; + lv_group_t *input_group_ = nullptr; + lv_obj_t *root_screen_ = nullptr; lv_obj_t *screen() { return root_screen_; } lv_group_t *input_group() { return input_group_; } std::function navigate_home; @@ -308,8 +308,10 @@ class AppPageRoot } virtual ~AppPageRoot() { - lv_obj_del(root_screen_); - lv_group_delete(input_group_); + if (root_screen_) + lv_obj_del(root_screen_); + if (input_group_) + lv_group_delete(input_group_); } template @@ -474,15 +476,16 @@ class AppPageWithBottomBarLayout : virtual public AppTopBarRegion, virtual publi class home_base : public AppPageRoot { private: - lv_obj_t *ui_TOP_logo; - lv_obj_t *ui_TOP_time; - lv_obj_t *ui_TOP_time_Label; - lv_obj_t *ui_TOP_Power; - lv_obj_t *ui_TOP_power_Label; + lv_obj_t *ui_TOP_Container = nullptr; + lv_obj_t *ui_TOP_logo = nullptr; + lv_obj_t *ui_TOP_time = nullptr; + lv_obj_t *ui_TOP_time_Label = nullptr; + lv_obj_t *ui_TOP_Power = nullptr; + lv_obj_t *ui_TOP_power_Label = nullptr; lv_timer_t *status_timer_ = nullptr; public: - lv_obj_t *ui_APP_Container; + lv_obj_t *ui_APP_Container = nullptr; public: home_base() : AppPageRoot() @@ -496,6 +499,10 @@ class home_base : public AppPageRoot { if (status_timer_) lv_timer_delete(status_timer_); + if (::ui_Screen1 == root_screen_) + ::ui_Screen1 = nullptr; + if (::ui_APP_Container == ui_APP_Container) + ::ui_APP_Container = nullptr; } static void home_battery_event_cb(lv_event_t *e) @@ -517,9 +524,8 @@ class home_base : public AppPageRoot void update_status_bar() { - char time_buf[16]; - cp0_time_str(time_buf, sizeof(time_buf)); - lv_label_set_text(ui_TOP_time_Label, time_buf); + update_time_status(); + update_wifi_status(); } void update_battery_status(const cp0_battery_info_t &bat) @@ -546,26 +552,57 @@ class home_base : public AppPageRoot } } + lv_obj_t *content_container() const + { + return ui_APP_Container; + } + + lv_obj_t *top_container() const + { + return ui_TOP_Container; + } + private: /* ================================================================== */ /* UI construction */ /* ================================================================== */ void creat_Top_UI() { - ui_TOP_logo = lv_img_create(root_screen_); + ::ui_Screen1 = root_screen_; + + ui_TOP_Container = lv_obj_create(root_screen_); + lv_obj_remove_style_all(ui_TOP_Container); + lv_obj_set_size(ui_TOP_Container, 320, 20); + lv_obj_clear_flag(ui_TOP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + +#ifdef APPLAUNCH_LOGO_USE_PNG + ui_TOP_logo = lv_img_create(ui_TOP_Container); + ::ui_Image1 = ui_TOP_logo; lv_img_set_src(ui_TOP_logo, cp0_file_path_c("launcher_brand_logo.png")); - lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); /// 49 - lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); /// 12 + lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); + lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); lv_obj_set_x(ui_TOP_logo, 5); lv_obj_set_y(ui_TOP_logo, 5); - lv_obj_add_flag(ui_TOP_logo, LV_OBJ_FLAG_ADV_HITTEST); /// Flags - lv_obj_clear_flag(ui_TOP_logo, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_add_flag(ui_TOP_logo, LV_OBJ_FLAG_ADV_HITTEST); + lv_obj_clear_flag(ui_TOP_logo, LV_OBJ_FLAG_SCROLLABLE); +#else + ui_TOP_logo = lv_label_create(ui_TOP_Container); + ::ui_Image1 = ui_TOP_logo; + lv_label_set_text(ui_TOP_logo, "ZERO"); + lv_obj_set_x(ui_TOP_logo, 5); + lv_obj_set_y(ui_TOP_logo, 2); + lv_obj_set_style_text_font(ui_TOP_logo, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(ui_TOP_logo, lv_color_hex(0xCCAA00), LV_PART_MAIN | LV_STATE_DEFAULT); +#endif + + create_wifi_status(ui_TOP_Container); - ui_TOP_time = lv_obj_create(root_screen_); - lv_obj_set_width(ui_TOP_time, 45); + ui_TOP_time = lv_obj_create(ui_TOP_Container); + ::ui_Panel1 = ui_TOP_time; + lv_obj_set_width(ui_TOP_time, 40); lv_obj_set_height(ui_TOP_time, 16); - lv_obj_set_x(ui_TOP_time, 237); - lv_obj_set_y(ui_TOP_time, 5); + lv_obj_set_x(ui_TOP_time, 236); + lv_obj_set_y(ui_TOP_time, 4); lv_obj_clear_flag(ui_TOP_time, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_TOP_time, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -573,46 +610,124 @@ class home_base : public AppPageRoot lv_obj_set_style_bg_img_src(ui_TOP_time, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); ui_TOP_time_Label = lv_label_create(ui_TOP_time); - lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_time_Label, LV_SIZE_CONTENT); /// 1 + ::ui_timeLabel = ui_TOP_time_Label; + lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); + lv_obj_set_height(ui_TOP_time_Label, LV_SIZE_CONTENT); lv_obj_set_align(ui_TOP_time_Label, LV_ALIGN_CENTER); lv_label_set_text(ui_TOP_time_Label, "15:21"); lv_obj_set_style_text_color(ui_TOP_time_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_TOP_time_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_TOP_Power = lv_bar_create(root_screen_); + ::ui_batteryPanel = lv_obj_create(ui_TOP_Container); + lv_obj_set_width(::ui_batteryPanel, 36); + lv_obj_set_height(::ui_batteryPanel, 16); + lv_obj_set_x(::ui_batteryPanel, 280); + lv_obj_set_y(::ui_batteryPanel, 4); + lv_obj_clear_flag(::ui_batteryPanel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(::ui_batteryPanel, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(::ui_batteryPanel, cp0_file_path_c("status_battery_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + ui_TOP_Power = lv_bar_create(::ui_batteryPanel); + ::ui_Bar1 = ui_TOP_Power; lv_bar_set_value(ui_TOP_Power, 96, LV_ANIM_OFF); lv_bar_set_start_value(ui_TOP_Power, 0, LV_ANIM_OFF); - lv_obj_set_width(ui_TOP_Power, 29); - lv_obj_set_height(ui_TOP_Power, 13); - lv_obj_set_x(ui_TOP_Power, 286); - lv_obj_set_y(ui_TOP_Power, 5); + lv_obj_set_width(ui_TOP_Power, 33); + lv_obj_set_height(ui_TOP_Power, 14); + lv_obj_set_align(ui_TOP_Power, LV_ALIGN_CENTER); lv_obj_set_style_radius(ui_TOP_Power, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_TOP_Power, lv_color_hex(0x484847), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_Power, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(ui_TOP_Power, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_radius(ui_TOP_Power, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_TOP_Power, lv_color_hex(0x666633), LV_PART_INDICATOR | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_TOP_Power, 255, LV_PART_INDICATOR | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(ui_TOP_Power, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); ui_TOP_power_Label = lv_label_create(ui_TOP_Power); - lv_obj_set_width(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(ui_TOP_power_Label, LV_SIZE_CONTENT); /// 1 + ::ui_powerLabel = ui_TOP_power_Label; + lv_obj_set_width(ui_TOP_power_Label, LV_SIZE_CONTENT); + lv_obj_set_height(ui_TOP_power_Label, LV_SIZE_CONTENT); lv_obj_set_align(ui_TOP_power_Label, LV_ALIGN_CENTER); lv_label_set_text(ui_TOP_power_Label, "96%"); lv_obj_set_style_text_color(ui_TOP_power_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_TOP_power_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); ui_APP_Container = lv_obj_create(root_screen_); + ::ui_APP_Container = ui_APP_Container; lv_obj_remove_style_all(ui_APP_Container); - lv_obj_set_width(ui_APP_Container, 320); - lv_obj_set_height(ui_APP_Container, 150); - lv_obj_set_x(ui_APP_Container, 0); - lv_obj_set_y(ui_APP_Container, 10); - lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); + lv_obj_set_size(ui_APP_Container, 320, 150); lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags } + void create_wifi_status(lv_obj_t *parent) + { + static const int bar_heights[4] = {6, 9, 12, 15}; + lv_obj_t **bars[4] = {&::ui_wifiBar1, &::ui_wifiBar2, &::ui_wifiBar3, &::ui_wifiBar4}; + + ::ui_wifiPanel = lv_obj_create(parent); + lv_obj_set_width(::ui_wifiPanel, 24); + lv_obj_set_height(::ui_wifiPanel, 15); + lv_obj_set_x(::ui_wifiPanel, 210); + lv_obj_set_y(::ui_wifiPanel, 4); + lv_obj_clear_flag(::ui_wifiPanel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(::ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); + + for (int i = 0; i < 4; ++i) + { + *bars[i] = lv_obj_create(::ui_wifiPanel); + lv_obj_set_width(*bars[i], 4); + lv_obj_set_height(*bars[i], bar_heights[i]); + lv_obj_set_align(*bars[i], LV_ALIGN_BOTTOM_LEFT); + lv_obj_set_x(*bars[i], i * 6); + lv_obj_clear_flag(*bars[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(*bars[i], 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(*bars[i], lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(*bars[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(*bars[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + } + } + + void update_time_status() + { + if (!ui_TOP_time_Label) + return; + + char time_buf[16]; + cp0_time_str(time_buf, sizeof(time_buf)); + lv_label_set_text(ui_TOP_time_Label, time_buf); + } + + void update_wifi_status() + { + if (!::ui_wifiPanel) + return; + + cp0_wifi_status_t ws = cp0_wifi_get_status(); + lv_obj_t *bars[4] = {::ui_wifiBar1, ::ui_wifiBar2, ::ui_wifiBar3, ::ui_wifiBar4}; + static const int thresholds[4] = {1, 30, 60, 80}; + + for (int i = 0; i < 4; ++i) + { + if (!bars[i]) + continue; + lv_obj_set_style_bg_color(bars[i], + lv_color_hex(ws.connected && ws.signal >= thresholds[i] ? 0x00CCFF : 0x4D4D4D), + LV_PART_MAIN | LV_STATE_DEFAULT); + } + + if (ws.connected) + lv_obj_clear_flag(::ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(::ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); + } + void UI_bind_event() { lv_obj_add_event_cb(root_screen_, home_battery_event_cb, launcher_ui::events::battery_event(), this); From f3502507d7679ec986746c2f7d0716a377f7032f Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:36:45 +0800 Subject: [PATCH 33/70] refactor(APPLaunch): remove local compatibility headers --- .../APPLaunch/main/include/compat/fb_compat.h | 33 -------- .../main/include/compat/input_keys.h | 82 ------------------- .../APPLaunch/main/include/keyboard_input.h | 48 ----------- 3 files changed, 163 deletions(-) delete mode 100644 projects/APPLaunch/main/include/compat/fb_compat.h delete mode 100644 projects/APPLaunch/main/include/compat/input_keys.h delete mode 100644 projects/APPLaunch/main/include/keyboard_input.h diff --git a/projects/APPLaunch/main/include/compat/fb_compat.h b/projects/APPLaunch/main/include/compat/fb_compat.h deleted file mode 100644 index 9bc7e6d3..00000000 --- a/projects/APPLaunch/main/include/compat/fb_compat.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -// Cross-platform framebuffer stubs for macOS/Windows - -#ifdef __linux__ -#include -#else - -#include - -#ifndef _FB_COMPAT_H -#define _FB_COMPAT_H - -struct fb_var_screeninfo { - uint32_t xres; - uint32_t yres; - uint32_t bits_per_pixel; - uint32_t xres_virtual; - uint32_t yres_virtual; - uint32_t xoffset; - uint32_t yoffset; -}; - -struct fb_fix_screeninfo { - char id[16]; - unsigned long smem_len; - uint32_t line_length; -}; - -#define FBIOGET_VSCREENINFO 0x4600 -#define FBIOGET_FSCREENINFO 0x4602 - -#endif // _FB_COMPAT_H -#endif // __linux__ diff --git a/projects/APPLaunch/main/include/compat/input_keys.h b/projects/APPLaunch/main/include/compat/input_keys.h deleted file mode 100644 index da5eefe3..00000000 --- a/projects/APPLaunch/main/include/compat/input_keys.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once -// Cross-platform KEY_* constants -// On Linux: use the real header. On macOS/Windows: define ourselves. - -#ifdef __linux__ -#include -#else - -#ifndef KEY_ESC -#define KEY_ESC 1 -#define KEY_1 2 -#define KEY_2 3 -#define KEY_3 4 -#define KEY_4 5 -#define KEY_5 6 -#define KEY_6 7 -#define KEY_7 8 -#define KEY_8 9 -#define KEY_9 10 -#define KEY_0 11 -#define KEY_BACKSPACE 14 -#define KEY_TAB 15 -#define KEY_Q 16 -#define KEY_W 17 -#define KEY_E 18 -#define KEY_R 19 -#define KEY_T 20 -#define KEY_Y 21 -#define KEY_U 22 -#define KEY_I 23 -#define KEY_O 24 -#define KEY_P 25 -#define KEY_ENTER 28 -#define KEY_LEFTCTRL 29 -#define KEY_A 30 -#define KEY_S 31 -#define KEY_D 32 -#define KEY_F 33 -#define KEY_G 34 -#define KEY_H 35 -#define KEY_J 36 -#define KEY_K 37 -#define KEY_L 38 -#define KEY_LEFTSHIFT 42 -#define KEY_Z 44 -#define KEY_X 45 -#define KEY_C 46 -#define KEY_V 47 -#define KEY_B 48 -#define KEY_N 49 -#define KEY_M 50 -#define KEY_SPACE 57 -#define KEY_LEFTALT 56 -#define KEY_CAPSLOCK 58 -#define KEY_F1 59 -#define KEY_F2 60 -#define KEY_F3 61 -#define KEY_F4 62 -#define KEY_F5 63 -#define KEY_F6 64 -#define KEY_F7 65 -#define KEY_F8 66 -#define KEY_F9 67 -#define KEY_F10 68 -#define KEY_F11 87 -#define KEY_F12 88 -#define KEY_KPENTER 96 -#define KEY_HOME 102 -#define KEY_UP 103 -#define KEY_PAGEUP 104 -#define KEY_LEFT 105 -#define KEY_RIGHT 106 -#define KEY_END 107 -#define KEY_DOWN 108 -#define KEY_PAGEDOWN 109 -#define KEY_INSERT 110 -#define KEY_DELETE 111 -#define KEY_NEXT 407 -#define KEY_PREVIOUS 412 -#endif - -#endif // __linux__ diff --git a/projects/APPLaunch/main/include/keyboard_input.h b/projects/APPLaunch/main/include/keyboard_input.h deleted file mode 100644 index ada761b4..00000000 --- a/projects/APPLaunch/main/include/keyboard_input.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef __MAIN__H__ -#define __MAIN__H__ - -#include -#include -#ifdef __cplusplus -extern "C" { -#endif - -// modifier bitmask -#define KBD_MOD_SHIFT (1u << 0) -#define KBD_MOD_CTRL (1u << 1) -#define KBD_MOD_ALT (1u << 2) -#define KBD_MOD_LOGO (1u << 3) -#define KBD_MOD_CAPS (1u << 4) -#define KBD_MOD_NUM (1u << 5) - -// key state -#define KBD_KEY_RELEASED 0 -#define KBD_KEY_PRESSED 1 -#define KBD_KEY_REPEATED 2 - -struct key_item { - uint32_t key_code; // Linux evdev key code - uint32_t keysym; // primary XKB keysym (xkb_keysym_t) - uint32_t codepoint; // corresponding Unicode code point, or 0 if none - uint32_t mods; // modifier bitmask (KBD_MOD_*) - int key_state; // 0=released, 1=pressed, 2=repeat - char sym_name[65]; // XKB keysym name - char utf8[16]; // UTF-8 character (supports multi-byte compose output) - char flage; // whether free is required - STAILQ_ENTRY(key_item) entries; -}; - -STAILQ_HEAD(keyboard_queue_t, key_item); -extern struct keyboard_queue_t keyboard_queue; -extern pthread_mutex_t keyboard_mutex; -extern volatile int LVGL_HOME_KEY_FLAG; -extern volatile int LVGL_RUN_FLAGE; -extern volatile uint32_t LV_EVENT_KEYBOARD; - -void *keyboard_read_thread(void *argv); -const char *kbd_state_name(int state); -void kbd_dump_keymap_table(void); -#ifdef __cplusplus -} -#endif -#endif \ No newline at end of file From f1207417a10edefae816c216d0ac774c3544612a Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:52:02 +0800 Subject: [PATCH 34/70] Refactor APPLaunch Debian packager --- .github/workflows/launcher-build.yml | 4 +- README.md | 5 +- README_ZH.md | 5 +- .../00-overview-and-reading-path.md | 2 +- .../09-packaging-deployment-and-systemd.md | 144 +++-- .../12-common-modification-entry-points.md | 2 +- ...05\350\257\273\350\267\257\347\272\277.md" | 2 +- ...203\250\347\275\262\344\270\216systemd.md" | 144 +++-- ...45\345\217\243\351\200\237\346\237\245.md" | 2 +- docs/macos-docker-build.md | 2 +- projects/APPLaunch/tools/llm_pack.py | 194 ------ scripts/debian_packager.py | 579 ++++++++++++++++++ 12 files changed, 748 insertions(+), 337 deletions(-) delete mode 100755 projects/APPLaunch/tools/llm_pack.py create mode 100755 scripts/debian_packager.py diff --git a/.github/workflows/launcher-build.yml b/.github/workflows/launcher-build.yml index 524cbd81..e35b6ab7 100644 --- a/.github/workflows/launcher-build.yml +++ b/.github/workflows/launcher-build.yml @@ -12,6 +12,7 @@ on: - 'SDK/**' - '.gitmodules' - '.github/workflows/launcher-build.yml' + - 'scripts/debian_packager.py' pull_request: branches: [master] workflow_dispatch: @@ -99,8 +100,7 @@ jobs: - name: Build .deb package run: | - cd projects/APPLaunch/tools - python3 llm_pack.py + python3 scripts/debian_packager.py - name: Collect artifacts run: | diff --git a/README.md b/README.md index da236466..8b92f2a0 100644 --- a/README.md +++ b/README.md @@ -237,9 +237,8 @@ Refer to the HelloWorld build instructions. ### Package ```bash -# brew install dpkg # mac install dpkg tool. -cd projects/APPLaunch/tools -./llm_pack.py +# Optional: install dpkg to use dpkg-deb; otherwise the Python builder is used. +python3 scripts/debian_packager.py ``` This command generates a DEB installation package. After transferring the package to the device with `scp`, install it with `sudo dpkg -i ./***.deb`. diff --git a/README_ZH.md b/README_ZH.md index 679086dd..ea92fc2c 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -228,9 +228,8 @@ scons push ### 打包 ```bash -# brew install dpkg # mac 需要安装 dpkg 工具 -cd projects/APPLaunch/tools -./llm_pack.py +# 可选:安装 dpkg 后会使用 dpkg-deb;否则使用 Python 写包器。 +python3 scripts/debian_packager.py ``` 该命令会生成一个 DEB 安装包。将安装包通过 `scp` 传输到设备后,可使用 `sudo dpkg -i ./***.deb` 进行安装。 diff --git a/docs/launcher-project-guide/00-overview-and-reading-path.md b/docs/launcher-project-guide/00-overview-and-reading-path.md index 2ab3433c..6f69ac0d 100644 --- a/docs/launcher-project-guide/00-overview-and-reading-path.md +++ b/docs/launcher-project-guide/00-overview-and-reading-path.md @@ -73,7 +73,7 @@ If you only want to complete a specific task: | `projects/APPLaunch/main/ui/components/page_app` | Built-in page implementations | | `projects/APPLaunch/APPLaunch` | Resource tree packaged into the runtime environment | | `ext_components/cp0_lvgl` | Platform adaptation layer that wraps file, process, input, and system interfaces | -| `projects/APPLaunch/tools/llm_pack.py` | Debian package build script | +| `scripts/debian_packager.py` | Debian package build script | ## 5. Terminology diff --git a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md index 1f3f0bbf..49207a55 100644 --- a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md +++ b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md @@ -82,38 +82,31 @@ scons -j2 file dist/M5CardputerZero-APPLaunch ``` -## 3. `llm_pack.py` Packaging Script +## 3. `debian_packager.py` Shared Packaging Script -The packaging script is located at: +The repository-level packaging script is located at: ```text -projects/APPLaunch/tools/llm_pack.py +scripts/debian_packager.py ``` -Core constants: +It replaces the former APPLaunch-local packaging script so other projects under `projects/` can reuse the same Debian packaging flow. APPLaunch remains the default target, so running the script with no arguments still packages APPLaunch. -| Constant | Value | Description | -| --- | --- | --- | -| `PACKAGE_NAME` | `applaunch` | Debian package name | -| `APP_NAME` | `APPLaunch` | Base application and service name | -| `BIN_NAME` | `M5CardputerZero-APPLaunch` | Main executable name | -| `INSTALL_PPREFIX` | `usr/share` | Parent installation prefix | -| `INSTALL_PREFIX` | `usr/share/APPLaunch` | Application installation root | -| `BIN_PATH` | `usr/share/APPLaunch/bin` | Executable directory | -| `LIB_PATH` | `usr/share/APPLaunch/lib` | Dynamic-library directory | -| `SHARE_PATH` | `usr/share/APPLaunch/share` | Shared-resource directory | -| `APP_PATH` | `usr/share/APPLaunch/applications` | `.desktop` application descriptor directory | -| `SERVICE_PATH` | `lib/systemd/system` | systemd service directory | - -Default version information at the script entry point: - -```python -version = '0.2.1' -src_folder = '../dist' -revision = 'm5stack1' -``` +Key defaults and options: -Generated package filename format: +| Option / Default | Value | Description | +| --- | --- | --- | +| `--project` | `APPLaunch` | Project name under `projects/`, or a project path | +| `--package-name` | `applaunch` | Debian package name | +| `--app-name` | `APPLaunch` | Installed application name and systemd service name | +| `--bin-name` | `M5CardputerZero-APPLaunch` | Main executable name | +| `--src` / `--src-folder` | `dist` | Build-output directory, resolved relative to the project directory | +| `--app-tree` | auto | Runtime resource tree; defaults to `/` then `/` | +| `--output-dir` | `/tools` | Output directory for the generated `.deb` | +| `--work-dir` | output directory | Staging directory parent | +| `--builder` | `auto` | Uses `dpkg-deb` when available, otherwise the pure Python writer | + +Generated APPLaunch package filename format: ```text applaunch_0.2.1-m5stack1_arm64.deb @@ -121,7 +114,7 @@ applaunch_0.2.1-m5stack1_arm64.deb ## 4. `.deb` Package Directory Structure -After running the script, a temporary directory is generated under `projects/APPLaunch/tools`: +After running the script with default APPLaunch options, the staging directory is generated under `projects/APPLaunch/tools`: ```text projects/APPLaunch/tools/debian-APPLaunch/ @@ -141,11 +134,12 @@ projects/APPLaunch/tools/debian-APPLaunch/ │ └── M5CardputerZero-APPLaunch ├── lib/ └── share/ + ├── audio/ ├── font/ └── images/ ``` -The final `.deb` file is located at: +The final APPLaunch `.deb` file is located at: ```text projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb @@ -155,6 +149,8 @@ projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb ### 5.1 Install Packaging Tools +`debian_packager.py` can build `.deb` files with only Python. If `dpkg-deb` is installed, the default `--builder auto` path uses it automatically. + Linux development machine: ```bash @@ -162,58 +158,76 @@ sudo apt update sudo apt install -y dpkg-dev fakeroot ``` -Only `dpkg-deb` is required: +macOS can use either the Python builder or Homebrew `dpkg`: ```bash -dpkg-deb --version +brew install dpkg ``` -### 5.2 Run Packaging +### 5.2 Run APPLaunch Packaging + +Run from the repository root: ```bash -cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools -python3 llm_pack.py +python3 scripts/debian_packager.py +``` + +Equivalent explicit command: + +```bash +python3 scripts/debian_packager.py build \ + --project APPLaunch \ + --package-name applaunch \ + --app-name APPLaunch \ + --bin-name M5CardputerZero-APPLaunch ``` On success, output similar to the following appears: ```text -Creating Debian package applaunch 0.2.1 ... -Debian package created: .../applaunch_0.2.1-m5stack1_arm64.deb -applaunch create success! +Creating Debian package applaunch_0.2.1-m5stack1_arm64.deb ... +Staged package tree: .../projects/APPLaunch/tools/debian-APPLaunch +Debian package created: .../projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +Builder: dpkg-deb ``` ### 5.3 Specify a Custom Version -The current script entry point uses fixed values `0.2.1` and `m5stack1`. To build a temporary custom version without modifying repository files, call the function directly from Python: +```bash +python3 scripts/debian_packager.py build --version 0.2.2 --revision m5stack2 +``` + +For another project, override the project metadata and executable name: ```bash -cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools -python3 - <<'PY' -from llm_pack import create_applaunch_deb -print(create_applaunch_deb(version='0.2.1', src_folder='../dist', revision='m5stack1')) -PY +python3 scripts/debian_packager.py build \ + --project Calculator \ + --package-name calculator \ + --app-name Calculator \ + --bin-name M5CardputerZero-Calculator \ + --src dist \ + --app-tree share ``` -If the version number needs to change long-term, make a formal code change to `llm_pack.py` and update the release notes accordingly. +Adjust `--app-tree` to the resource tree that should become `/usr/share/` in the package. ### 5.4 Clean Packaging Artifacts The script supports: ```bash -python3 llm_pack.py clean -python3 llm_pack.py distclean +python3 scripts/debian_packager.py clean +python3 scripts/debian_packager.py distclean ``` Differences: | Command | Behavior | | --- | --- | -| `clean` | Deletes `*.deb` in the current directory and deletes first-level subdirectories under the current directory | -| `distclean` | Deletes `*.deb` and `m5stack_*` under the current directory | +| `clean` | Deletes default APPLaunch `*.deb` files and `debian-APPLaunch` under `projects/APPLaunch/tools` | +| `distclean` | Runs `clean` and also deletes legacy `m5stack_*` outputs under `projects/APPLaunch/tools` | -Note: `clean` deletes first-level directories under `tools`, including temporary directories such as `debian-APPLaunch`. Do not run it from an unintended directory that contains important subdirectories. +The same clean commands accept `--project`, `--project-dir`, `--app-name`, and `--output-dir` for non-default projects. ## 6. Packaging Script Copy Rules @@ -329,7 +343,7 @@ Purpose: - Creates a `cache` symlink under the read-only/system resource directory. - Enables and starts the systemd service. -Note: if `/usr/share/APPLaunch/cache` already exists, `ln -s` may fail. The current script does not use `ln -sfn`, so check install logs on repeated installation. +Note: the current shared packager uses `ln -sfn`, so repeated installation can refresh the cache link safely. ### 7.3 `DEBIAN/prerm` @@ -753,18 +767,18 @@ ls /dev/fb0 ### 14.4 `ln: failed to create symbolic link '/usr/share/APPLaunch/cache': File exists` -Cause: during repeated installation, `postinst` uses `ln -s` and the target already exists. +Cause: older packages created the cache link with non-idempotent `ln -s`, and the target already exists. Fix: ```bash sudo rm -rf /usr/share/APPLaunch/cache sudo mkdir -p /var/cache/APPLaunch -sudo ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache +sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache sudo systemctl restart APPLaunch.service ``` -To fix this at the root, change the packaging script to use `ln -sfn`, but that is a code change. +The current shared packager already writes `ln -sfn`; rebuild and reinstall the package to make the fix persistent. ### 14.5 `dpkg-deb: error: failed to open package info file .../DEBIAN/control` @@ -773,14 +787,14 @@ Cause: the packaging directory structure is incomplete, or the script failed mid Fix: ```bash -cd projects/APPLaunch/tools -python3 llm_pack.py clean -python3 llm_pack.py +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py clean +python3 scripts/debian_packager.py ``` -### 14.6 `FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist` +### 14.6 `Binary M5CardputerZero-APPLaunch not found in .../dist` -Cause: the project has not been built, the build directory is not `projects/APPLaunch/dist`, or the packaging script was not run from the `tools` directory. +Cause: the project has not been built, or the build directory is not `projects/APPLaunch/dist`. Fix: @@ -789,8 +803,8 @@ cd /home/nihao/w2T/github/launcher/projects/APPLaunch export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 ls -l dist/M5CardputerZero-APPLaunch -cd tools -python3 llm_pack.py +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py ``` ### 14.7 Black Screen After Service Starts @@ -846,10 +860,10 @@ file dist/M5CardputerZero-APPLaunch After packaging: ```bash -cd tools -python3 llm_pack.py -dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb -dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +dpkg-deb -I projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +dpkg-deb -c projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 ``` After installation: @@ -883,9 +897,9 @@ scons distclean export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 file dist/M5CardputerZero-APPLaunch -cd tools -python3 llm_pack.py -scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +scp projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl status APPLaunch.service --no-pager' ``` diff --git a/docs/launcher-project-guide/12-common-modification-entry-points.md b/docs/launcher-project-guide/12-common-modification-entry-points.md index effbe2fd..4178c901 100644 --- a/docs/launcher-project-guide/12-common-modification-entry-points.md +++ b/docs/launcher-project-guide/12-common-modification-entry-points.md @@ -30,7 +30,7 @@ git status --short | Change startup flow | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`, `cp0_lvgl_init()`, `ui_init()`, main loop | Check `[BOOT]` logs | | Change build dependencies | `projects/APPLaunch/main/SConstruct` | `SRCS`, `INCLUDE`, `REQUIREMENTS`, `STATIC_FILES` | scons build | | Change build configuration | `projects/APPLaunch/*.mk` | Different configs for SDL2, device, and cross build | Build with a specific `CONFIG_DEFAULT_FILE` | -| Change package contents | `projects/APPLaunch/tools/llm_pack.py`, `projects/APPLaunch/APPLaunch/` | Asset tree and install path | Check file list after building package | +| Change package contents | `scripts/debian_packager.py`, `projects/APPLaunch/APPLaunch/` | Asset tree and install path | Check file list after building package | | Change platform HAL | `ext_components/cp0_lvgl/src/cp0/`, `ext_components/cp0_lvgl/include/hal/` | framebuffer, audio, network, settings, process, etc. | Test on the device | ## 2. Source Directory Quick Reference diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" index a25c5620..599b263f 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" @@ -73,7 +73,7 @@ APPLaunch 首页、状态栏、轮播、应用管理器 | `projects/APPLaunch/main/ui/components/page_app` | 内置页面实现 | | `projects/APPLaunch/APPLaunch` | 打包进运行环境的资源树 | | `ext_components/cp0_lvgl` | 平台适配层,封装文件、进程、输入、系统接口 | -| `projects/APPLaunch/tools/llm_pack.py` | Debian 包打包脚本 | +| `scripts/debian_packager.py` | Debian 包打包脚本 | ## 5. 名词约定 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" index c936be4f..741ce7b4 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" @@ -82,38 +82,31 @@ scons -j2 file dist/M5CardputerZero-APPLaunch ``` -## 3. `llm_pack.py` 打包脚本说明 +## 3. `debian_packager.py` 通用打包脚本说明 -打包脚本位于: +仓库级打包脚本位于: ```text -projects/APPLaunch/tools/llm_pack.py +scripts/debian_packager.py ``` -核心常量: +它替代原来 APPLaunch 项目内的打包脚本,便于 `projects/` 下其他项目复用同一套 Debian 打包流程。APPLaunch 仍然是默认目标,所以不带参数运行时仍会打包 APPLaunch。 -| 常量 | 值 | 说明 | -| --- | --- | --- | -| `PACKAGE_NAME` | `applaunch` | Debian 包名 | -| `APP_NAME` | `APPLaunch` | 应用名和服务名基础 | -| `BIN_NAME` | `M5CardputerZero-APPLaunch` | 主可执行文件名 | -| `INSTALL_PPREFIX` | `usr/share` | 安装前缀上层目录 | -| `INSTALL_PREFIX` | `usr/share/APPLaunch` | 应用安装根目录 | -| `BIN_PATH` | `usr/share/APPLaunch/bin` | 可执行文件目录 | -| `LIB_PATH` | `usr/share/APPLaunch/lib` | 动态库目录 | -| `SHARE_PATH` | `usr/share/APPLaunch/share` | 共享资源目录 | -| `APP_PATH` | `usr/share/APPLaunch/applications` | `.desktop` 应用描述目录 | -| `SERVICE_PATH` | `lib/systemd/system` | systemd 服务目录 | - -默认版本信息在脚本入口处: - -```python -version = '0.2.1' -src_folder = '../dist' -revision = 'm5stack1' -``` +关键默认值和参数: -生成的包文件名格式: +| 参数 / 默认值 | 值 | 说明 | +| --- | --- | --- | +| `--project` | `APPLaunch` | `projects/` 下的项目名,也可以传项目路径 | +| `--package-name` | `applaunch` | Debian 包名 | +| `--app-name` | `APPLaunch` | 安装到 `/usr/share` 下的应用名,也是 systemd 服务名 | +| `--bin-name` | `M5CardputerZero-APPLaunch` | 主可执行文件名 | +| `--src` / `--src-folder` | `dist` | 构建输出目录,相对项目目录解析 | +| `--app-tree` | 自动 | 运行时资源树,默认依次查找 `/`、`/` | +| `--output-dir` | `/tools` | `.deb` 输出目录 | +| `--work-dir` | 输出目录 | 打包临时目录所在目录 | +| `--builder` | `auto` | 有 `dpkg-deb` 时使用 `dpkg-deb`,否则使用纯 Python 写包器 | + +APPLaunch 默认生成的包文件名格式: ```text applaunch_0.2.1-m5stack1_arm64.deb @@ -121,7 +114,7 @@ applaunch_0.2.1-m5stack1_arm64.deb ## 4. `.deb` 包目录结构 -运行脚本后会在 `projects/APPLaunch/tools` 下生成临时目录: +使用默认 APPLaunch 参数运行脚本后,会在 `projects/APPLaunch/tools` 下生成临时目录: ```text projects/APPLaunch/tools/debian-APPLaunch/ @@ -141,11 +134,12 @@ projects/APPLaunch/tools/debian-APPLaunch/ │ └── M5CardputerZero-APPLaunch ├── lib/ └── share/ + ├── audio/ ├── font/ └── images/ ``` -最终 `.deb` 文件位于: +APPLaunch 最终 `.deb` 文件位于: ```text projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb @@ -155,6 +149,8 @@ projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb ### 5.1 安装打包工具 +`debian_packager.py` 只依赖 Python 也可以生成 `.deb`。如果系统安装了 `dpkg-deb`,默认 `--builder auto` 会优先使用它。 + Linux 开发机: ```bash @@ -162,58 +158,76 @@ sudo apt update sudo apt install -y dpkg-dev fakeroot ``` -只要有 `dpkg-deb` 即可: +macOS 可以直接使用 Python 写包器,也可以通过 Homebrew 安装 `dpkg`: ```bash -dpkg-deb --version +brew install dpkg ``` -### 5.2 执行打包 +### 5.2 执行 APPLaunch 打包 + +从仓库根目录运行: ```bash -cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools -python3 llm_pack.py +python3 scripts/debian_packager.py +``` + +等价的完整参数写法: + +```bash +python3 scripts/debian_packager.py build \ + --project APPLaunch \ + --package-name applaunch \ + --app-name APPLaunch \ + --bin-name M5CardputerZero-APPLaunch ``` 成功时会看到类似: ```text -Creating Debian package applaunch 0.2.1 ... -Debian package created: .../applaunch_0.2.1-m5stack1_arm64.deb -applaunch create success! +Creating Debian package applaunch_0.2.1-m5stack1_arm64.deb ... +Staged package tree: .../projects/APPLaunch/tools/debian-APPLaunch +Debian package created: .../projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +Builder: dpkg-deb ``` ### 5.3 指定自定义版本 -当前脚本入口固定使用 `0.2.1` 和 `m5stack1`。如果需要临时打自定义版本,可以直接从 Python 调用函数,不修改仓库文件: +```bash +python3 scripts/debian_packager.py build --version 0.2.2 --revision m5stack2 +``` + +给其他项目复用时,需要覆盖项目元数据和主程序名: ```bash -cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools -python3 - <<'PY' -from llm_pack import create_applaunch_deb -print(create_applaunch_deb(version='0.2.1', src_folder='../dist', revision='m5stack1')) -PY +python3 scripts/debian_packager.py build \ + --project Calculator \ + --package-name calculator \ + --app-name Calculator \ + --bin-name M5CardputerZero-Calculator \ + --src dist \ + --app-tree share ``` -如果要长期改变版本号,应通过正式代码变更修改 `llm_pack.py`,并同步记录发布说明。 +其中 `--app-tree` 应指向需要安装为 `/usr/share/` 的资源树。 ### 5.4 清理打包产物 脚本支持: ```bash -python3 llm_pack.py clean -python3 llm_pack.py distclean +python3 scripts/debian_packager.py clean +python3 scripts/debian_packager.py distclean ``` 差异: | 命令 | 行为 | | --- | --- | -| `clean` | 删除当前目录下 `*.deb`,并删除当前目录一级子目录 | -| `distclean` | 删除当前目录下 `*.deb` 和 `m5stack_*` | +| `clean` | 删除 `projects/APPLaunch/tools` 下默认 APPLaunch 的 `*.deb` 和 `debian-APPLaunch` | +| `distclean` | 在 `clean` 基础上额外删除 `projects/APPLaunch/tools` 下旧版 `m5stack_*` 输出 | -注意:`clean` 会删除 `tools` 目录下的一级目录,包括 `debian-APPLaunch` 这类临时目录。不要在放有重要子目录的非预期目录执行。 +同样可以给清理命令传 `--project`、`--project-dir`、`--app-name` 和 `--output-dir`,用于非默认项目。 ## 6. 打包脚本复制规则 @@ -329,7 +343,7 @@ exit 0 - 在只读/系统资源目录下建立 `cache` 软链接。 - 启用并启动 systemd 服务。 -注意:如果 `/usr/share/APPLaunch/cache` 已存在,`ln -s` 可能报错。当前脚本没有使用 `ln -sfn`,重复安装时需要留意安装日志。 +注意:当前通用打包脚本使用 `ln -sfn`,重复安装时可以安全刷新缓存链接。 ### 7.3 `DEBIAN/prerm` @@ -753,18 +767,18 @@ ls /dev/fb0 ### 14.4 `ln: failed to create symbolic link '/usr/share/APPLaunch/cache': File exists` -原因:重复安装时 `postinst` 使用 `ln -s`,目标已存在。 +原因:旧版安装包使用非幂等的 `ln -s` 创建缓存链接,目标已存在。 处理: ```bash sudo rm -rf /usr/share/APPLaunch/cache sudo mkdir -p /var/cache/APPLaunch -sudo ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache +sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache sudo systemctl restart APPLaunch.service ``` -如果要从根本上修复,应修改打包脚本为 `ln -sfn`,但这属于代码变更。 +当前通用打包脚本已经写入 `ln -sfn`;重新打包并安装即可持久修复。 ### 14.5 `dpkg-deb: error: failed to open package info file .../DEBIAN/control` @@ -773,14 +787,14 @@ sudo systemctl restart APPLaunch.service 处理: ```bash -cd projects/APPLaunch/tools -python3 llm_pack.py clean -python3 llm_pack.py +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py clean +python3 scripts/debian_packager.py ``` -### 14.6 `FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist` +### 14.6 `Binary M5CardputerZero-APPLaunch not found in .../dist` -原因:未构建,或构建目录不是 `projects/APPLaunch/dist`,或打包脚本不是从 `tools` 目录运行。 +原因:未构建,或构建目录不是 `projects/APPLaunch/dist`。 处理: @@ -789,8 +803,8 @@ cd /home/nihao/w2T/github/launcher/projects/APPLaunch export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 ls -l dist/M5CardputerZero-APPLaunch -cd tools -python3 llm_pack.py +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py ``` ### 14.7 服务启动后黑屏 @@ -846,10 +860,10 @@ file dist/M5CardputerZero-APPLaunch 打包后: ```bash -cd tools -python3 llm_pack.py -dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb -dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +dpkg-deb -I projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +dpkg-deb -c projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 ``` 安装后: @@ -883,9 +897,9 @@ scons distclean export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 file dist/M5CardputerZero-APPLaunch -cd tools -python3 llm_pack.py -scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +scp projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl status APPLaunch.service --no-pager' ``` diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" index 624546c0..aad86546 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" @@ -30,7 +30,7 @@ git status --short | 修改启动流程 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、主循环 | 看 `[BOOT]` 日志 | | 修改构建依赖 | `projects/APPLaunch/main/SConstruct` | `SRCS`、`INCLUDE`、`REQUIREMENTS`、`STATIC_FILES` | scons 编译 | | 修改构建配置 | `projects/APPLaunch/*.mk` | SDL2、设备端、交叉编译不同配置 | 指定 `CONFIG_DEFAULT_FILE` 构建 | -| 修改打包内容 | `projects/APPLaunch/tools/llm_pack.py`、`projects/APPLaunch/APPLaunch/` | 资源树和安装路径 | 构建包后检查文件列表 | +| 修改打包内容 | `scripts/debian_packager.py`、`projects/APPLaunch/APPLaunch/` | 资源树和安装路径 | 构建包后检查文件列表 | | 修改平台 HAL | `ext_components/cp0_lvgl/src/cp0/`、`ext_components/cp0_lvgl/include/hal/` | framebuffer、音频、网络、设置、进程等 | 设备端实测 | ## 2. 源码目录速查 diff --git a/docs/macos-docker-build.md b/docs/macos-docker-build.md index 0cf1f18a..a4bfadac 100644 --- a/docs/macos-docker-build.md +++ b/docs/macos-docker-build.md @@ -61,7 +61,7 @@ docker run --rm --platform linux/arm64 \ 编译 + 打包 deb(一条命令): ```bash docker run --rm -v $(git rev-parse --show-toplevel):/src -w /src/projects/APPLaunch \ - cardputer-build bash -c "CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4 && cd tools && python3 llm_pack.py" + cardputer-build bash -c "CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4 && cd ../.. && python3 scripts/debian_packager.py" ``` 产物路径:`projects/APPLaunch/tools/applaunch_*.deb`(约 15MB) diff --git a/projects/APPLaunch/tools/llm_pack.py b/projects/APPLaunch/tools/llm_pack.py deleted file mode 100755 index 70d4e33a..00000000 --- a/projects/APPLaunch/tools/llm_pack.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/bin/env python3 -import os -import sys -import shutil -import subprocess -import glob -from datetime import datetime - -''' -{package_name}_{version}-{revision}_{architecture}.deb -applaunch_0.1-m5stack1_arm64.deb - -debian-APPLaunch directory structure: - DEBIAN/ - control - postinst - prerm - lib/ - systemd/ - system/ - APPLaunch.service - usr/ - share/ - APPLaunch/ - applications/ - vim.desktop.temple - bin/ - M5CardputerZero-APPLaunch - lib/ - lvgl.so - share/ - font/ - *.ttf - images/ - *.png -''' - -PACKAGE_NAME = 'applaunch' -APP_NAME = 'APPLaunch' -BIN_NAME = 'M5CardputerZero-APPLaunch' -INSTALL_PPREFIX = 'usr/share' -INSTALL_PREFIX = f'{INSTALL_PPREFIX}/APPLaunch' -BIN_PATH = f'{INSTALL_PREFIX}/bin' -LIB_PATH = f'{INSTALL_PREFIX}/lib' -SHARE_PATH = f'{INSTALL_PREFIX}/share' -APP_PATH = f'{INSTALL_PREFIX}/applications' -SERVICE_PATH = 'lib/systemd/system' - - -def create_applaunch_deb(version='0.1', src_folder='../dist', revision='m5stack1'): - """ - Build the APPLaunch Debian package from src_folder. - - Expected files inside src_folder: - bin/M5CardputerZero-APPLaunch (or directly M5CardputerZero-APPLaunch) - lib/lvgl.so - share/font/*.ttf - share/images/*.png - applications/vim.desktop.temple (optional) - """ - tools_dir = os.path.dirname(os.path.abspath(__file__)) - deb_folder = os.path.join(tools_dir, f'debian-{APP_NAME}') - deb_file = os.path.join(tools_dir, f'{PACKAGE_NAME}_{version}-{revision}_arm64.deb') - - # ------------------------------------------------------------------ cleanup - if os.path.exists(deb_folder): - shutil.rmtree(deb_folder) - - print(f'Creating Debian package {PACKAGE_NAME} {version} ...') - - # --------------------------------------------------------- create directories - for d in [ - os.path.join(deb_folder, 'DEBIAN'), - os.path.join(deb_folder, BIN_PATH), - os.path.join(deb_folder, LIB_PATH), - os.path.join(deb_folder, SHARE_PATH, 'font'), - os.path.join(deb_folder, SHARE_PATH, 'images'), - os.path.join(deb_folder, APP_PATH), - os.path.join(deb_folder, SERVICE_PATH), - ]: - os.makedirs(d, exist_ok=True) - - # ------------------------------------------------------- copy binary - # Search in src_folder directly, or inside src_folder/bin/ - bin_src = os.path.join(src_folder, BIN_NAME) - if not os.path.exists(bin_src): - bin_src = os.path.join(src_folder, 'bin', BIN_NAME) - if not os.path.exists(bin_src): - raise FileNotFoundError(f'Binary {BIN_NAME} not found in {src_folder}') - shutil.copy2(bin_src, os.path.join(deb_folder, BIN_PATH, BIN_NAME)) - - # ------------------------------------------------------- copy bundled app binaries + backends - for extra in ['M5CardputerZero-AppStore', 'appstore.py', 'M5CardputerZero-Calculator']: - extra_src = os.path.join(src_folder, 'bin', extra) - if os.path.exists(extra_src): - dest = os.path.join(deb_folder, BIN_PATH, extra) - shutil.copy2(extra_src, dest) - if not extra.endswith('.py'): - os.chmod(dest, 0o755) - print(f' Included: {extra}') - - # ------------------------------------------------------- APPLaunch/ - source_app_tree = os.path.abspath(os.path.join(tools_dir, '..', 'APPLaunch')) - app_src = source_app_tree if os.path.exists(source_app_tree) else os.path.join(src_folder, "APPLaunch") - app_dst = os.path.join(deb_folder, INSTALL_PREFIX) - print(app_src, app_dst) - shutil.copytree(app_src, app_dst, dirs_exist_ok=True) - - appstore_images = os.path.abspath(os.path.join(tools_dir, '..', '..', 'AppStore', 'share', 'images')) - if os.path.isdir(appstore_images): - images_dst = os.path.join(app_dst, 'share', 'images') - os.makedirs(images_dst, exist_ok=True) - for pattern in ('store_wordmark.png', 'store_arrow_*.png'): - for image_src in glob.glob(os.path.join(appstore_images, pattern)): - shutil.copy2(image_src, os.path.join(images_dst, os.path.basename(image_src))) - - - # ------------------------------------------------------- DEBIAN/control - with open(os.path.join(deb_folder, 'DEBIAN', 'control'), 'w') as f: - f.write(f'Package: {PACKAGE_NAME}\n') - f.write(f'Version: {version}\n') - f.write(f'Architecture: arm64\n') - f.write(f'Maintainer: dianjixz \n') - f.write(f'Original-Maintainer: m5stack \n') - f.write(f'Section: {APP_NAME}\n') - f.write(f'Priority: optional\n') - f.write(f'Homepage: https://www.m5stack.com\n') - f.write(f'Packaged-Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n') - f.write(f'Description: M5CardputerZero {APP_NAME}\n') - - # ------------------------------------------------------- DEBIAN/postinst - with open(os.path.join(deb_folder, 'DEBIAN', 'postinst'), 'w') as f: - f.write('#!/bin/sh\n') - f.write(f'mkdir -p /var/cache/APPLaunch\n') - f.write(f'ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache\n') - f.write(f'[ -f "/lib/systemd/system/{APP_NAME}.service" ] && systemctl enable {APP_NAME}.service\n') - f.write(f'[ -f "/lib/systemd/system/{APP_NAME}.service" ] && systemctl start {APP_NAME}.service\n') - f.write('exit 0\n') - - # ------------------------------------------------------- DEBIAN/prerm - with open(os.path.join(deb_folder, 'DEBIAN', 'prerm'), 'w') as f: - f.write('#!/bin/sh\n') - f.write(f'[ -f "/lib/systemd/system/{APP_NAME}.service" ] && systemctl stop {APP_NAME}.service\n') - f.write(f'[ -f "/lib/systemd/system/{APP_NAME}.service" ] && systemctl disable {APP_NAME}.service\n') - f.write(f'rm -rf /var/cache/APPLaunch\n') - f.write('exit 0\n') - - # ------------------------------------------------------- lib/systemd/system/APPLaunch.service - service_file = os.path.join(deb_folder, SERVICE_PATH, f'{APP_NAME}.service') - with open(service_file, 'w') as f: - f.write('[Unit]\n') - f.write(f'Description={APP_NAME} Service\n') - f.write('\n') - f.write('[Service]\n') - f.write(f'ExecStart=/{BIN_PATH}/{BIN_NAME}\n') - f.write(f'WorkingDirectory=/{INSTALL_PREFIX}\n') - f.write('Restart=always\n') - f.write('RestartSec=1\n') - f.write('StartLimitInterval=0\n') - f.write('\n') - f.write('[Install]\n') - f.write('WantedBy=multi-user.target\n') - f.write('\n') - - # ------------------------------------------------------- fix permissions - os.chmod(os.path.join(deb_folder, 'DEBIAN', 'postinst'), 0o755) - os.chmod(os.path.join(deb_folder, 'DEBIAN', 'prerm'), 0o755) - os.chmod(os.path.join(deb_folder, BIN_PATH, BIN_NAME), 0o755) - - # ------------------------------------------------------- build .deb - subprocess.run(['dpkg-deb', '-b', deb_folder, deb_file], check=True) - print(f'Debian package created: {deb_file}') - - # shutil.rmtree(deb_folder) - return f'{PACKAGE_NAME} create success!' - - -if __name__ == '__main__': - - if 'clean' in sys.argv: - os.system('rm -f ./*.deb') - os.system('find . -maxdepth 1 -type d ! -name "." -exec rm -rf {} +') - sys.exit(0) - - if 'distclean' in sys.argv: - os.system('rm -rf ./*.deb m5stack_*') - sys.exit(0) - - version = '0.2.1' - src_folder = '../dist' - revision = 'm5stack1' - - result = create_applaunch_deb(version, src_folder, revision) - print(result) diff --git a/scripts/debian_packager.py b/scripts/debian_packager.py new file mode 100755 index 00000000..1792b78a --- /dev/null +++ b/scripts/debian_packager.py @@ -0,0 +1,579 @@ +#!/usr/bin/env python3 +"""Build project Debian packages on Linux, macOS, or Windows. + +APPLaunch remains the default target, while CLI options allow other projects in +this repository to reuse the same cross-platform package builder. +""" + +from __future__ import annotations + +import argparse +import io +import os +import platform +import shutil +import subprocess +import sys +import tarfile +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path, PurePosixPath +from typing import Iterable, Sequence + + +DEFAULT_PROJECT = "APPLaunch" +PACKAGE_NAME = "applaunch" +APP_NAME = "APPLaunch" +BIN_NAME = "M5CardputerZero-APPLaunch" +DEFAULT_VERSION = "0.2.1" +DEFAULT_REVISION = "m5stack1" +DEFAULT_ARCHITECTURE = "arm64" + +INSTALL_PARENT = PurePosixPath("usr/share") +SERVICE_PATH = PurePosixPath("lib/systemd/system") + +OPTIONAL_BINARIES = ( + "M5CardputerZero-AppStore", + "appstore.py", + "M5CardputerZero-Calculator", +) +APPSTORE_IMAGE_PATTERNS = ("store_wordmark.png", "store_arrow_*.png") + + +class PackError(RuntimeError): + """Raised when the package cannot be assembled.""" + + +@dataclass(frozen=True) +class PackageConfig: + version: str = DEFAULT_VERSION + revision: str = DEFAULT_REVISION + architecture: str = DEFAULT_ARCHITECTURE + package_name: str = PACKAGE_NAME + app_name: str = APP_NAME + bin_name: str = BIN_NAME + maintainer: str = "dianjixz " + original_maintainer: str = "m5stack " + section: str = APP_NAME + priority: str = "optional" + homepage: str = "https://www.m5stack.com" + description: str = "M5CardputerZero APPLaunch" + + @property + def install_prefix(self) -> PurePosixPath: + return INSTALL_PARENT / self.app_name + + @property + def bin_path(self) -> PurePosixPath: + return self.install_prefix / "bin" + + @property + def service_path(self) -> PurePosixPath: + return SERVICE_PATH + + @property + def file_name(self) -> str: + return f"{self.package_name}_{self.version}-{self.revision}_{self.architecture}.deb" + + +@dataclass(frozen=True) +class Paths: + repo_root: Path + tool_dir: Path + project_dir: Path + src_dir: Path + output_dir: Path + work_dir: Path + package_root: Path + package_file: Path + + +def _posix_path(path: PurePosixPath | str) -> str: + return str(path).replace("\\", "/") + + +def _resolve_path(path: str | os.PathLike[str], base: Path) -> Path: + candidate = Path(path).expanduser() + if not candidate.is_absolute(): + candidate = base / candidate + return candidate.resolve() + + +def _safe_remove(path: Path) -> None: + if path.is_dir() and not path.is_symlink(): + shutil.rmtree(path) + elif path.exists(): + path.unlink() + + +def _chmod(path: Path, mode: int) -> None: + try: + path.chmod(mode) + except PermissionError: + if platform.system() != "Windows": + raise + + +def _mkdir(root: Path, relative: PurePosixPath | str) -> Path: + target = root / Path(*PurePosixPath(relative).parts) + target.mkdir(parents=True, exist_ok=True) + return target + + +def _copy_file(src: Path, dst: Path, mode: int | None = None) -> None: + if not src.is_file(): + raise PackError(f"required file not found: {src}") + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + if mode is not None: + _chmod(dst, mode) + + +def _find_binary(src_dir: Path, bin_name: str) -> Path: + for candidate in (src_dir / bin_name, src_dir / "bin" / bin_name): + if candidate.is_file(): + return candidate + raise PackError(f"binary {bin_name} not found in {src_dir} or {src_dir / 'bin'}") + + +def _default_app_tree(project_dir: Path, src_dir: Path, app_name: str) -> Path: + for candidate in (project_dir / app_name, src_dir / app_name): + if candidate.is_dir(): + return candidate + raise PackError( + f"{app_name} resource tree not found. Expected one of: " + f"{project_dir / app_name}, {src_dir / app_name}" + ) + + +def _copy_tree(src: Path, dst: Path) -> None: + if not src.is_dir(): + raise PackError(f"required directory not found: {src}") + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst, symlinks=True) + + +def _copy_optional_binaries(src_dir: Path, package_root: Path, config: PackageConfig) -> list[str]: + copied: list[str] = [] + for name in OPTIONAL_BINARIES: + candidates = (src_dir / "bin" / name, src_dir / name) + source = next((path for path in candidates if path.is_file()), None) + if source is None: + continue + mode = 0o644 if name.endswith(".py") else 0o755 + _copy_file(source, package_root / Path(*config.bin_path.parts) / name, mode=mode) + copied.append(name) + return copied + + +def _copy_appstore_images(project_dir: Path, app_dst: Path) -> list[str]: + images_src = project_dir.parent / "AppStore" / "share" / "images" + if not images_src.is_dir(): + return [] + images_dst = app_dst / "share" / "images" + images_dst.mkdir(parents=True, exist_ok=True) + copied: list[str] = [] + for pattern in APPSTORE_IMAGE_PATTERNS: + for image in sorted(images_src.glob(pattern)): + if image.is_file(): + shutil.copy2(image, images_dst / image.name) + copied.append(image.name) + return copied + + +def _control_text(config: PackageConfig) -> str: + fields = { + "Package": config.package_name, + "Version": config.version, + "Architecture": config.architecture, + "Maintainer": config.maintainer, + "Original-Maintainer": config.original_maintainer, + "Section": config.section, + "Priority": config.priority, + "Homepage": config.homepage, + "Packaged-Date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "Description": config.description, + } + return "".join(f"{key}: {value}\n" for key, value in fields.items()) + + +def _postinst_text(config: PackageConfig) -> str: + return f"""#!/bin/sh +set -e +mkdir -p /var/cache/{config.app_name} +ln -sfn /var/cache/{config.app_name} /usr/share/{config.app_name}/cache +if command -v systemctl >/dev/null 2>&1 && [ -f "/lib/systemd/system/{config.app_name}.service" ]; then + systemctl daemon-reload || true + systemctl enable {config.app_name}.service || true + systemctl restart {config.app_name}.service || systemctl start {config.app_name}.service || true +fi +exit 0 +""" + + +def _prerm_text(config: PackageConfig) -> str: + return f"""#!/bin/sh +set -e +if command -v systemctl >/dev/null 2>&1 && [ -f "/lib/systemd/system/{config.app_name}.service" ]; then + systemctl stop {config.app_name}.service || true + systemctl disable {config.app_name}.service || true +fi +rm -rf /var/cache/{config.app_name} +exit 0 +""" + + +def _service_text(config: PackageConfig) -> str: + return f"""[Unit] +Description={config.app_name} Service +After=multi-user.target + +[Service] +ExecStart=/{_posix_path(config.bin_path / config.bin_name)} +WorkingDirectory=/{_posix_path(config.install_prefix)} +Restart=always +RestartSec=1 +StartLimitInterval=0 + +[Install] +WantedBy=multi-user.target +""" + + +def _write_text(path: Path, text: str, mode: int = 0o644) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8", newline="\n") + _chmod(path, mode) + + +def _repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def _default_project_dir(repo_root: Path, project: str) -> Path: + candidate = repo_root / "projects" / project + if candidate.is_dir(): + return candidate + return _resolve_path(project, repo_root) + + +def _prepare_paths( + src_folder: str | os.PathLike[str], + output_dir: str | os.PathLike[str] | None, + work_dir: str | os.PathLike[str] | None, + config: PackageConfig, + project: str = DEFAULT_PROJECT, + project_dir: str | os.PathLike[str] | None = None, +) -> Paths: + repo_root = _repo_root() + tool_dir = Path(__file__).resolve().parent + resolved_project_dir = ( + _resolve_path(project_dir, repo_root) + if project_dir + else _default_project_dir(repo_root, project) + ) + src_dir = _resolve_path(src_folder, resolved_project_dir) + out_dir = _resolve_path(output_dir or resolved_project_dir / "tools", repo_root) + staging_parent = _resolve_path(work_dir or out_dir, repo_root) + package_root = staging_parent / f"debian-{config.app_name}" + return Paths( + repo_root=repo_root, + tool_dir=tool_dir, + project_dir=resolved_project_dir, + src_dir=src_dir, + output_dir=out_dir, + work_dir=staging_parent, + package_root=package_root, + package_file=out_dir / config.file_name, + ) + + +def prepare_package_tree( + config: PackageConfig, + paths: Paths, + app_tree: str | os.PathLike[str] | None = None, +) -> None: + if paths.package_root.exists(): + shutil.rmtree(paths.package_root) + + for directory in ( + PurePosixPath("DEBIAN"), + config.bin_path, + config.service_path, + ): + _mkdir(paths.package_root, directory) + + app_src = _resolve_path(app_tree, paths.project_dir) if app_tree else _default_app_tree(paths.project_dir, paths.src_dir, config.app_name) + app_dst = paths.package_root / Path(*config.install_prefix.parts) + _copy_tree(app_src, app_dst) + + binary = _find_binary(paths.src_dir, config.bin_name) + _copy_file(binary, paths.package_root / Path(*config.bin_path.parts) / config.bin_name, mode=0o755) + + copied_bins = _copy_optional_binaries(paths.src_dir, paths.package_root, config) + copied_images = ( + _copy_appstore_images(paths.project_dir, app_dst) + if config.app_name == APP_NAME + else [] + ) + + _write_text(paths.package_root / "DEBIAN" / "control", _control_text(config)) + _write_text(paths.package_root / "DEBIAN" / "postinst", _postinst_text(config), mode=0o755) + _write_text(paths.package_root / "DEBIAN" / "prerm", _prerm_text(config), mode=0o755) + _write_text( + paths.package_root / Path(*config.service_path.parts) / f"{config.app_name}.service", + _service_text(config), + ) + + print(f"Staged package tree: {paths.package_root}") + print(f" binary: {binary}") + print(f" app tree: {app_src}") + if copied_bins: + print(f" optional binaries: {', '.join(copied_bins)}") + if copied_images: + print(f" AppStore images: {', '.join(copied_images)}") + + +def _tar_filter(tar_info: tarfile.TarInfo) -> tarfile.TarInfo: + tar_info.uid = 0 + tar_info.gid = 0 + tar_info.uname = "root" + tar_info.gname = "root" + if tar_info.isdir(): + tar_info.mode = 0o755 + elif tar_info.mode & 0o111: + tar_info.mode = 0o755 + else: + tar_info.mode = 0o644 + return tar_info + + +def _tar_tree(root: Path, names: Iterable[str]) -> bytes: + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:gz", format=tarfile.GNU_FORMAT) as tar: + for name in names: + source = root / name + if not source.exists(): + continue + tar.add(source, arcname=name, recursive=True, filter=_tar_filter) + return buffer.getvalue() + + +def _data_members(package_root: Path) -> list[str]: + return sorted( + entry.name for entry in package_root.iterdir() if entry.name != "DEBIAN" + ) + + +def _ar_member_header(name: str, size: int, mode: int = 0o100644) -> bytes: + if len(name) > 15: + raise PackError(f"ar member name too long: {name}") + header = ( + f"{name + '/':<16}" + f"{int(datetime.now().timestamp()):<12}" + f"{0:<6}" + f"{0:<6}" + f"{format(mode, 'o'):<8}" + f"{size:<10}`\n" + ) + return header.encode("ascii") + + +def _write_ar_member(handle, name: str, data: bytes) -> None: + handle.write(_ar_member_header(name, len(data))) + handle.write(data) + if len(data) % 2: + handle.write(b"\n") + + +def build_deb_with_python(package_root: Path, deb_file: Path) -> None: + control_tar = _tar_tree(package_root / "DEBIAN", ("control", "postinst", "prerm")) + data_tar = _tar_tree(package_root, _data_members(package_root)) + + deb_file.parent.mkdir(parents=True, exist_ok=True) + with deb_file.open("wb") as handle: + handle.write(b"!\n") + _write_ar_member(handle, "debian-binary", b"2.0\n") + _write_ar_member(handle, "control.tar.gz", control_tar) + _write_ar_member(handle, "data.tar.gz", data_tar) + + +def build_deb_with_dpkg(package_root: Path, deb_file: Path) -> None: + deb_file.parent.mkdir(parents=True, exist_ok=True) + command = ["dpkg-deb", "--root-owner-group", "-b", str(package_root), str(deb_file)] + subprocess.run(command, check=True) + + +def build_deb(package_root: Path, deb_file: Path, builder: str = "auto") -> str: + selected = builder + if builder == "auto": + selected = "dpkg-deb" if shutil.which("dpkg-deb") else "python" + + if selected == "dpkg-deb": + build_deb_with_dpkg(package_root, deb_file) + elif selected == "python": + build_deb_with_python(package_root, deb_file) + else: + raise PackError(f"unsupported builder: {builder}") + return selected + + +def create_deb_package( + version: str = DEFAULT_VERSION, + src_folder: str | os.PathLike[str] = "dist", + revision: str = DEFAULT_REVISION, + architecture: str = DEFAULT_ARCHITECTURE, + output_dir: str | os.PathLike[str] | None = None, + work_dir: str | os.PathLike[str] | None = None, + builder: str = "auto", + app_tree: str | os.PathLike[str] | None = None, + keep_staging: bool = True, + project: str = DEFAULT_PROJECT, + project_dir: str | os.PathLike[str] | None = None, + package_name: str = PACKAGE_NAME, + app_name: str = APP_NAME, + bin_name: str = BIN_NAME, +) -> str: + """Build a project as a Debian package and return the output path.""" + config = PackageConfig( + version=version, + revision=revision, + architecture=architecture, + package_name=package_name, + app_name=app_name, + bin_name=bin_name, + section=app_name, + description=f"M5CardputerZero {app_name}", + ) + paths = _prepare_paths(src_folder, output_dir, work_dir, config, project, project_dir) + + print(f"Creating Debian package {config.file_name} ...") + prepare_package_tree(config, paths, app_tree=app_tree) + selected_builder = build_deb(paths.package_root, paths.package_file, builder=builder) + + if not keep_staging: + shutil.rmtree(paths.package_root) + + print(f"Debian package created: {paths.package_file}") + print(f"Builder: {selected_builder}") + return str(paths.package_file) + + +def create_applaunch_deb(**kwargs) -> str: + """Compatibility wrapper for older callers that imported this helper.""" + return create_deb_package(**kwargs) + + +def clean_outputs(output_dir: Path, app_name: str, distclean: bool = False) -> None: + patterns = ["*.deb", f"debian-{app_name}"] + if distclean: + patterns.append("m5stack_*") + for pattern in patterns: + for path in output_dir.glob(pattern): + _safe_remove(path) + print(f"removed: {path}") + + +def resolve_output_dir( + project: str, + project_dir: str | os.PathLike[str] | None, + output_dir: str | os.PathLike[str] | None, +) -> Path: + repo_root = _repo_root() + resolved_project_dir = ( + _resolve_path(project_dir, repo_root) + if project_dir + else _default_project_dir(repo_root, project) + ) + return _resolve_path(output_dir or resolved_project_dir / "tools", repo_root) + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Package repository projects into Debian .deb files on Linux, macOS, or Windows." + ) + subparsers = parser.add_subparsers(dest="command") + + build = subparsers.add_parser("build", help="build the Debian package") + build.add_argument("--project", default=DEFAULT_PROJECT, help="project name under projects/ or a project path") + build.add_argument("--project-dir", default=None, help="explicit project directory; overrides --project") + build.add_argument("--package-name", default=PACKAGE_NAME, help="Debian package name") + build.add_argument("--app-name", default=APP_NAME, help="installed application name under /usr/share") + build.add_argument("--bin-name", default=BIN_NAME, help="main executable name") + build.add_argument("--version", default=DEFAULT_VERSION, help="package version") + build.add_argument("--revision", default=DEFAULT_REVISION, help="Debian package revision") + build.add_argument("--architecture", default=DEFAULT_ARCHITECTURE, help="Debian architecture") + build.add_argument("--src", "--src-folder", dest="src", default="dist", help="dist directory containing the built binary; relative paths are resolved from the project directory") + build.add_argument("--app-tree", default=None, help="resource tree to install as /usr/share/") + build.add_argument("--output-dir", default=None, help="directory for the generated .deb") + build.add_argument("--work-dir", default=None, help="directory for the staging tree") + build.add_argument( + "--builder", + choices=("auto", "python", "dpkg-deb"), + default="auto", + help="package writer to use; auto prefers dpkg-deb when available", + ) + build.add_argument("--remove-staging", action="store_true", help="delete staging tree after build") + + def add_clean_args(subparser: argparse.ArgumentParser) -> None: + subparser.add_argument("--project", default=DEFAULT_PROJECT, help="project name under projects/ or a project path") + subparser.add_argument("--project-dir", default=None, help="explicit project directory; overrides --project") + subparser.add_argument("--app-name", default=APP_NAME, help="installed application name under /usr/share") + subparser.add_argument("--output-dir", default=None, help="directory containing generated package artifacts") + + clean = subparsers.add_parser("clean", help="remove generated .deb files and staging tree") + add_clean_args(clean) + distclean = subparsers.add_parser("distclean", help="clean plus legacy m5stack_* outputs") + add_clean_args(distclean) + + # Backward compatibility: bare execution still builds the APPLaunch package. + normalized = list(argv) + if not normalized: + normalized = ["build"] + elif normalized[0].startswith("-") and normalized[0] not in ("-h", "--help"): + normalized.insert(0, "build") + return parser.parse_args(normalized) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(sys.argv[1:] if argv is None else argv) + try: + if args.command == "clean": + clean_outputs( + resolve_output_dir(args.project, args.project_dir, args.output_dir), + args.app_name, + ) + return 0 + if args.command == "distclean": + clean_outputs( + resolve_output_dir(args.project, args.project_dir, args.output_dir), + args.app_name, + distclean=True, + ) + return 0 + + create_deb_package( + version=args.version, + src_folder=args.src, + revision=args.revision, + architecture=args.architecture, + output_dir=args.output_dir, + work_dir=args.work_dir, + builder=args.builder, + app_tree=args.app_tree, + keep_staging=not args.remove_staging, + project=args.project, + project_dir=args.project_dir, + package_name=args.package_name, + app_name=args.app_name, + bin_name=args.bin_name, + ) + return 0 + except (OSError, subprocess.CalledProcessError, PackError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 17a59713c22b2be184d4d3090694175c59793b13 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 17:57:36 +0800 Subject: [PATCH 35/70] refactor(APPLaunch): remove launcher global UI objects --- projects/APPLaunch/main/ui/Launch.cpp | 77 +-- projects/APPLaunch/main/ui/UILaunchPage.cpp | 130 ++-- projects/APPLaunch/main/ui/UILaunchPage.h | 1 + .../main/ui/components/ui_app_page.hpp | 111 ++-- .../main/ui/components/ui_launch_page.hpp | 586 ------------------ projects/APPLaunch/main/ui/ui.cpp | 9 - projects/APPLaunch/main/ui/ui.h | 8 - projects/APPLaunch/main/ui/ui_obj.h | 15 - 8 files changed, 124 insertions(+), 813 deletions(-) delete mode 100644 projects/APPLaunch/main/ui/components/ui_launch_page.hpp delete mode 100644 projects/APPLaunch/main/ui/ui_obj.h diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index d01f8029..28923be7 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -127,10 +127,10 @@ struct app class LaunchImpl { private: + std::shared_ptr launch_page_; int current_app = 2; cp0_watcher_t dir_watcher = NULL; lv_timer_t *watch_timer = nullptr; // LVGL 3s timer - lv_timer_t *status_timer = nullptr; // status-bar refresh timer int fixed_count; public: @@ -138,7 +138,8 @@ class LaunchImpl std::shared_ptr app_Page; std::shared_ptr home_Page; public: - LaunchImpl() + explicit LaunchImpl(std::shared_ptr launch_page) + : launch_page_(std::move(launch_page)) { // Fixed icon; users cannot modify it app_list.emplace_back("Python", @@ -236,9 +237,6 @@ class LaunchImpl // Create a 3s LVGL timer to periodically check directory changes watch_timer = lv_timer_create(app_dir_watch_cb, 3000, this); - // Refresh the status bar (time + battery) every 5 seconds - update_home_status_bar(); - status_timer = lv_timer_create(home_status_timer_cb, 5000, this); } void launch_app() @@ -252,8 +250,8 @@ class LaunchImpl auto self = (LaunchImpl *)arg; SLOGI("[HOME] lv_go_back_home executing (page=%p)", self->app_Page.get()); lv_timer_enable(true); - UILaunchPage::bind_home_input_group(); - lv_disp_load_scr(ui_Screen1); + if (self->launch_page_) + self->launch_page_->show_home_screen(); lv_refr_now(NULL); if (self->app_Page) self->app_Page.reset(); @@ -308,7 +306,8 @@ class LaunchImpl lv_timer_enable(true); if (indev) lv_indev_set_group(indev, UILaunchPage::home_input_group()); - lv_disp_load_scr(ui_Screen1); + if (launch_page_) + launch_page_->show_home_screen(); /* Child process has returned; we are back on the launcher home. * Hide the overlay so it doesn't linger. */ ui_loading::hide(); @@ -535,61 +534,6 @@ class LaunchImpl refresh_ui_panels(); } - // ============================================================ - // Home status-bar refresh: time + battery (BQ27220) - // ============================================================ - static void home_status_timer_cb(lv_timer_t *timer) - { - auto *self = static_cast(lv_timer_get_user_data(timer)); - if (self) - self->update_home_status_bar(); - } - - void update_home_status_bar() - { - // WiFi signal bars: show/hide + color by strength - cp0_wifi_status_t wifi = cp0_wifi_get_status(); - fprintf(stderr, "[HOME_STATUS] connected=%d sig=%d ssid=%s\n", - wifi.connected, wifi.signal, wifi.ssid); - if (wifi.connected) { - lv_obj_clear_flag(ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); - int sig = wifi.signal; - uint32_t on_color = 0x33CC33; - uint32_t off_color = 0x4D4D4D; - lv_obj_set_style_bg_color(ui_wifiBar1, lv_color_hex(sig > 0 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar2, lv_color_hex(sig >= 30 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar3, lv_color_hex(sig >= 60 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_wifiBar4, lv_color_hex(sig >= 80 ? on_color : off_color), LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_add_flag(ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); - } - - // Time - char time_buf[16]; - cp0_time_str(time_buf, sizeof(time_buf)); - lv_label_set_text(ui_timeLabel, time_buf); - - // Battery - cp0_battery_info_t bat = cp0_battery_read(); - if (bat.valid) - { - int soc = bat.soc; - if (soc > 100) - soc = 100; - if (soc < 0) - soc = 0; - lv_bar_set_value(ui_Bar1, soc, LV_ANIM_ON); - - char pwr_buf[16]; - snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); - lv_label_set_text(ui_powerLabel, pwr_buf); - if (soc == 100) - lv_obj_set_style_text_font(ui_powerLabel, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - else - lv_obj_set_style_text_font(ui_powerLabel, LV_FONT_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } - // ============================================================ // LVGL timer callback: check inotify events and refresh the list on changes // ============================================================ @@ -689,11 +633,6 @@ app::app(std::string name, // ============================================================ LaunchImpl::~LaunchImpl() { - if (status_timer) - { - lv_timer_delete(status_timer); - status_timer = nullptr; - } if (watch_timer) { lv_timer_delete(watch_timer); @@ -717,7 +656,7 @@ void Launch::set_launch_page(std::shared_ptr launch_page) void Launch::bind_ui() { - impl_ = std::make_unique(); + impl_ = std::make_unique(launch_page_); } void Launch::update_left_slot(lv_obj_t *panel, lv_obj_t *label) diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 69726931..e7a552ca 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -35,6 +35,9 @@ static void switch_right(lv_event_t *e); static void app_launch(lv_event_t *e); static void main_key_switch(lv_event_t *e); +lv_obj_t *left_arrow_button = nullptr; +lv_obj_t *right_arrow_button = nullptr; + // ==================== standard layout for carousel slots ==================== struct CarouselSlot { @@ -304,13 +307,6 @@ static void switch_left(lv_event_t *e) // screen / app // ============================================================ -static void go_back_home(lv_event_t *e) -{ - lv_disp_load_scr(ui_Screen1); - UILaunchPage::bind_home_input_group(); -} - - static void ui_event_Screen1(lv_event_t *e) { if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) @@ -522,12 +518,16 @@ void UILaunchPage::init_input_group() bind_home_input_group(); } -void UILaunchPage::load_home_screen() +void UILaunchPage::show_home_screen() { - SLOGI("[HOME] home_screen_load() - loading launcher home screen"); - lv_disp_load_scr(ui_Screen1); + SLOGI("[HOME] show_home_screen() - loading launcher home screen"); + lv_disp_load_scr(screen()); UILaunchPage::bind_home_input_group(); +} +void UILaunchPage::load_home_screen() +{ + show_home_screen(); cp0_signal_audio_api_play_asset("startup.mp3"); } @@ -587,11 +587,6 @@ void UILaunchPage::launch_selected_app() void UILaunchPage::create_screen() { - if (!ui_Screen1) - ui_Screen1 = screen(); - if (!::ui_APP_Container) - ::ui_APP_Container = content_container(); - if (carousel_elements[kCardCenter]) return; @@ -602,14 +597,14 @@ void UILaunchPage::create_screen() void UILaunchPage::create_app_container(lv_obj_t *parent) { - ::ui_APP_Container = parent; - if (!::ui_APP_Container) + lv_obj_t *app_container = parent; + if (!app_container) return; - lv_obj_set_size(::ui_APP_Container, 320, 150); - lv_obj_clear_flag(::ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_set_size(app_container, 320, 150); + lv_obj_clear_flag(app_container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - carousel_elements[kPageDot0] = lv_obj_create(::ui_APP_Container); + carousel_elements[kPageDot0] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kPageDot0], 5); lv_obj_set_height(carousel_elements[kPageDot0], 5); lv_obj_set_x(carousel_elements[kPageDot0], -20); @@ -622,7 +617,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kPageDot1] = lv_obj_create(::ui_APP_Container); + carousel_elements[kPageDot1] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kPageDot1], 5); lv_obj_set_height(carousel_elements[kPageDot1], 5); lv_obj_set_x(carousel_elements[kPageDot1], -10); @@ -635,7 +630,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kPageDot2] = lv_obj_create(::ui_APP_Container); + carousel_elements[kPageDot2] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kPageDot2], 10); lv_obj_set_height(carousel_elements[kPageDot2], 10); lv_obj_set_x(carousel_elements[kPageDot2], 0); @@ -648,7 +643,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kPageDot3] = lv_obj_create(::ui_APP_Container); + carousel_elements[kPageDot3] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kPageDot3], 5); lv_obj_set_height(carousel_elements[kPageDot3], 5); lv_obj_set_x(carousel_elements[kPageDot3], 10); @@ -661,7 +656,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kPageDot4] = lv_obj_create(::ui_APP_Container); + carousel_elements[kPageDot4] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kPageDot4], 5); lv_obj_set_height(carousel_elements[kPageDot4], 5); lv_obj_set_x(carousel_elements[kPageDot4], 20); @@ -674,7 +669,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kTitleCenter] = lv_label_create(::ui_APP_Container); + carousel_elements[kTitleCenter] = lv_label_create(app_container); lv_obj_set_width(carousel_elements[kTitleCenter], LV_SIZE_CONTENT); lv_obj_set_height(carousel_elements[kTitleCenter], LV_SIZE_CONTENT); /// 1 lv_obj_set_x(carousel_elements[kTitleCenter], 0); @@ -685,7 +680,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_text_color(carousel_elements[kTitleCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kTitleRight] = lv_label_create(::ui_APP_Container); + carousel_elements[kTitleRight] = lv_label_create(app_container); lv_obj_set_width(carousel_elements[kTitleRight], LV_SIZE_CONTENT); lv_obj_set_height(carousel_elements[kTitleRight], LV_SIZE_CONTENT); /// 1 lv_obj_set_x(carousel_elements[kTitleRight], 99); @@ -696,7 +691,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_text_font(carousel_elements[kTitleRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kTitleLeft] = lv_label_create(::ui_APP_Container); + carousel_elements[kTitleLeft] = lv_label_create(app_container); lv_obj_set_width(carousel_elements[kTitleLeft], LV_SIZE_CONTENT); lv_obj_set_height(carousel_elements[kTitleLeft], LV_SIZE_CONTENT); /// 1 lv_obj_set_x(carousel_elements[kTitleLeft], -99); @@ -707,7 +702,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_text_opa(carousel_elements[kTitleLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_font(carousel_elements[kTitleLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kCardLeft] = lv_obj_create(::ui_APP_Container); + carousel_elements[kCardLeft] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kCardLeft], 80); lv_obj_set_height(carousel_elements[kCardLeft], 80); lv_obj_set_x(carousel_elements[kCardLeft], -99); @@ -720,7 +715,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kCardLeft], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kCardLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kCardCenter] = lv_obj_create(::ui_APP_Container); + carousel_elements[kCardCenter] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kCardCenter], 100); lv_obj_set_height(carousel_elements[kCardCenter], 100); lv_obj_set_x(carousel_elements[kCardCenter], 0); @@ -734,7 +729,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_opa(carousel_elements[kCardCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(carousel_elements[kCardCenter], 2, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kCardRight] = lv_obj_create(::ui_APP_Container); + carousel_elements[kCardRight] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kCardRight], 80); lv_obj_set_height(carousel_elements[kCardRight], 80); lv_obj_set_x(carousel_elements[kCardRight], 99); @@ -747,7 +742,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kCardRight], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kCardRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kCardFarRight] = lv_obj_create(::ui_APP_Container); + carousel_elements[kCardFarRight] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kCardFarRight], 61); lv_obj_set_height(carousel_elements[kCardFarRight], 61); lv_obj_set_x(carousel_elements[kCardFarRight], 177); @@ -761,37 +756,37 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kCardFarRight], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kCardFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_leftButton = lv_btn_create(::ui_APP_Container); - lv_obj_set_width(ui_leftButton, 17); - lv_obj_set_height(ui_leftButton, 23); - lv_obj_set_x(ui_leftButton, -151); - lv_obj_set_y(ui_leftButton, -4); - lv_obj_set_align(ui_leftButton, LV_ALIGN_CENTER); - lv_obj_add_flag(ui_leftButton, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags - lv_obj_clear_flag(ui_leftButton, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_leftButton, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_leftButton, cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(ui_leftButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(ui_leftButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_rightButton = lv_btn_create(::ui_APP_Container); - lv_obj_set_width(ui_rightButton, 17); - lv_obj_set_height(ui_rightButton, 23); - lv_obj_set_x(ui_rightButton, 150); - lv_obj_set_y(ui_rightButton, -4); - lv_obj_set_align(ui_rightButton, LV_ALIGN_CENTER); - lv_obj_add_flag(ui_rightButton, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags - lv_obj_clear_flag(ui_rightButton, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_rightButton, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_rightButton, cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(ui_rightButton, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(ui_rightButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kCardFarLeft] = lv_obj_create(::ui_APP_Container); + left_arrow_button = lv_btn_create(app_container); + lv_obj_set_width(left_arrow_button, 17); + lv_obj_set_height(left_arrow_button, 23); + lv_obj_set_x(left_arrow_button, -151); + lv_obj_set_y(left_arrow_button, -4); + lv_obj_set_align(left_arrow_button, LV_ALIGN_CENTER); + lv_obj_add_flag(left_arrow_button, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags + lv_obj_clear_flag(left_arrow_button, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(left_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(left_arrow_button, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(left_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(left_arrow_button, cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_color(left_arrow_button, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_opa(left_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + right_arrow_button = lv_btn_create(app_container); + lv_obj_set_width(right_arrow_button, 17); + lv_obj_set_height(right_arrow_button, 23); + lv_obj_set_x(right_arrow_button, 150); + lv_obj_set_y(right_arrow_button, -4); + lv_obj_set_align(right_arrow_button, LV_ALIGN_CENTER); + lv_obj_add_flag(right_arrow_button, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags + lv_obj_clear_flag(right_arrow_button, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(right_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(right_arrow_button, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(right_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(right_arrow_button, cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_color(right_arrow_button, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_opa(right_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements[kCardFarLeft] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kCardFarLeft], 61); lv_obj_set_height(carousel_elements[kCardFarLeft], 61); lv_obj_set_x(carousel_elements[kCardFarLeft], -177); @@ -805,7 +800,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kCardFarLeft], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kCardFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kTitleFarLeft] = lv_label_create(::ui_APP_Container); + carousel_elements[kTitleFarLeft] = lv_label_create(app_container); lv_obj_set_width(carousel_elements[kTitleFarLeft], LV_SIZE_CONTENT); lv_obj_set_height(carousel_elements[kTitleFarLeft], LV_SIZE_CONTENT); /// 1 lv_obj_set_x(carousel_elements[kTitleFarLeft], -177); @@ -817,7 +812,7 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_text_color(carousel_elements[kTitleFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kTitleFarRight] = lv_label_create(::ui_APP_Container); + carousel_elements[kTitleFarRight] = lv_label_create(app_container); lv_obj_set_width(carousel_elements[kTitleFarRight], LV_SIZE_CONTENT); lv_obj_set_height(carousel_elements[kTitleFarRight], LV_SIZE_CONTENT); /// 1 lv_obj_set_x(carousel_elements[kTitleFarRight], 177); @@ -833,10 +828,11 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_add_event_cb(carousel_elements[kCardCenter], app_launch, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(carousel_elements[kCardRight], app_launch, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(carousel_elements[kCardFarRight], app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_leftButton, switch_right, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_rightButton, switch_left, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(left_arrow_button, switch_right, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(right_arrow_button, switch_left, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(carousel_elements[kCardFarLeft], app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(ui_Screen1, main_key_switch, (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); + if (active_launch_page) + lv_obj_add_event_cb(active_launch_page->screen(), main_key_switch, (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); } diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index e3623a86..c8b79e70 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -12,6 +12,7 @@ class UILaunchPage : public home_base explicit UILaunchPage(std::shared_ptr launch); ~UILaunchPage(); + void show_home_screen(); void load_home_screen(); void start_startup_gif(); void create_screen(); diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 90420e37..6a41e0f1 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -54,6 +54,7 @@ class UIAppTopBar ui_TOP_Container = lv_obj_create(parent); lv_obj_remove_style_all(ui_TOP_Container); lv_obj_set_size(ui_TOP_Container, 320, height_); + lv_obj_set_pos(ui_TOP_Container, 0, 0); lv_obj_clear_flag(ui_TOP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); lv_obj_set_flex_flow(ui_TOP_Container, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(ui_TOP_Container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); @@ -273,6 +274,7 @@ class UIAppContainer lv_obj_remove_style_all(ui_APP_Container); lv_obj_set_width(ui_APP_Container, 320); lv_obj_set_height(ui_APP_Container, height_); + lv_obj_set_pos(ui_APP_Container, 0, 20); lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); return ui_APP_Container; } @@ -328,10 +330,7 @@ class AppPageRoot { root_screen_ = lv_obj_create(NULL); lv_obj_clear_flag(root_screen_, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_flex_flow(root_screen_, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(root_screen_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(root_screen_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_row(root_screen_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(root_screen_, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(root_screen_, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } @@ -445,6 +444,7 @@ class AppBottomBarRegion : virtual public AppPageRoot, virtual public AppContent lv_obj_remove_style_all(ui_BOTTOM_Container); lv_obj_set_width(ui_BOTTOM_Container, 320); lv_obj_set_height(ui_BOTTOM_Container, 20); + lv_obj_set_pos(ui_BOTTOM_Container, 0, 150); lv_obj_clear_flag(ui_BOTTOM_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); } @@ -478,6 +478,8 @@ class home_base : public AppPageRoot private: lv_obj_t *ui_TOP_Container = nullptr; lv_obj_t *ui_TOP_logo = nullptr; + lv_obj_t *ui_TOP_wifiPanel = nullptr; + lv_obj_t *ui_TOP_wifiBars[4] = {}; lv_obj_t *ui_TOP_time = nullptr; lv_obj_t *ui_TOP_time_Label = nullptr; lv_obj_t *ui_TOP_Power = nullptr; @@ -499,10 +501,6 @@ class home_base : public AppPageRoot { if (status_timer_) lv_timer_delete(status_timer_); - if (::ui_Screen1 == root_screen_) - ::ui_Screen1 = nullptr; - if (::ui_APP_Container == ui_APP_Container) - ::ui_APP_Container = nullptr; } static void home_battery_event_cb(lv_event_t *e) @@ -541,6 +539,10 @@ class home_base : public AppPageRoot char pwr_buf[16]; snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); lv_label_set_text(ui_TOP_power_Label, pwr_buf); + if (soc == 100) + lv_obj_set_style_text_font(ui_TOP_power_Label, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); + else + lv_obj_set_style_text_font(ui_TOP_power_Label, LV_FONT_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); uint32_t color = 0x66CC33; if (soc <= 20) @@ -568,16 +570,14 @@ class home_base : public AppPageRoot /* ================================================================== */ void creat_Top_UI() { - ::ui_Screen1 = root_screen_; - ui_TOP_Container = lv_obj_create(root_screen_); lv_obj_remove_style_all(ui_TOP_Container); lv_obj_set_size(ui_TOP_Container, 320, 20); + lv_obj_set_pos(ui_TOP_Container, 0, 0); lv_obj_clear_flag(ui_TOP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); #ifdef APPLAUNCH_LOGO_USE_PNG ui_TOP_logo = lv_img_create(ui_TOP_Container); - ::ui_Image1 = ui_TOP_logo; lv_img_set_src(ui_TOP_logo, cp0_file_path_c("launcher_brand_logo.png")); lv_obj_set_width(ui_TOP_logo, LV_SIZE_CONTENT); lv_obj_set_height(ui_TOP_logo, LV_SIZE_CONTENT); @@ -587,7 +587,6 @@ class home_base : public AppPageRoot lv_obj_clear_flag(ui_TOP_logo, LV_OBJ_FLAG_SCROLLABLE); #else ui_TOP_logo = lv_label_create(ui_TOP_Container); - ::ui_Image1 = ui_TOP_logo; lv_label_set_text(ui_TOP_logo, "ZERO"); lv_obj_set_x(ui_TOP_logo, 5); lv_obj_set_y(ui_TOP_logo, 2); @@ -598,7 +597,6 @@ class home_base : public AppPageRoot create_wifi_status(ui_TOP_Container); ui_TOP_time = lv_obj_create(ui_TOP_Container); - ::ui_Panel1 = ui_TOP_time; lv_obj_set_width(ui_TOP_time, 40); lv_obj_set_height(ui_TOP_time, 16); lv_obj_set_x(ui_TOP_time, 236); @@ -610,7 +608,6 @@ class home_base : public AppPageRoot lv_obj_set_style_bg_img_src(ui_TOP_time, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_width(ui_TOP_time, 0, LV_PART_MAIN | LV_STATE_DEFAULT); ui_TOP_time_Label = lv_label_create(ui_TOP_time); - ::ui_timeLabel = ui_TOP_time_Label; lv_obj_set_width(ui_TOP_time_Label, LV_SIZE_CONTENT); lv_obj_set_height(ui_TOP_time_Label, LV_SIZE_CONTENT); lv_obj_set_align(ui_TOP_time_Label, LV_ALIGN_CENTER); @@ -618,21 +615,20 @@ class home_base : public AppPageRoot lv_obj_set_style_text_color(ui_TOP_time_Label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(ui_TOP_time_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - ::ui_batteryPanel = lv_obj_create(ui_TOP_Container); - lv_obj_set_width(::ui_batteryPanel, 36); - lv_obj_set_height(::ui_batteryPanel, 16); - lv_obj_set_x(::ui_batteryPanel, 280); - lv_obj_set_y(::ui_batteryPanel, 4); - lv_obj_clear_flag(::ui_batteryPanel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(::ui_batteryPanel, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(::ui_batteryPanel, cp0_file_path_c("status_battery_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(::ui_batteryPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - ui_TOP_Power = lv_bar_create(::ui_batteryPanel); - ::ui_Bar1 = ui_TOP_Power; + lv_obj_t *battery_panel = lv_obj_create(ui_TOP_Container); + lv_obj_set_width(battery_panel, 36); + lv_obj_set_height(battery_panel, 16); + lv_obj_set_x(battery_panel, 280); + lv_obj_set_y(battery_panel, 4); + lv_obj_clear_flag(battery_panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(battery_panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(battery_panel, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(battery_panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(battery_panel, cp0_file_path_c("status_battery_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(battery_panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(battery_panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + ui_TOP_Power = lv_bar_create(battery_panel); lv_bar_set_value(ui_TOP_Power, 96, LV_ANIM_OFF); lv_bar_set_start_value(ui_TOP_Power, 0, LV_ANIM_OFF); lv_obj_set_width(ui_TOP_Power, 33); @@ -647,7 +643,6 @@ class home_base : public AppPageRoot lv_obj_set_style_bg_opa(ui_TOP_Power, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT); ui_TOP_power_Label = lv_label_create(ui_TOP_Power); - ::ui_powerLabel = ui_TOP_power_Label; lv_obj_set_width(ui_TOP_power_Label, LV_SIZE_CONTENT); lv_obj_set_height(ui_TOP_power_Label, LV_SIZE_CONTENT); lv_obj_set_align(ui_TOP_power_Label, LV_ALIGN_CENTER); @@ -656,41 +651,40 @@ class home_base : public AppPageRoot lv_obj_set_style_text_opa(ui_TOP_power_Label, 255, LV_PART_MAIN | LV_STATE_DEFAULT); ui_APP_Container = lv_obj_create(root_screen_); - ::ui_APP_Container = ui_APP_Container; lv_obj_remove_style_all(ui_APP_Container); lv_obj_set_size(ui_APP_Container, 320, 150); + lv_obj_set_pos(ui_APP_Container, 0, 20); lv_obj_clear_flag(ui_APP_Container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags } void create_wifi_status(lv_obj_t *parent) { static const int bar_heights[4] = {6, 9, 12, 15}; - lv_obj_t **bars[4] = {&::ui_wifiBar1, &::ui_wifiBar2, &::ui_wifiBar3, &::ui_wifiBar4}; - - ::ui_wifiPanel = lv_obj_create(parent); - lv_obj_set_width(::ui_wifiPanel, 24); - lv_obj_set_height(::ui_wifiPanel, 15); - lv_obj_set_x(::ui_wifiPanel, 210); - lv_obj_set_y(::ui_wifiPanel, 4); - lv_obj_clear_flag(::ui_wifiPanel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(::ui_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_flag(::ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); + + ui_TOP_wifiPanel = lv_obj_create(parent); + lv_obj_set_width(ui_TOP_wifiPanel, 24); + lv_obj_set_height(ui_TOP_wifiPanel, 15); + lv_obj_set_x(ui_TOP_wifiPanel, 210); + lv_obj_set_y(ui_TOP_wifiPanel, 4); + lv_obj_clear_flag(ui_TOP_wifiPanel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(ui_TOP_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(ui_TOP_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(ui_TOP_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(ui_TOP_wifiPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(ui_TOP_wifiPanel, LV_OBJ_FLAG_HIDDEN); for (int i = 0; i < 4; ++i) { - *bars[i] = lv_obj_create(::ui_wifiPanel); - lv_obj_set_width(*bars[i], 4); - lv_obj_set_height(*bars[i], bar_heights[i]); - lv_obj_set_align(*bars[i], LV_ALIGN_BOTTOM_LEFT); - lv_obj_set_x(*bars[i], i * 6); - lv_obj_clear_flag(*bars[i], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(*bars[i], 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(*bars[i], lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(*bars[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(*bars[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_TOP_wifiBars[i] = lv_obj_create(ui_TOP_wifiPanel); + lv_obj_set_width(ui_TOP_wifiBars[i], 4); + lv_obj_set_height(ui_TOP_wifiBars[i], bar_heights[i]); + lv_obj_set_align(ui_TOP_wifiBars[i], LV_ALIGN_BOTTOM_LEFT); + lv_obj_set_x(ui_TOP_wifiBars[i], i * 6); + lv_obj_clear_flag(ui_TOP_wifiBars[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(ui_TOP_wifiBars[i], 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(ui_TOP_wifiBars[i], lv_color_hex(0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(ui_TOP_wifiBars[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(ui_TOP_wifiBars[i], 0, LV_PART_MAIN | LV_STATE_DEFAULT); } } @@ -706,26 +700,25 @@ class home_base : public AppPageRoot void update_wifi_status() { - if (!::ui_wifiPanel) + if (!ui_TOP_wifiPanel) return; cp0_wifi_status_t ws = cp0_wifi_get_status(); - lv_obj_t *bars[4] = {::ui_wifiBar1, ::ui_wifiBar2, ::ui_wifiBar3, ::ui_wifiBar4}; static const int thresholds[4] = {1, 30, 60, 80}; for (int i = 0; i < 4; ++i) { - if (!bars[i]) + if (!ui_TOP_wifiBars[i]) continue; - lv_obj_set_style_bg_color(bars[i], + lv_obj_set_style_bg_color(ui_TOP_wifiBars[i], lv_color_hex(ws.connected && ws.signal >= thresholds[i] ? 0x00CCFF : 0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); } if (ws.connected) - lv_obj_clear_flag(::ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(ui_TOP_wifiPanel, LV_OBJ_FLAG_HIDDEN); else - lv_obj_add_flag(::ui_wifiPanel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_TOP_wifiPanel, LV_OBJ_FLAG_HIDDEN); } void UI_bind_event() diff --git a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp b/projects/APPLaunch/main/ui/components/ui_launch_page.hpp deleted file mode 100644 index e6e55630..00000000 --- a/projects/APPLaunch/main/ui/components/ui_launch_page.hpp +++ /dev/null @@ -1,586 +0,0 @@ -#pragma once -#include "sample_log.h" - -#include "ui_app_page.hpp" -#include -#include -#include -#include "cp0_lvgl_app.h" - -// ==================== standard coordinates for 5 slots ==================== -static const lv_coord_t LP_SLOT_X[] = {-177, -99, 0, 99, 177, -177, -99, 0, 99, 177 }; -static const lv_coord_t LP_SLOT_Y[] = { 4, -6, -16, -6, 4, 57, 57, 50, 57, 57 }; -static const lv_coord_t LP_SLOT_W[] = { 61, 81, 101, 81, 61 }; -static const lv_coord_t LP_SLOT_H[] = { 61, 81, 101, 81, 61 }; - -struct lp_app_item -{ - std::string Name; - std::string Icon; - std::string Exec; - bool terminal; -}; - -class UILaunchPage : public home_base -{ -public: - UILaunchPage() : home_base() - { - // -------- Initialize the app list -------- - app_list_.push_back(lp_app_item{"Python", "A:/dist/images/PYTHON_logo.png", "python3", true }); - app_list_.push_back(lp_app_item{"STORE", "A:/dist/images/Store_logo.png", "launch_store", false}); - app_list_.push_back(lp_app_item{"CLI", "A:/dist/images/CLI_logo.png", "bash", true }); - app_list_.push_back(lp_app_item{"CLAW", "A:/dist/images/CLAW_logo.png", "launch_claw", false}); - app_list_.push_back(lp_app_item{"SETTING", "A:/dist/images/SETTING_logo.png", "launch_setting", false}); - app_list_.push_back(lp_app_item{"STORE1", "A:/dist/images/Store_logo.png", "launch_store1", false}); - app_list_.push_back(lp_app_item{"MUSIC", "A:/dist/images/MUSIC_logo.png", "launch_music", false}); - current_app_ = 2; - - creat_UI(); - init_circles(); - // Initialize indicators - update_indicator(); - } - ~UILaunchPage() {} - - // ==================== Public API ==================== - - // Switch right (content moves left, i.e. page right) - void switch_right() - { - if (is_animating_) { - delay_switch(&snap_timer_right_, [this](){ this->switch_right(); }); - return; - } - is_animating_ = true; - - // 1. Show the panel at pos0 (it is about to slide into view) - lv_obj_clear_flag(circle_[0], LV_OBJ_FLAG_HIDDEN); - - // 2. Move four panels one slot to the right at the same time - leftOuterPanelToLeft_Animation(circle_[0], 0, NULL); - leftPanelToCenter_Animation (circle_[1], 0, NULL); - centerPanelToRight_Animation(circle_[2], 0, NULL); - rightPanelToRightOuter_Animation (circle_[3], 0, [](lv_anim_t *a){ - // Use user data to call back into the object - UILaunchPage *self = (UILaunchPage *)lv_anim_get_user_data(a); - if (self) self->snap_all_panels(); - }); - // The last animation frame needs to carry the this pointer - // Because rightPanelToRightOuter_Animation ready_cb does not support user data, - // use a member timer instead (consistent with the original ui_events.c). - // -- Reuse a 50ms timer to correct positions -- - // Note: the third argument to rightPanelToRightOuter_Animation is NULL; correction is done by the timer - start_snap_timer(); - - // 3. Move the pos4 panel instantly to pos0 - snap_panel_to_slot(circle_[4], 0); - - // 4. Show the label at label pos0 - lv_obj_clear_flag(label_[0], LV_OBJ_FLAG_HIDDEN); - - // 5. Move four labels one slot to the right at the same time - leftOuterLabelToLeft_Animation(label_[0], 0, NULL); - leftLabelToCenter_Animation (label_[1], 0, NULL); - centerLabelToRight_Animation(label_[2], 0, NULL); - rightLabelToRightOuter_Animation (label_[3], 0, NULL); - - // 6. Move the label at pos4 instantly to label pos0 - snap_label_to_slot(label_[4], 5); - update_right(circle_[4], label_[4]); - - // 7. Rotate the arrays (circular) - disable_center_click(); - rotate_right(circle_, 0, 4); - enable_center_click(); - rotate_right(label_, 0, 4); - - // 8. Update indicators - update_indicator(); - } - - // Switch left - void switch_left() - { - if (is_animating_) { - delay_switch(&snap_timer_left_, [this](){ this->switch_left(); }); - return; - } - is_animating_ = true; - - // 1. Show the panel at pos4 - lv_obj_clear_flag(circle_[4], LV_OBJ_FLAG_HIDDEN); - - // 2. Move four panels one slot to the left at the same time - rightOuterPanelToRight_Animation(circle_[4], 0, NULL); - rightPanelToCenter_Animation (circle_[3], 0, NULL); - centerPanelToLeft_Animation(circle_[2], 0, NULL); - leftPanelToLeftOuter_Animation (circle_[1], 0, NULL); - - start_snap_timer(); - - // 3. Move the pos0 panel instantly to pos4 - snap_panel_to_slot(circle_[0], 4); - - // 4. Show the label at label pos4 - lv_obj_clear_flag(label_[4], LV_OBJ_FLAG_HIDDEN); - - // 5. Move four labels one slot to the left at the same time - rightOuterLabelToRight_Animation(label_[4], 0, NULL); - rightLabelToCenter_Animation (label_[3], 0, NULL); - centerLabelToLeft_Animation(label_[2], 0, NULL); - leftLabelToLeftOuter_Animation (label_[1], 0, NULL); - - // 6. Move the label at pos0 instantly to label pos4 - snap_label_to_slot(label_[0], 9); - update_left(circle_[0], label_[0]); - - // 7. Rotate arrays - disable_center_click(); - rotate_left(circle_, 0, 4); - enable_center_click(); - rotate_left(label_, 0, 4); - - // 8. Update indicators - update_indicator(); - } - - // Launch the currently centered app - void launch_app() - { - } - -private: - // ==================== Data members ==================== - std::list app_list_; - int current_app_ = 2; - - // panel array [0..4], label array [0..4] - lv_obj_t *circle_[5] = {}; - lv_obj_t *label_[5] = {}; - - // indicator array (reuses Container child objects from ui_obj) - lv_obj_t *indicator_dots_[8] = {}; - int indicator_count_ = 0; - int indicator_current_ = 0; // currently active dot (relative position for current_app_) - - bool is_animating_ = false; - lv_timer_t *snap_timer_right_ = NULL; - lv_timer_t *snap_timer_left_ = NULL; - lv_timer_t *snap_timer_ = NULL; // generic position correction timer - - std::unordered_map ui_obj_; - - // ==================== UI construction ==================== - void creat_UI() - { - // ---- pos1 left panel ---- - circle_[1] = lv_obj_create(ui_APP_Container); - lv_obj_set_width(circle_[1], 81); - lv_obj_set_height(circle_[1], 81); - lv_obj_set_x(circle_[1], LP_SLOT_X[1]); - lv_obj_set_y(circle_[1], LP_SLOT_Y[1]); - lv_obj_set_align(circle_[1], LV_ALIGN_CENTER); - lv_obj_clear_flag(circle_[1], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - lv_obj_set_style_radius(circle_[1], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(circle_[1], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(circle_[1], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[1], cp0_file_path_c("store_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(circle_[1], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(circle_[1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- pos2 center panel ---- - circle_[2] = lv_obj_create(ui_APP_Container); - lv_obj_set_width(circle_[2], 101); - lv_obj_set_height(circle_[2], 101); - lv_obj_set_x(circle_[2], LP_SLOT_X[2]); - lv_obj_set_y(circle_[2], LP_SLOT_Y[2]); - lv_obj_set_align(circle_[2], LV_ALIGN_CENTER); - lv_obj_clear_flag(circle_[2], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(circle_[2], 22, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(circle_[2], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(circle_[2], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[2], cp0_file_path_c("cli_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(circle_[2], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(circle_[2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- pos3 right panel ---- - circle_[3] = lv_obj_create(ui_APP_Container); - lv_obj_set_width(circle_[3], 81); - lv_obj_set_height(circle_[3], 81); - lv_obj_set_x(circle_[3], LP_SLOT_X[3]); - lv_obj_set_y(circle_[3], LP_SLOT_Y[3]); - lv_obj_set_align(circle_[3], LV_ALIGN_CENTER); - lv_obj_clear_flag(circle_[3], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - lv_obj_set_style_radius(circle_[3], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(circle_[3], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(circle_[3], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[3], cp0_file_path_c("claw_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(circle_[3], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(circle_[3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- pos0 hidden panel(far left outside) ---- - circle_[0] = lv_obj_create(ui_APP_Container); - lv_obj_set_width(circle_[0], LP_SLOT_W[0]); - lv_obj_set_height(circle_[0], LP_SLOT_H[0]); - lv_obj_set_x(circle_[0], LP_SLOT_X[0]); - lv_obj_set_y(circle_[0], LP_SLOT_Y[0]); - lv_obj_set_align(circle_[0], LV_ALIGN_CENTER); - lv_obj_add_flag(circle_[0], LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(circle_[0], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - lv_obj_set_style_radius(circle_[0], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(circle_[0], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(circle_[0], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[0], cp0_file_path_c("python_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(circle_[0], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(circle_[0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- pos4 hidden panel(far right outside) ---- - circle_[4] = lv_obj_create(ui_APP_Container); - lv_obj_set_width(circle_[4], LP_SLOT_W[4]); - lv_obj_set_height(circle_[4], LP_SLOT_H[4]); - lv_obj_set_x(circle_[4], LP_SLOT_X[4]); - lv_obj_set_y(circle_[4], LP_SLOT_Y[4]); - lv_obj_set_align(circle_[4], LV_ALIGN_CENTER); - lv_obj_add_flag(circle_[4], LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(circle_[4], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - lv_obj_set_style_radius(circle_[4], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(circle_[4], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(circle_[4], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[4], cp0_file_path_c("setting_100.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(circle_[4], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(circle_[4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- left label pos6 ---- - label_[1] = lv_label_create(ui_APP_Container); - lv_obj_set_width(label_[1], LV_SIZE_CONTENT); - lv_obj_set_height(label_[1], LV_SIZE_CONTENT); - lv_obj_set_x(label_[1], LP_SLOT_X[6]); - lv_obj_set_y(label_[1], LP_SLOT_Y[6]); - lv_obj_set_align(label_[1], LV_ALIGN_CENTER); - lv_label_set_text(label_[1], "STORE"); - lv_obj_set_style_text_color(label_[1], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(label_[1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- center label pos7 ---- - label_[2] = lv_label_create(ui_APP_Container); - lv_obj_set_width(label_[2], LV_SIZE_CONTENT); - lv_obj_set_height(label_[2], LV_SIZE_CONTENT); - lv_obj_set_x(label_[2], LP_SLOT_X[7]); - lv_obj_set_y(label_[2], LP_SLOT_Y[7]); - lv_obj_set_align(label_[2], LV_ALIGN_CENTER); - lv_label_set_text(label_[2], "CLI"); - lv_obj_set_style_text_color(label_[2], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(label_[2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- right label pos8 ---- - label_[3] = lv_label_create(ui_APP_Container); - lv_obj_set_width(label_[3], LV_SIZE_CONTENT); - lv_obj_set_height(label_[3], LV_SIZE_CONTENT); - lv_obj_set_x(label_[3], LP_SLOT_X[8]); - lv_obj_set_y(label_[3], LP_SLOT_Y[8]); - lv_obj_set_align(label_[3], LV_ALIGN_CENTER); - lv_label_set_text(label_[3], "CLAW"); - lv_obj_set_style_text_color(label_[3], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(label_[3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- left outside label pos5(hidden) ---- - label_[0] = lv_label_create(ui_APP_Container); - lv_obj_set_width(label_[0], LV_SIZE_CONTENT); - lv_obj_set_height(label_[0], LV_SIZE_CONTENT); - lv_obj_set_x(label_[0], LP_SLOT_X[5]); - lv_obj_set_y(label_[0], LP_SLOT_Y[5]); - lv_obj_set_align(label_[0], LV_ALIGN_CENTER); - lv_label_set_text(label_[0], "Python"); - lv_obj_add_flag(label_[0], LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_text_color(label_[0], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(label_[0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- right outside label pos9(hidden) ---- - label_[4] = lv_label_create(ui_APP_Container); - lv_obj_set_width(label_[4], LV_SIZE_CONTENT); - lv_obj_set_height(label_[4], LV_SIZE_CONTENT); - lv_obj_set_x(label_[4], LP_SLOT_X[9]); - lv_obj_set_y(label_[4], LP_SLOT_Y[9]); - lv_obj_set_align(label_[4], LV_ALIGN_CENTER); - lv_label_set_text(label_[4], "SETTING"); - lv_obj_add_flag(label_[4], LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_text_color(label_[4], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(label_[4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - // ---- left/right arrow buttons ---- - ui_obj_["ui_rightButton"] = lv_btn_create(ui_APP_Container); - lv_obj_set_width(ui_obj_["ui_rightButton"], 17); - lv_obj_set_height(ui_obj_["ui_rightButton"], 23); - lv_obj_set_x(ui_obj_["ui_rightButton"], 150); - lv_obj_set_y(ui_obj_["ui_rightButton"], -14); - lv_obj_set_align(ui_obj_["ui_rightButton"], LV_ALIGN_CENTER); - lv_obj_add_flag(ui_obj_["ui_rightButton"], LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_clear_flag(ui_obj_["ui_rightButton"], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_obj_["ui_rightButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_obj_["ui_rightButton"], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_obj_["ui_rightButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_obj_["ui_rightButton"], cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(ui_obj_["ui_rightButton"], lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(ui_obj_["ui_rightButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_event_cb(ui_obj_["ui_rightButton"], [](lv_event_t *e){ - UILaunchPage *self = (UILaunchPage *)lv_event_get_user_data(e); - if (self) self->switch_right(); - }, LV_EVENT_CLICKED, this); - - ui_obj_["ui_leftButton"] = lv_btn_create(ui_APP_Container); - lv_obj_set_width(ui_obj_["ui_leftButton"], 17); - lv_obj_set_height(ui_obj_["ui_leftButton"], 23); - lv_obj_set_x(ui_obj_["ui_leftButton"], -151); - lv_obj_set_y(ui_obj_["ui_leftButton"], -14); - lv_obj_set_align(ui_obj_["ui_leftButton"], LV_ALIGN_CENTER); - lv_obj_add_flag(ui_obj_["ui_leftButton"], LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_clear_flag(ui_obj_["ui_leftButton"], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(ui_obj_["ui_leftButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(ui_obj_["ui_leftButton"], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(ui_obj_["ui_leftButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(ui_obj_["ui_leftButton"], cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(ui_obj_["ui_leftButton"], lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(ui_obj_["ui_leftButton"], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_event_cb(ui_obj_["ui_leftButton"], [](lv_event_t *e){ - UILaunchPage *self = (UILaunchPage *)lv_event_get_user_data(e); - if (self) self->switch_left(); - }, LV_EVENT_CLICKED, this); - - // ---- indicator container ---- - ui_obj_["ui_dot_container"] = lv_obj_create(ui_APP_Container); - lv_obj_remove_style_all(ui_obj_["ui_dot_container"]); - lv_obj_set_width(ui_obj_["ui_dot_container"], 320); - lv_obj_set_height(ui_obj_["ui_dot_container"], 10); - lv_obj_set_x(ui_obj_["ui_dot_container"], 0); - lv_obj_set_y(ui_obj_["ui_dot_container"], 60); - lv_obj_set_align(ui_obj_["ui_dot_container"], LV_ALIGN_CENTER); - lv_obj_clear_flag(ui_obj_["ui_dot_container"], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - lv_obj_set_style_layout(ui_obj_["ui_dot_container"], LV_LAYOUT_FLEX, LV_PART_MAIN); - lv_obj_set_style_flex_flow(ui_obj_["ui_dot_container"], LV_FLEX_FLOW_ROW, LV_PART_MAIN); - lv_obj_set_style_flex_main_place(ui_obj_["ui_dot_container"], LV_FLEX_ALIGN_CENTER, LV_PART_MAIN); - lv_obj_set_style_flex_cross_place(ui_obj_["ui_dot_container"], LV_FLEX_ALIGN_CENTER, LV_PART_MAIN); - lv_obj_set_style_pad_column(ui_obj_["ui_dot_container"], 4, LV_PART_MAIN); - - // Create indicators matching the app_list_ count - indicator_count_ = (int)app_list_.size(); - for (int i = 0; i < indicator_count_; i++) { - indicator_dots_[i] = lv_obj_create(ui_obj_["ui_dot_container"]); - lv_obj_set_width(indicator_dots_[i], 5); - lv_obj_set_height(indicator_dots_[i], 5); - lv_obj_clear_flag(indicator_dots_[i], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(indicator_dots_[i], LV_RADIUS_CIRCLE, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(indicator_dots_[i], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(indicator_dots_[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(indicator_dots_[i], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(indicator_dots_[i], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } - - // ==================== Initialize circle / label array contents ==================== - void init_circles() - { - // Set visible panel icons/labels from current_app_ - // Panel layout: circle_[0]hidden(left-out), [1]left, [2]center, [3]right, [4]hidden(right-out) - // Corresponding app indices: current_app_-2, current_app_-1, current_app_, current_app_+1, current_app_+2 - int sz = (int)app_list_.size(); - auto app_at = [&](int idx) -> lp_app_item & { - idx = ((idx % sz) + sz) % sz; - return *std::next(app_list_.begin(), idx); - }; - - // Initialize icons for 5 panels - lv_obj_set_style_bg_img_src(circle_[0], app_at(current_app_ - 2).Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[1], app_at(current_app_ - 1).Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[2], app_at(current_app_ ).Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[3], app_at(current_app_ + 1).Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(circle_[4], app_at(current_app_ + 2).Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - - // Initialize text for 5 labels - lv_label_set_text(label_[0], app_at(current_app_ - 2).Name.c_str()); - lv_label_set_text(label_[1], app_at(current_app_ - 1).Name.c_str()); - lv_label_set_text(label_[2], app_at(current_app_ ).Name.c_str()); - lv_label_set_text(label_[3], app_at(current_app_ + 1).Name.c_str()); - lv_label_set_text(label_[4], app_at(current_app_ + 2).Name.c_str()); - } - - // ==================== Position correction (called after animation ends) ==================== - void snap_all_panels() - { - for (int i = 0; i < 5; i++) { - snap_panel_to_slot(circle_[i], i); - } - for (int i = 0; i < 5; i++) { - snap_label_to_slot(label_[i], i + 5); - } - is_animating_ = false; - } - - static void snap_timer_cb_(lv_timer_t *timer) - { - UILaunchPage *self = (UILaunchPage *)lv_timer_get_user_data(timer); - if (self) self->snap_all_panels(); - // lv_timer_set_repeat_count set to 1 for automatic deletion - } - - void start_snap_timer() - { - if (snap_timer_) return; - snap_timer_ = lv_timer_create(snap_timer_cb_, 50, this); - lv_timer_set_repeat_count(snap_timer_, 1); - // Clear the pointer after automatic deletion - // Because snap_all_panels() is called in the callback, clear it there too - } - - // ==================== Delayed switching (debounce) ==================== - struct DelayData { - UILaunchPage *self; - lv_timer_t **timer_ptr; - bool is_right_switch; - }; - - static void delay_timer_cb_(lv_timer_t *timer) - { - DelayData *d = (DelayData *)lv_timer_get_user_data(timer); - UILaunchPage *self = d->self; - bool is_right_switch = d->is_right_switch; - *(d->timer_ptr) = NULL; - lv_free(d); - if (is_right_switch) self->switch_right(); - else self->switch_left(); - } - - template - void delay_switch(lv_timer_t **timer_ptr, Fn /*fn*/) - { - // Do not create another wait timer if one already exists - if (*timer_ptr) return; - bool is_right_switch = (timer_ptr == &snap_timer_right_); - DelayData *d = (DelayData *)lv_malloc(sizeof(DelayData)); - d->self = this; - d->timer_ptr = timer_ptr; - d->is_right_switch = is_right_switch; - *timer_ptr = lv_timer_create(delay_timer_cb_, 50, d); - lv_timer_set_repeat_count(*timer_ptr, 1); - } - - // ==================== Slot snap helpers ==================== - static void snap_panel_to_slot(lv_obj_t *panel, int slot) - { - lv_obj_set_x(panel, LP_SLOT_X[slot]); - lv_obj_set_y(panel, LP_SLOT_Y[slot]); - lv_obj_set_width(panel, LP_SLOT_W[slot < 5 ? slot : 4]); - lv_obj_set_height(panel, LP_SLOT_H[slot < 5 ? slot : 4]); - if (slot == 0 || slot == 4) { - lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); - } - } - - static void snap_label_to_slot(lv_obj_t *label, int slot) - { - lv_obj_set_x(label, LP_SLOT_X[slot]); - lv_obj_set_y(label, LP_SLOT_Y[slot]); - if (slot == 5 || slot == 9) { - lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_clear_flag(label, LV_OBJ_FLAG_HIDDEN); - } - } - - // ==================== Circular array rotation ==================== - static void rotate_left(lv_obj_t **arr, int start, int end) - { - lv_obj_t *tmp = arr[start]; - for (int i = start; i < end; i++) arr[i] = arr[i + 1]; - arr[end] = tmp; - } - - static void rotate_right(lv_obj_t **arr, int start, int end) - { - lv_obj_t *tmp = arr[end]; - for (int i = end; i > start; i--) arr[i] = arr[i - 1]; - arr[start] = tmp; - } - - // ==================== Center panel clickability ==================== - void disable_center_click() - { - lv_obj_clear_flag(circle_[2], LV_OBJ_FLAG_CLICKABLE); - } - void enable_center_click() - { - lv_obj_add_flag(circle_[2], LV_OBJ_FLAG_CLICKABLE); - } - - // ==================== Update indicators ==================== - void update_indicator() - { - // The active dot follows current_app_ - for (int i = 0; i < indicator_count_; i++) { - if (i == current_app_) { - // Active: larger and brighter - lv_obj_set_width(indicator_dots_[i], 10); - lv_obj_set_height(indicator_dots_[i], 10); - lv_obj_set_style_bg_color(indicator_dots_[i], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(indicator_dots_[i], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_width(indicator_dots_[i], 5); - lv_obj_set_height(indicator_dots_[i], 5); - lv_obj_set_style_bg_color(indicator_dots_[i], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(indicator_dots_[i], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - } - } - } - - // ==================== Update hidden panel icons/labels (fill new content after switching) ==================== - void update_right(lv_obj_t *panel, lv_obj_t *label) - { - // When switching right, panel/label come from old pos4 and move to pos0 (circular right direction) - // current_app_ is already updated before rotate (right direction decrements current_app_) - // Here the panel corresponds to current_app_-2 (the element filled into the far left from the right side) - current_app_ = current_app_ == 0 ? (int)app_list_.size() - 1 : current_app_ - 1; - int sz = (int)app_list_.size(); - int prev2 = ((current_app_ - 2) % sz + sz) % sz; - auto it = std::next(app_list_.begin(), prev2); - lv_label_set_text(label, it->Name.c_str()); - lv_obj_set_style_bg_img_src(panel, it->Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - } - - void update_left(lv_obj_t *panel, lv_obj_t *label) - { - // When switching left, panel/label come from old pos0 and move to pos4 (circular left direction) - current_app_ = current_app_ == (int)app_list_.size() - 1 ? 0 : current_app_ + 1; - int sz = (int)app_list_.size(); - int next2 = (current_app_ + 2) % sz; - auto it = std::next(app_list_.begin(), next2); - lv_label_set_text(label, it->Name.c_str()); - lv_obj_set_style_bg_img_src(panel, it->Icon.c_str(), LV_PART_MAIN | LV_STATE_DEFAULT); - } - - // ==================== App launch helper ==================== - void launch_exec_in_terminal(lp_app_item *it) - { - SLOGI("Launching terminal app: %s", it->Exec.c_str()); - // Simple implementation: fork+exec directly without terminal UI - launch_exec(it); - } - - void launch_exec(lp_app_item *it) - { - SLOGI("Launching external app: %s", it->Exec.c_str()); - lv_disp_t *disp = lv_disp_get_default(); - lv_indev_t *indev = lv_indev_get_next(NULL); - if (indev) lv_indev_set_group(indev, NULL); - lv_timer_enable(false); - lv_refr_now(disp); - - int ret = cp0_process_exec_blocking(it->Exec.c_str(), &LVGL_HOME_KEY_FLAG, 0); - SLOGI("App %s exited with code %d", it->Exec.c_str(), ret); - lv_timer_enable(true); - if (indev) lv_indev_set_group(lv_indev_get_next(NULL), get_key_group()); - lv_disp_load_scr(ui_Screen1); - lv_refr_now(disp); - } -}; diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp index 39c15f7c..c12a0325 100644 --- a/projects/APPLaunch/main/ui/ui.cpp +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -19,15 +19,6 @@ #error "LV_COLOR_16_SWAP should be 0 to match SquareLine Studio's settings" #endif -// LVGL objects exported through ui_obj.h. -#undef UI_DEFINE_OBJECT -#undef UI_DEFINE_EVENT_FUN -#define UI_DEFINE_OBJECT(x) lv_obj_t *x; -#define UI_DEFINE_EVENT_FUN(x) -#include "ui_obj.h" -#undef UI_DEFINE_OBJECT -#undef UI_DEFINE_EVENT_FUN - std::unique_ptr home; namespace launcher_ui { diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index d2a0a510..e8449b81 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -13,14 +13,6 @@ #include "keyboard_input.h" #include "cp0_lvgl_app.h" -#undef UI_DEFINE_OBJECT -#undef UI_DEFINE_EVENT_FUN -#define UI_DEFINE_OBJECT(x) extern lv_obj_t *x; -#define UI_DEFINE_EVENT_FUN(x) void x(lv_event_t *e); -#include "ui_obj.h" -#undef UI_DEFINE_OBJECT -#undef UI_DEFINE_EVENT_FUN - // Launcher layout constants #define BORDER_COLOR_CENTER 0x444444 #define BORDER_COLOR_SIDE 0x222222 diff --git a/projects/APPLaunch/main/ui/ui_obj.h b/projects/APPLaunch/main/ui/ui_obj.h deleted file mode 100644 index bcd7013e..00000000 --- a/projects/APPLaunch/main/ui/ui_obj.h +++ /dev/null @@ -1,15 +0,0 @@ -UI_DEFINE_OBJECT(ui_Screen1) -UI_DEFINE_OBJECT(ui_Image1) -UI_DEFINE_OBJECT(ui_wifiPanel) -UI_DEFINE_OBJECT(ui_wifiBar1) -UI_DEFINE_OBJECT(ui_wifiBar2) -UI_DEFINE_OBJECT(ui_wifiBar3) -UI_DEFINE_OBJECT(ui_wifiBar4) -UI_DEFINE_OBJECT(ui_Panel1) -UI_DEFINE_OBJECT(ui_timeLabel) -UI_DEFINE_OBJECT(ui_batteryPanel) -UI_DEFINE_OBJECT(ui_Bar1) -UI_DEFINE_OBJECT(ui_powerLabel) -UI_DEFINE_OBJECT(ui_APP_Container) -UI_DEFINE_OBJECT(ui_leftButton) -UI_DEFINE_OBJECT(ui_rightButton) From 9cbcea658d3260f3ffc1530d7f58b2e756fe9790 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:09:08 +0800 Subject: [PATCH 36/70] refactor(APPLaunch): use cp0 file paths directly --- projects/APPLaunch/main/ui/Launch.cpp | 34 +++++++++---------- .../ui/components/page_app/ui_app_setup.hpp | 14 ++++---- .../main/ui/components/ui_app_page.hpp | 9 ----- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 28923be7..512ffeb9 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -23,8 +23,6 @@ #include #include -/* img_path() now defined in ui_app_page.hpp */ - #define PANEL_BORDER_CENTER 0x444444 #define PANEL_BORDER_SIDE 0x222222 #define PANEL_PAD_CENTER 0 @@ -143,17 +141,17 @@ class LaunchImpl { // Fixed icon; users cannot modify it app_list.emplace_back("Python", - img_path("python_100.png"), "python3", true, false); + cp0_file_path("python_100.png"), "python3", true, false); app_list.emplace_back("STORE", - img_path("store_100.png"), + cp0_file_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); app_list.emplace_back("CLI", - img_path("cli_100.png"), "bash", true, false); + cp0_file_path("cli_100.png"), "bash", true, false); app_list.emplace_back("GAME", - img_path("game_100.png"), page_v); + cp0_file_path("game_100.png"), page_v); app_list.emplace_back("SETTING", - img_path("setting_100.png"), page_v); + cp0_file_path("setting_100.png"), page_v); { auto it = std::next(app_list.begin(), 0); @@ -190,40 +188,40 @@ class LaunchImpl if (APP_ENABLED("Music")) app_list.emplace_back("MUSIC", - img_path("music_100.png"), page_v); + cp0_file_path("music_100.png"), page_v); if (APP_ENABLED("Math")) app_list.emplace_back("MATH", - img_path("math_100.png"), + cp0_file_path("math_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); app_list.emplace_back("Compass", - img_path("compass_needle_80.png"), page_v); + cp0_file_path("compass_needle_80.png"), page_v); #if defined(__linux__) && !defined(HAL_PLATFORM_SDL) if (APP_ENABLED("IP_Panel")) app_list.emplace_back("IP_PANEL", - img_path("ip_panel_100.png"), page_v); + cp0_file_path("ip_panel_100.png"), page_v); if (APP_ENABLED("File")) app_list.emplace_back("FILE", - img_path("file_100.png"), page_v); + cp0_file_path("file_100.png"), page_v); if (APP_ENABLED("SSH")) app_list.emplace_back("SSH", - img_path("ssh_100.png"), page_v); + cp0_file_path("ssh_100.png"), page_v); if (APP_ENABLED("Mesh")) app_list.emplace_back("MESH", - img_path("mesh_100.png"), page_v); + cp0_file_path("mesh_100.png"), page_v); if (APP_ENABLED("Rec")) app_list.emplace_back("REC", - img_path("rec_100.png"), page_v); + cp0_file_path("rec_100.png"), page_v); if (APP_ENABLED("Camera")) app_list.emplace_back("CAMERA", - img_path("camera_100.png"), page_v); + cp0_file_path("camera_100.png"), page_v); if (APP_ENABLED("LoRa")) - app_list.emplace_back("LORA", img_path("lora_100.png"), page_v); + app_list.emplace_back("LORA", cp0_file_path("lora_100.png"), page_v); if (APP_ENABLED("Tank")) - app_list.emplace_back("TANK", img_path("tank_100.png"), page_v); + app_list.emplace_back("TANK", cp0_file_path("tank_100.png"), page_v); #endif #undef APP_ENABLED diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp index 534ae0e3..07dca601 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp @@ -128,13 +128,13 @@ class UISetupPage : public AppPage void cache_image_paths() { - img_arrow_up_ = img_path("setting_red_up.png"); - img_arrow_down_ = img_path("setting_red_down.png"); - img_right_arrow_ = img_path("setting_right_arrow.png"); - img_ok_ = img_path("setting_ok.png"); - img_cross_ = img_path("setting_cross.png"); - snd_enter_ = audio_path("key_enter.wav"); - snd_back_ = audio_path("key_back.wav"); + img_arrow_up_ = cp0_file_path("setting_red_up.png"); + img_arrow_down_ = cp0_file_path("setting_red_down.png"); + img_right_arrow_ = cp0_file_path("setting_right_arrow.png"); + img_ok_ = cp0_file_path("setting_ok.png"); + img_cross_ = cp0_file_path("setting_cross.png"); + snd_enter_ = cp0_file_path("key_enter.wav"); + snd_back_ = cp0_file_path("key_back.wav"); } // ==================== Menu init ==================== diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 6a41e0f1..49068d05 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -19,15 +19,6 @@ #include "cp0_lvgl_file.hpp" #define APP_CONSOLE_EXIT_EVENT (lv_event_code_t)(LV_EVENT_LAST + 1) -static inline std::string img_path(const char *name) -{ - return cp0_file_path(name); -} -static inline std::string audio_path(const char *name) -{ - return cp0_file_path(name); -} - class UIAppTopBar { public: From 15d6a654d38ebd5d4a11c81b415a7a69a145797a Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:11:23 +0800 Subject: [PATCH 37/70] Fix SDL battery event updates --- .../cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp | 17 ++++---- .../cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp | 43 +++++++++++++++++++ ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c | 1 + ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h | 1 + .../ui/components/page_app/ui_app_compass.hpp | 4 +- .../main/ui/components/ui_app_page.hpp | 2 +- 6 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp index b756efb1..1bed3cfc 100644 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp +++ b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp @@ -1,5 +1,4 @@ #include "hal/hal_settings.h" -#include #include #include #include @@ -8,14 +7,14 @@ hal_battery_info_t hal_battery_read(void) { hal_battery_info_t info; memset(&info, 0, sizeof(info)); - const time_t now = time(NULL); - constexpr double kPi = 3.14159265358979323846; - constexpr double kBatteryPeriodSeconds = 20.0; - const double phase = (static_cast(now % static_cast(kBatteryPeriodSeconds)) / - kBatteryPeriodSeconds) * - (2.0 * kPi) - - (kPi / 2.0); - const int soc = static_cast((std::sin(phase) + 1.0) * 50.0 + 0.5); + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + constexpr int kMinSoc = 55; + constexpr int kMaxSoc = 96; + constexpr int kRange = kMaxSoc - kMinSoc; + const long tick_100ms = now.tv_sec * 10L + now.tv_nsec / 100000000L; + const int step = static_cast(tick_100ms % (kRange * 2)); + const int soc = (step <= kRange) ? (kMaxSoc - step) : (kMinSoc + step - kRange); info.voltage_mv = 3300 + soc * 9; info.current_ma = soc < 50 ? 200 : -200; diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp new file mode 100644 index 00000000..19fa7945 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp @@ -0,0 +1,43 @@ +#include "hal_lvgl_bsp.h" +#include "lvgl/lvgl.h" +#include "cp0_lvgl_app.h" +#include +#include + +class BatterySystem +{ +public: + void pub() + { + if (lv_c_event[CP0_C_EVENT_BATTERY] == 0) + return; + + cp0_battery_info_t info = cp0_battery_read(); + if (!info.valid) + return; + + lv_obj_t *root = lv_display_get_screen_active(NULL); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_BATTERY], (void *)&info); + } +}; + +static void battery_timer_cb(lv_timer_t *timer) +{ + auto *battery = static_cast(lv_timer_get_user_data(timer)); + if (battery != nullptr) + battery->pub(); +} + +extern "C" void init_battery() +{ + static std::shared_ptr battery; + if (battery) + return; + + battery = std::make_shared(); + BatterySystem *battery_ptr = battery.get(); + cp0_signal_battery_pub.append([battery_ptr](std::function fun) + { battery_ptr->pub(); }); + lv_timer_create(battery_timer_cb, 100, battery_ptr); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c index c4720619..036b6a9e 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c @@ -15,5 +15,6 @@ void cp0_lvgl_init(void) init_sdl_disp(); init_sdl_input(); init_audio(); + init_battery(); init_camera(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h index adee8709..0706ff4a 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h @@ -7,6 +7,7 @@ extern "C" void init_sdl_disp(); void init_sdl_input(); void init_audio(); +void init_battery(); void init_camera(void); #ifdef __cplusplus } diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp index 033dd1be..803db17f 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp @@ -41,7 +41,7 @@ class UICompassPage : public AppPageRoot static constexpr int kStatusH = 30; static constexpr int kBottomH = 25; static constexpr int kBtnW = kScreenW / 5; - static constexpr int kCompassDia = 116; + static constexpr int kCompassDia = 120; static constexpr int kLevelDia = 100; static constexpr uint32_t kColorBg = 0x000000; @@ -189,7 +189,7 @@ class UICompassPage : public AppPageRoot compass_disc_ = lv_img_create(parent); lv_img_set_src(compass_disc_, cp0_file_path("compass_disc_transparent.png").c_str()); - lv_obj_set_pos(compass_disc_, 12, kStatusH + 1); + lv_obj_set_pos(compass_disc_, 20, kStatusH + 2); lv_obj_set_size(compass_disc_, kCompassDia, kCompassDia); lv_img_set_pivot(compass_disc_, kCompassDia / 2, kCompassDia / 2); lv_obj_clear_flag(compass_disc_, LV_OBJ_FLAG_SCROLLABLE); diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index 49068d05..bb5b27bc 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -702,7 +702,7 @@ class home_base : public AppPageRoot if (!ui_TOP_wifiBars[i]) continue; lv_obj_set_style_bg_color(ui_TOP_wifiBars[i], - lv_color_hex(ws.connected && ws.signal >= thresholds[i] ? 0x00CCFF : 0x4D4D4D), + lv_color_hex(ws.connected && ws.signal >= thresholds[i] ? 0x33CC33 : 0x4D4D4D), LV_PART_MAIN | LV_STATE_DEFAULT); } From 37f29c176f47064f946d478148d95730ce9d9dff Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:22:17 +0800 Subject: [PATCH 38/70] Fix camera page top offset --- .../APPLaunch/main/ui/components/page_app/ui_app_camera.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp index ff57f5b5..b1084ac1 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp @@ -343,7 +343,7 @@ class UICameraPage : public AppPage { lv_obj_clean(ui_APP_Container); lv_obj_set_height(ui_APP_Container, camera_app::kContentH); - lv_obj_set_y(ui_APP_Container, 10); + lv_obj_set_y(ui_APP_Container, 20); lv_obj_clear_flag(ui_APP_Container, LV_OBJ_FLAG_SCROLLABLE); page_camera_ = make_page(); From 5c7d3a1d2c9591ec387e8be9f1c3118d16d52093 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:24:54 +0800 Subject: [PATCH 39/70] fix(APPLaunch): align recorder page controls --- .../main/ui/components/page_app/ui_app_rec.hpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp index 0fb5f4e0..a00e8eb9 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp +++ b/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp @@ -163,13 +163,13 @@ class rec_page : public AppPage void init_APP_UI() { - lv_obj_set_height(ui_APP_Container, 120); - lv_obj_set_y(ui_APP_Container, 5); + lv_obj_set_height(ui_APP_Container, 125); + lv_obj_set_y(ui_APP_Container, 20); lv_obj_clear_flag(ui_APP_Container, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *bg = lv_obj_create(ui_APP_Container); lv_obj_remove_style_all(bg); - lv_obj_set_size(bg, 320, 120); + lv_obj_set_size(bg, 320, 125); lv_obj_set_pos(bg, 0, 0); lv_obj_set_style_bg_color(bg, lv_color_hex(0x0D1117), 0); lv_obj_set_style_bg_opa(bg, LV_OPA_COVER, 0); @@ -303,7 +303,7 @@ class rec_page : public AppPage namespace rec_ui2 { static constexpr int kScreenW = 320; -static constexpr int kContentH = 120; +static constexpr int kContentH = 125; static constexpr int kBtnCount = 5; static constexpr int kWaveBarCount = 40; static constexpr uint32_t kBg = 0x0D1117; @@ -852,7 +852,7 @@ class UIRecPage : public rec_page { lv_obj_clean(ui_APP_Container); lv_obj_set_height(ui_APP_Container, rec_ui2::kContentH); - lv_obj_set_y(ui_APP_Container, 5); + lv_obj_set_y(ui_APP_Container, 20); lv_obj_clear_flag(ui_APP_Container, LV_OBJ_FLAG_SCROLLABLE); for (size_t i = 0; i < pages_.size(); ++i) @@ -921,6 +921,13 @@ class UIRecPage : public rec_page if (!key || key->key_state != 0) return; + if (key->key_code == KEY_ESC) + { + if (button_actions_[0]) + button_actions_[0](); + return; + } + int index = -1; switch (key->key_code) { From b2216bb3fb2286626570b9d93e374b490c9e7af8 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:25:49 +0800 Subject: [PATCH 40/70] Use bold font for home title --- projects/APPLaunch/main/ui/UILaunchPage.cpp | 1 + projects/APPLaunch/main/ui/components/ui_app_page.hpp | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index e7a552ca..920363fd 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -521,6 +521,7 @@ void UILaunchPage::init_input_group() void UILaunchPage::show_home_screen() { SLOGI("[HOME] show_home_screen() - loading launcher home screen"); + use_bold_home_title_font(); lv_disp_load_scr(screen()); UILaunchPage::bind_home_input_group(); } diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/components/ui_app_page.hpp index bb5b27bc..4b3ca7bb 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/components/ui_app_page.hpp @@ -517,6 +517,17 @@ class home_base : public AppPageRoot update_wifi_status(); } + void use_bold_home_title_font() + { +#ifndef APPLAUNCH_LOGO_USE_PNG + if (!ui_TOP_logo) + return; + lv_obj_set_style_text_font(ui_TOP_logo, + launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), + LV_PART_MAIN | LV_STATE_DEFAULT); +#endif + } + void update_battery_status(const cp0_battery_info_t &bat) { if (bat.valid) From fbb9dae198fe953a1a6a457c01c4d7def1b1aa9f Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:39:15 +0800 Subject: [PATCH 41/70] refactor(APPLaunch): flatten UI components and add SPDX headers --- .gitignore | 3 +- projects/APPLaunch/main/Kconfig | 4 + projects/APPLaunch/main/SConstruct | 6 +- projects/APPLaunch/main/src/main.cpp | 6 + .../APPLaunch/main/ui/Animation/Animation.hpp | 6 + .../ui/Animation/ui_launcher_animation.cpp | 6 + .../main/ui/Animation/ui_launcher_animation.h | 6 + projects/APPLaunch/main/ui/Launch.cpp | 8 +- projects/APPLaunch/main/ui/Launch.h | 6 + projects/APPLaunch/main/ui/UILaunchPage.cpp | 605 +++++++++--------- projects/APPLaunch/main/ui/UILaunchPage.h | 38 +- .../generate_page_app_includes.py | 14 +- .../page_app/ui_app_IpPanel.hpp | 6 + .../page_app/ui_app_camera.hpp | 6 + .../page_app/ui_app_compass.hpp | 8 +- .../page_app/ui_app_console.hpp | 6 + .../{components => }/page_app/ui_app_file.hpp | 6 + .../{components => }/page_app/ui_app_game.hpp | 6 + .../{components => }/page_app/ui_app_lora.hpp | 6 + .../{components => }/page_app/ui_app_mesh.hpp | 6 + .../page_app/ui_app_music.hpp | 6 + .../{components => }/page_app/ui_app_rec.hpp | 6 + .../page_app/ui_app_setup.hpp | 6 + .../{components => }/page_app/ui_app_ssh.hpp | 7 + .../page_app/ui_app_tank_battle.hpp | 6 + projects/APPLaunch/main/ui/ui.cpp | 6 + projects/APPLaunch/main/ui/ui.h | 11 + .../main/ui/{components => }/ui_app_page.hpp | 36 +- projects/APPLaunch/main/ui/ui_global_hint.cpp | 6 + projects/APPLaunch/main/ui/ui_global_hint.h | 6 + projects/APPLaunch/main/ui/ui_loading.cpp | 6 + projects/APPLaunch/main/ui/ui_loading.h | 6 + projects/APPLaunch/main/ui/zero_lvgl_os.cpp | 6 + projects/APPLaunch/main/ui/zero_lvgl_os.h | 6 + 34 files changed, 561 insertions(+), 317 deletions(-) rename projects/APPLaunch/main/ui/{components => }/generate_page_app_includes.py (83%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_IpPanel.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_camera.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_compass.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_console.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_file.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_game.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_lora.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_mesh.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_music.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_rec.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_setup.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_ssh.hpp (98%) rename projects/APPLaunch/main/ui/{components => }/page_app/ui_app_tank_battle.hpp (99%) rename projects/APPLaunch/main/ui/{components => }/ui_app_page.hpp (96%) diff --git a/.gitignore b/.gitignore index 8fad5c7c..1ca928b4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,10 +9,11 @@ doc tools setup.ini pi -projects/APPLaunch/main/ui/components/page_app.h +projects/APPLaunch/main/ui/page_app.h .agents dist.bak data docs/*.html skills-lock.json *.bak +projects/APPLaunch/docs/UILaunchPage_carousel_animation.html diff --git a/projects/APPLaunch/main/Kconfig b/projects/APPLaunch/main/Kconfig index e69de29b..e7e7458e 100644 --- a/projects/APPLaunch/main/Kconfig +++ b/projects/APPLaunch/main/Kconfig @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +# +# SPDX-License-Identifier: MIT + diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index d1c6768a..8b88d84e 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +# +# SPDX-License-Identifier: MIT + import os import sys import platform @@ -26,7 +30,7 @@ LINK_SEARCH_PATH = [] STATIC_FILES = [] # run generate_page_app_includes.py to generate the header file for including app pages in the UI. -script_path = os.path.abspath(str(AFile('ui/components/generate_page_app_includes.py'))) +script_path = os.path.abspath(str(AFile('ui/generate_page_app_includes.py'))) script_dir = os.path.dirname(script_path) subprocess.run( [sys.executable, script_path], diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index 345b4891..295eee03 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #include "lvgl/lvgl.h" #include "lvgl/demos/lv_demos.h" #include diff --git a/projects/APPLaunch/main/ui/Animation/Animation.hpp b/projects/APPLaunch/main/ui/Animation/Animation.hpp index 7a6ebeaa..027f96d9 100644 --- a/projects/APPLaunch/main/ui/Animation/Animation.hpp +++ b/projects/APPLaunch/main/ui/Animation/Animation.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "lvgl.h" diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp index 5f7b7830..c6d34ef8 100644 --- a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #include "ui_launcher_animation.h" #include "Animation.hpp" diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h index 7c69d827..88b5f03f 100644 --- a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h +++ b/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "lvgl/lvgl.h" diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 512ffeb9..45336482 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #include "Launch.h" #include "ui.h" #include "UILaunchPage.h" #include "ui_loading.h" -#include "components/page_app.h" +#include "page_app.h" #include "cp0_lvgl_app.h" #include "cp0_lvgl_file.hpp" #include "sample_log.h" diff --git a/projects/APPLaunch/main/ui/Launch.h b/projects/APPLaunch/main/ui/Launch.h index f3429bc0..6215358b 100644 --- a/projects/APPLaunch/main/ui/Launch.h +++ b/projects/APPLaunch/main/ui/Launch.h @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "lvgl/lvgl.h" diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 920363fd..71704336 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #include "UILaunchPage.h" #include "Launch.h" @@ -26,17 +32,8 @@ static void rotate_carousel_right(size_t start, size_t end) namespace { -typedef void (*switch_cb_t)(lv_event_t *); - UILaunchPage *active_launch_page = nullptr; - -static void switch_left(lv_event_t *e); -static void switch_right(lv_event_t *e); -static void app_launch(lv_event_t *e); -static void main_key_switch(lv_event_t *e); - -lv_obj_t *left_arrow_button = nullptr; -lv_obj_t *right_arrow_button = nullptr; +lv_group_t *home_input_group = nullptr; // ==================== standard layout for carousel slots ==================== @@ -61,13 +58,6 @@ static const CarouselSlot CAROUSEL_SLOTS[] = { {177, LABEL_Y_SIDE, 0, 0, true}, }; -static bool is_animating = false; -static switch_cb_t pending_switch = NULL; - -static int Panel_current_pos = 2; -static int switch_current_pos = UILaunchPage::kPageDot2; - - // ============================================================ // audio // ============================================================ @@ -182,147 +172,6 @@ static void snap_label_to_slot(lv_obj_t *label, int slot) } } - -// ============================================================ -// Correct all panel positions after animation ends -// ============================================================ - -static void snap_all_panels() -{ - for (int i = 0; i < 5; i++) - { - snap_panel_to_slot(UILaunchPage::carousel_elements[i], i); - } - - for (int i = 5; i < 10; i++) - { - snap_label_to_slot(UILaunchPage::carousel_elements[i], i); - } - - is_animating = false; - - // Reset border colors: center=bright, sides=dark - for (int i = 0; i < 5; i++) { - uint32_t color = (i == 2) ? BORDER_COLOR_CENTER : BORDER_COLOR_SIDE; - lv_obj_set_style_border_color(UILaunchPage::carousel_elements[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); - } - - // Reset all label fonts to bold - for (int i = 5; i < 10; i++) { - lv_obj_set_style_text_font(UILaunchPage::carousel_elements[i], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - } - - if (pending_switch) { - switch_cb_t cb = pending_switch; - pending_switch = NULL; - cb(NULL); - } -} - - -// ============================================================ -// Switch right; called when the right arrow is clicked -// ============================================================ - -static void switch_right(lv_event_t *e) -{ - if (is_animating) - { - pending_switch = &switch_right; - return; - } - - is_animating = true; - - lv_obj_clear_flag(UILaunchPage::carousel_elements[0], LV_OBJ_FLAG_HIDDEN); - - launcher_home_animation::animate_right(UILaunchPage::carousel_elements.data(), snap_all_panels); - - snap_panel_to_slot(UILaunchPage::carousel_elements[4], 0); - - lv_obj_clear_flag(UILaunchPage::carousel_elements[5], LV_OBJ_FLAG_HIDDEN); - - snap_label_to_slot(UILaunchPage::carousel_elements[9], 5); - - if (active_launch_page) - active_launch_page->update_right_slot(UILaunchPage::carousel_elements[4], UILaunchPage::carousel_elements[9]); - - switchpanleEnableClick(2, 0); - rotate_carousel_right(0, 4); - switchpanleEnableClick(2, 1); - - rotate_carousel_right(5, 9); - - switchpanleEnable(switch_current_pos, 0); - - switch_current_pos = switch_current_pos == UILaunchPage::kPageDot0 ? UILaunchPage::kPageDot4 : switch_current_pos - 1; - - switchpanleEnable(switch_current_pos, 1); -} - - -// ============================================================ -// Switch left; called when the left arrow is clicked -// ============================================================ - -static void switch_left(lv_event_t *e) -{ - if (is_animating) - { - pending_switch = &switch_left; - return; - } - - is_animating = true; - - lv_obj_clear_flag(UILaunchPage::carousel_elements[4], LV_OBJ_FLAG_HIDDEN); - - launcher_home_animation::animate_left(UILaunchPage::carousel_elements.data(), snap_all_panels); - - snap_panel_to_slot(UILaunchPage::carousel_elements[0], 4); - - lv_obj_clear_flag(UILaunchPage::carousel_elements[9], LV_OBJ_FLAG_HIDDEN); - - snap_label_to_slot(UILaunchPage::carousel_elements[5], 9); - - if (active_launch_page) - active_launch_page->update_left_slot(UILaunchPage::carousel_elements[0], UILaunchPage::carousel_elements[5]); - - switchpanleEnableClick(2, 0); - rotate_carousel_left(0, 4); - switchpanleEnableClick(2, 1); - - rotate_carousel_left(5, 9); - - switchpanleEnable(switch_current_pos, 0); - - switch_current_pos = switch_current_pos == UILaunchPage::kPageDot4 ? UILaunchPage::kPageDot0 : switch_current_pos + 1; - - switchpanleEnable(switch_current_pos, 1); -} - - - -// ============================================================ -// screen / app -// ============================================================ - -static void ui_event_Screen1(lv_event_t *e) -{ - if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) - { - main_key_switch(e); - } -} - - -static void app_launch(lv_event_t *e) -{ - if (active_launch_page) - active_launch_page->launch_selected_app(); -} - - static uint32_t fzxc_to_arrow(uint32_t key) { switch (key) @@ -344,101 +193,13 @@ static uint32_t fzxc_to_arrow(uint32_t key) } } - -// ============================================================ -// key handler -// ============================================================ - -static int lvping_lock = 0; - -static void main_key_switch(lv_event_t *e) +static UILaunchPage *page_from_event(lv_event_t *event) { - struct key_item *elm = (struct key_item *)lv_event_get_param(e); - uint32_t code = fzxc_to_arrow(elm->key_code); - - SLOGI("[LAUNCHER] main_key_switch raw=%u->code=%u state=%s sym=%s", - elm->key_code, - code, - kbd_state_name(elm->key_state), - elm->sym_name); - - if (elm->key_state) - { - switch (code) - { - case KEY_UP: - break; - - case KEY_DOWN: - break; - - case KEY_LEFT: - { - /* Play the preloaded sound effect directly before switching pages. */ - if (!lvping_lock) - { - audio_play_switch(); - switch_right(NULL); - } - } - break; - - case KEY_RIGHT: - { - if (!lvping_lock) - { - audio_play_switch(); - switch_left(NULL); - } - } - break; - - default: - break; - } - } - else if (code == KEY_ENTER) - { - audio_play_enter(); - app_launch(NULL); - } - else if (code == KEY_F12) - { - static lv_obj_t *green_bg; - if (lvping_lock == 0) - { - lvping_lock = 1; - green_bg = lv_obj_create(lv_scr_act()); - lv_obj_set_size(green_bg, 320, 170); - lv_obj_align(green_bg, LV_ALIGN_TOP_LEFT, 0, 0); - - lv_obj_set_style_bg_color(green_bg, lv_color_hex(0x00FF00), LV_PART_MAIN); - lv_obj_set_style_bg_opa(green_bg, LV_OPA_COVER, LV_PART_MAIN); - - lv_obj_set_style_border_width(green_bg, 0, LV_PART_MAIN); - lv_obj_set_style_radius(green_bg, 0, LV_PART_MAIN); - lv_obj_set_style_shadow_width(green_bg, 0, LV_PART_MAIN); - lv_obj_set_style_pad_all(green_bg, 0, LV_PART_MAIN); - } - else - { - lvping_lock = 0; - lv_obj_del(green_bg); - } - } + return event ? static_cast(lv_event_get_user_data(event)) : nullptr; } - } // namespace -namespace { - -char gif_path[256]; -lv_group_t *home_input_group = nullptr; - -} // namespace - -lv_obj_t *startup_gif = nullptr; LauncherFonts::~LauncherFonts() { @@ -506,7 +267,7 @@ lv_obj_t *UILaunchPage::label(size_t slot) void UILaunchPage::bind_home_input_group() { - lv_indev_t *indev = lv_indev_get_next(NULL); + lv_indev_t *indev = lv_indev_get_next(nullptr); if (indev) { lv_indev_set_group(indev, home_input_group()); } @@ -532,28 +293,15 @@ void UILaunchPage::load_home_screen() cp0_signal_audio_api_play_asset("startup.mp3"); } -static void ui_event_logo_over(lv_event_t *e) -{ - static int done = 0; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_READY && !done) { - done = 1; - SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); - if (startup_gif) lv_gif_pause(startup_gif); - - if (active_launch_page) - active_launch_page->load_home_screen(); - } -} - void UILaunchPage::start_startup_gif() { - snprintf(gif_path, sizeof(gif_path), "%s", cp0_file_path("logo_output.gif").c_str()); - startup_gif = lv_gif_create(NULL); - lv_gif_set_src(startup_gif, gif_path); - lv_obj_center(startup_gif); - lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); - lv_disp_load_scr(startup_gif); + snprintf(startup_gif_path_.data(), startup_gif_path_.size(), "%s", cp0_file_path("logo_output.gif").c_str()); + startup_gif_done_ = false; + startup_gif_ = lv_gif_create(nullptr); + lv_gif_set_src(startup_gif_, startup_gif_path_.data()); + lv_obj_center(startup_gif_); + lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); + lv_disp_load_scr(startup_gif_); } UILaunchPage::UILaunchPage(std::shared_ptr launch) @@ -564,6 +312,10 @@ UILaunchPage::UILaunchPage(std::shared_ptr launch) UILaunchPage::~UILaunchPage() { + if (green_bg_) { + lv_obj_del(green_bg_); + green_bg_ = nullptr; + } if (active_launch_page == this) active_launch_page = nullptr; } @@ -586,6 +338,246 @@ void UILaunchPage::launch_selected_app() launch_->launch_app(); } +void UILaunchPage::finish_switch_animation() +{ + for (int i = 0; i < 5; i++) + { + snap_panel_to_slot(carousel_elements[i], i); + } + + for (int i = 5; i < 10; i++) + { + snap_label_to_slot(carousel_elements[i], i); + } + + is_animating_ = false; + + for (int i = 0; i < 5; i++) { + uint32_t color = (i == 2) ? BORDER_COLOR_CENTER : BORDER_COLOR_SIDE; + lv_obj_set_style_border_color(carousel_elements[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); + } + + for (int i = 5; i < 10; i++) { + lv_obj_set_style_text_font(carousel_elements[i], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + } + + run_pending_switch(); +} + +void UILaunchPage::run_pending_switch() +{ + PendingSwitch pending = pending_switch_; + pending_switch_ = PendingSwitch::None; + + switch (pending) { + case PendingSwitch::Left: + switch_left(); + break; + case PendingSwitch::Right: + switch_right(); + break; + case PendingSwitch::None: + break; + } +} + +void UILaunchPage::switch_right() +{ + if (is_animating_) + { + pending_switch_ = PendingSwitch::Right; + return; + } + + is_animating_ = true; + + lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); + + launcher_home_animation::animate_right(carousel_elements.data(), [this]() { finish_switch_animation(); }); + + snap_panel_to_slot(carousel_elements[4], 0); + + lv_obj_clear_flag(carousel_elements[5], LV_OBJ_FLAG_HIDDEN); + + snap_label_to_slot(carousel_elements[9], 5); + + update_right_slot(carousel_elements[4], carousel_elements[9]); + + switchpanleEnableClick(2, 0); + rotate_carousel_right(0, 4); + switchpanleEnableClick(2, 1); + + rotate_carousel_right(5, 9); + + switchpanleEnable(switch_current_pos_, 0); + + switch_current_pos_ = switch_current_pos_ == UILaunchPage::kPageDot0 ? UILaunchPage::kPageDot4 : switch_current_pos_ - 1; + + switchpanleEnable(switch_current_pos_, 1); +} + +void UILaunchPage::switch_left() +{ + if (is_animating_) + { + pending_switch_ = PendingSwitch::Left; + return; + } + + is_animating_ = true; + + lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); + + launcher_home_animation::animate_left(carousel_elements.data(), [this]() { finish_switch_animation(); }); + + snap_panel_to_slot(carousel_elements[0], 4); + + lv_obj_clear_flag(carousel_elements[9], LV_OBJ_FLAG_HIDDEN); + + snap_label_to_slot(carousel_elements[5], 9); + + update_left_slot(carousel_elements[0], carousel_elements[5]); + + switchpanleEnableClick(2, 0); + rotate_carousel_left(0, 4); + switchpanleEnableClick(2, 1); + + rotate_carousel_left(5, 9); + + switchpanleEnable(switch_current_pos_, 0); + + switch_current_pos_ = switch_current_pos_ == UILaunchPage::kPageDot4 ? UILaunchPage::kPageDot0 : switch_current_pos_ + 1; + + switchpanleEnable(switch_current_pos_, 1); +} + +void UILaunchPage::handle_home_key(lv_event_t *event) +{ + if (!event) + return; + + struct key_item *elm = static_cast(lv_event_get_param(event)); + if (!elm) + return; + + uint32_t code = fzxc_to_arrow(elm->key_code); + + SLOGI("[LAUNCHER] main_key_switch raw=%u->code=%u state=%s sym=%s", + elm->key_code, + code, + kbd_state_name(elm->key_state), + elm->sym_name); + + if (elm->key_state) + { + switch (code) + { + case KEY_UP: + break; + + case KEY_DOWN: + break; + + case KEY_LEFT: + { + if (!lvping_lock_) + { + audio_play_switch(); + switch_right(); + } + } + break; + + case KEY_RIGHT: + { + if (!lvping_lock_) + { + audio_play_switch(); + switch_left(); + } + } + break; + + default: + break; + } + } + else if (code == KEY_ENTER) + { + audio_play_enter(); + launch_selected_app(); + } + else if (code == KEY_F12) + { + if (lvping_lock_ == 0) + { + lvping_lock_ = 1; + green_bg_ = lv_obj_create(lv_scr_act()); + lv_obj_set_size(green_bg_, 320, 170); + lv_obj_align(green_bg_, LV_ALIGN_TOP_LEFT, 0, 0); + + lv_obj_set_style_bg_color(green_bg_, lv_color_hex(0x00FF00), LV_PART_MAIN); + lv_obj_set_style_bg_opa(green_bg_, LV_OPA_COVER, LV_PART_MAIN); + + lv_obj_set_style_border_width(green_bg_, 0, LV_PART_MAIN); + lv_obj_set_style_radius(green_bg_, 0, LV_PART_MAIN); + lv_obj_set_style_shadow_width(green_bg_, 0, LV_PART_MAIN); + lv_obj_set_style_pad_all(green_bg_, 0, LV_PART_MAIN); + } + else + { + lvping_lock_ = 0; + if (green_bg_) { + lv_obj_del(green_bg_); + green_bg_ = nullptr; + } + } + } +} + +void UILaunchPage::handle_startup_gif_event(lv_event_t *event) +{ + if (!event || lv_event_get_code(event) != LV_EVENT_READY || startup_gif_done_) + return; + + startup_gif_done_ = true; + SLOGI("[GIF] first LV_EVENT_READY -> pause + home_screen_load()"); + if (startup_gif_) + lv_gif_pause(startup_gif_); + + load_home_screen(); +} + +void UILaunchPage::on_left_arrow_clicked(lv_event_t *event) +{ + if (UILaunchPage *self = page_from_event(event)) + self->switch_right(); +} + +void UILaunchPage::on_right_arrow_clicked(lv_event_t *event) +{ + if (UILaunchPage *self = page_from_event(event)) + self->switch_left(); +} + +void UILaunchPage::on_app_clicked(lv_event_t *event) +{ + if (UILaunchPage *self = page_from_event(event)) + self->launch_selected_app(); +} + +void UILaunchPage::on_home_key(lv_event_t *event) +{ + if (UILaunchPage *self = page_from_event(event)) + self->handle_home_key(event); +} + +void UILaunchPage::on_startup_gif_event(lv_event_t *event) +{ + if (UILaunchPage *self = page_from_event(event)) + self->handle_startup_gif_event(event); +} + void UILaunchPage::create_screen() { if (carousel_elements[kCardCenter]) @@ -757,35 +749,35 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_border_color(carousel_elements[kCardFarRight], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(carousel_elements[kCardFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - left_arrow_button = lv_btn_create(app_container); - lv_obj_set_width(left_arrow_button, 17); - lv_obj_set_height(left_arrow_button, 23); - lv_obj_set_x(left_arrow_button, -151); - lv_obj_set_y(left_arrow_button, -4); - lv_obj_set_align(left_arrow_button, LV_ALIGN_CENTER); - lv_obj_add_flag(left_arrow_button, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags - lv_obj_clear_flag(left_arrow_button, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(left_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(left_arrow_button, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(left_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(left_arrow_button, cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(left_arrow_button, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(left_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - right_arrow_button = lv_btn_create(app_container); - lv_obj_set_width(right_arrow_button, 17); - lv_obj_set_height(right_arrow_button, 23); - lv_obj_set_x(right_arrow_button, 150); - lv_obj_set_y(right_arrow_button, -4); - lv_obj_set_align(right_arrow_button, LV_ALIGN_CENTER); - lv_obj_add_flag(right_arrow_button, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags - lv_obj_clear_flag(right_arrow_button, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(right_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(right_arrow_button, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(right_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(right_arrow_button, cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(right_arrow_button, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(right_arrow_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + left_arrow_button_ = lv_btn_create(app_container); + lv_obj_set_width(left_arrow_button_, 17); + lv_obj_set_height(left_arrow_button_, 23); + lv_obj_set_x(left_arrow_button_, -151); + lv_obj_set_y(left_arrow_button_, -4); + lv_obj_set_align(left_arrow_button_, LV_ALIGN_CENTER); + lv_obj_add_flag(left_arrow_button_, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags + lv_obj_clear_flag(left_arrow_button_, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(left_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(left_arrow_button_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(left_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(left_arrow_button_, cp0_file_path_c("carousel_left_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_color(left_arrow_button_, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_opa(left_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + right_arrow_button_ = lv_btn_create(app_container); + lv_obj_set_width(right_arrow_button_, 17); + lv_obj_set_height(right_arrow_button_, 23); + lv_obj_set_x(right_arrow_button_, 150); + lv_obj_set_y(right_arrow_button_, -4); + lv_obj_set_align(right_arrow_button_, LV_ALIGN_CENTER); + lv_obj_add_flag(right_arrow_button_, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags + lv_obj_clear_flag(right_arrow_button_, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(right_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(right_arrow_button_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(right_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(right_arrow_button_, cp0_file_path_c("carousel_right_arrow.png"), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_color(right_arrow_button_, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_opa(right_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); carousel_elements[kCardFarLeft] = lv_obj_create(app_container); lv_obj_set_width(carousel_elements[kCardFarLeft], 61); @@ -825,15 +817,14 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_text_color(carousel_elements[kTitleFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_opa(carousel_elements[kTitleFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_event_cb(carousel_elements[kCardLeft], app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(carousel_elements[kCardCenter], app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(carousel_elements[kCardRight], app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(carousel_elements[kCardFarRight], app_launch, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(left_arrow_button, switch_right, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(right_arrow_button, switch_left, LV_EVENT_CLICKED, NULL); - lv_obj_add_event_cb(carousel_elements[kCardFarLeft], app_launch, LV_EVENT_CLICKED, NULL); - if (active_launch_page) - lv_obj_add_event_cb(active_launch_page->screen(), main_key_switch, (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); + lv_obj_add_event_cb(carousel_elements[kCardLeft], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements[kCardCenter], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements[kCardRight], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements[kCardFarRight], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(left_arrow_button_, on_left_arrow_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(right_arrow_button_, on_right_arrow_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements[kCardFarLeft], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(screen(), on_home_key, (lv_event_code_t)LV_EVENT_KEYBOARD, this); } diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index c8b79e70..b0c23c71 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -1,6 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once -#include "components/ui_app_page.hpp" +#include "ui_app_page.hpp" #include #include @@ -48,7 +54,35 @@ class UILaunchPage : public home_base static std::array carousel_elements; private: - static void create_app_container(lv_obj_t *parent); + enum class PendingSwitch { + None, + Left, + Right, + }; + + void create_app_container(lv_obj_t *parent); + void switch_left(); + void switch_right(); + void finish_switch_animation(); + void run_pending_switch(); + void handle_home_key(lv_event_t *event); + void handle_startup_gif_event(lv_event_t *event); + + static void on_left_arrow_clicked(lv_event_t *event); + static void on_right_arrow_clicked(lv_event_t *event); + static void on_app_clicked(lv_event_t *event); + static void on_home_key(lv_event_t *event); + static void on_startup_gif_event(lv_event_t *event); std::shared_ptr launch_; + lv_obj_t *startup_gif_ = nullptr; + lv_obj_t *left_arrow_button_ = nullptr; + lv_obj_t *right_arrow_button_ = nullptr; + lv_obj_t *green_bg_ = nullptr; + std::array startup_gif_path_ = {}; + bool is_animating_ = false; + bool startup_gif_done_ = false; + int lvping_lock_ = 0; + PendingSwitch pending_switch_ = PendingSwitch::None; + int switch_current_pos_ = kPageDot2; }; diff --git a/projects/APPLaunch/main/ui/components/generate_page_app_includes.py b/projects/APPLaunch/main/ui/generate_page_app_includes.py similarity index 83% rename from projects/APPLaunch/main/ui/components/generate_page_app_includes.py rename to projects/APPLaunch/main/ui/generate_page_app_includes.py index 6f403f3c..7e1f438e 100644 --- a/projects/APPLaunch/main/ui/components/generate_page_app_includes.py +++ b/projects/APPLaunch/main/ui/generate_page_app_includes.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD +# +# SPDX-License-Identifier: MIT + import os PAGE_APP_DIR = "page_app" @@ -19,7 +23,15 @@ def generate_includes(): print(f"No .hpp files found in '{PAGE_APP_DIR}'.") return - new_content = "#pragma once\n\n" + new_content = """/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +""" for hpp_file in hpp_files: new_content += f'#include "{PAGE_APP_DIR}/{hpp_file}"\n' diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp index 37b546a9..46bc2066 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "../ui_app_page.hpp" #include "compat/input_keys.h" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp index b1084ac1..99dd67b4 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "../ui_app_page.hpp" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp index 803db17f..a8826ca6 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp @@ -1,6 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once -#include "../../ui.h" +#include "../ui.h" #include "../ui_app_page.hpp" #include "compat/input_keys.h" #include "hal_lvgl_bsp.h" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_console.hpp index ad05e8df..ef4a07ea 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "sample_log.h" #include "../ui_app_page.hpp" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_file.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_file.hpp index 351cfe5e..6ca13c05 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_file.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "sample_log.h" #include "../ui_app_page.hpp" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_game.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_game.hpp index 3ef9fe22..511cfb1e 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_game.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "../ui_app_page.hpp" #include diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp index 4c4a0015..5d710f12 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "sample_log.h" #if !defined(HAL_PLATFORM_SDL) diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp index 731f24bc..38c6ec95 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "../ui_app_page.hpp" #include diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_music.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_music.hpp index 3cf1fc5a..d2536589 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_music.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "sample_log.h" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp index a00e8eb9..14333d13 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "sample_log.h" diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp index 07dca601..a56246ba 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once // Note: this file used to be wrapped in `#if !defined(HAL_PLATFORM_SDL)` to // exclude it from the emulator build, but ui_app_launch.cpp references diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp similarity index 98% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp index 1f9dc705..92fbc75f 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "sample_log.h" #include "../ui_app_page.hpp" @@ -230,6 +236,7 @@ class UISSHPage : public AppPage console_page_->navigate_home = [this]() { // Return to the SSH input view console_page_.reset(); + view_state_ = ViewState::INPUT; // Switch screen back to our root lv_disp_load_scr(this->screen()); lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); diff --git a/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp similarity index 99% rename from projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp index 82c3b4ce..d55276f7 100644 --- a/projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "ui/ui.h" diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp index c12a0325..357f20a9 100644 --- a/projects/APPLaunch/main/ui/ui.cpp +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + // This file was generated by SquareLine Studio // SquareLine Studio version: SquareLine Studio 1.5.0 // LVGL version: 8.3.11 diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index e8449b81..9bb2a9d4 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + // This file was generated by SquareLine Studio // SquareLine Studio version: SquareLine Studio 1.5.0 // LVGL version: 8.3.11 @@ -61,6 +67,11 @@ inline lv_event_code_t battery_event() return static_cast(lv_c_event[CP0_C_EVENT_BATTERY]); } +inline lv_event_code_t datetime_event() +{ + return static_cast(lv_c_event[CP0_C_EVENT_DATATIME]); +} + inline const cp0_battery_info_t *battery_info(lv_event_t *event) { return static_cast(lv_event_get_param(event)); diff --git a/projects/APPLaunch/main/ui/components/ui_app_page.hpp b/projects/APPLaunch/main/ui/ui_app_page.hpp similarity index 96% rename from projects/APPLaunch/main/ui/components/ui_app_page.hpp rename to projects/APPLaunch/main/ui/ui_app_page.hpp index 4b3ca7bb..547a4a0b 100644 --- a/projects/APPLaunch/main/ui/components/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/ui_app_page.hpp @@ -1,5 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once -#include "../ui.h" +#include "ui.h" #include #include #include @@ -343,6 +349,7 @@ class AppTopBarRegion : virtual public AppPageRoot top_bar_.set_height(top_bar_height_px_); add_bar(top_bar_); UI_bind_event(); + update_datetime_status(); update_status_bar(); status_timer_ = lv_timer_create(app_status_timer_cb, 5000, this); } @@ -355,7 +362,12 @@ class AppTopBarRegion : virtual public AppPageRoot void update_status_bar() { - top_bar_.update_status(); + top_bar_.update_wifi(); + } + + void update_datetime_status() + { + top_bar_.update_time(); } void update_battery_status(const cp0_battery_info_t &bat) @@ -383,6 +395,14 @@ class AppTopBarRegion : virtual public AppPageRoot self->update_battery_status(*bat); } + static void app_datetime_event_cb(lv_event_t *e) + { + AppTopBarRegion *self = static_cast(lv_event_get_user_data(e)); + if (!self || lv_event_get_code(e) != launcher_ui::events::datetime_event()) + return; + self->update_datetime_status(); + } + static void app_status_timer_cb(lv_timer_t *timer) { AppTopBarRegion *self = static_cast(lv_timer_get_user_data(timer)); @@ -393,6 +413,7 @@ class AppTopBarRegion : virtual public AppPageRoot void UI_bind_event() { lv_obj_add_event_cb(root_screen_, app_battery_event_cb, launcher_ui::events::battery_event(), this); + lv_obj_add_event_cb(root_screen_, app_datetime_event_cb, launcher_ui::events::datetime_event(), this); } }; @@ -485,6 +506,7 @@ class home_base : public AppPageRoot { creat_Top_UI(); UI_bind_event(); + update_time_status(); update_status_bar(); status_timer_ = lv_timer_create(home_status_timer_cb, 5000, this); } @@ -504,6 +526,14 @@ class home_base : public AppPageRoot self->update_battery_status(*bat); } + static void home_datetime_event_cb(lv_event_t *e) + { + home_base *self = static_cast(lv_event_get_user_data(e)); + if (!self || lv_event_get_code(e) != launcher_ui::events::datetime_event()) + return; + self->update_time_status(); + } + static void home_status_timer_cb(lv_timer_t *timer) { home_base *self = static_cast(lv_timer_get_user_data(timer)); @@ -513,7 +543,6 @@ class home_base : public AppPageRoot void update_status_bar() { - update_time_status(); update_wifi_status(); } @@ -726,6 +755,7 @@ class home_base : public AppPageRoot void UI_bind_event() { lv_obj_add_event_cb(root_screen_, home_battery_event_cb, launcher_ui::events::battery_event(), this); + lv_obj_add_event_cb(root_screen_, home_datetime_event_cb, launcher_ui::events::datetime_event(), this); } }; diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index 624de7de..0503fe3c 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + /* * ui_global_hint.cpp * diff --git a/projects/APPLaunch/main/ui/ui_global_hint.h b/projects/APPLaunch/main/ui/ui_global_hint.h index ded790a0..c054e46d 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.h +++ b/projects/APPLaunch/main/ui/ui_global_hint.h @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + /* * ui_global_hint.h * diff --git a/projects/APPLaunch/main/ui/ui_loading.cpp b/projects/APPLaunch/main/ui/ui_loading.cpp index 5bdcd44b..b9429aae 100644 --- a/projects/APPLaunch/main/ui/ui_loading.cpp +++ b/projects/APPLaunch/main/ui/ui_loading.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + /* * ui_loading.cpp * diff --git a/projects/APPLaunch/main/ui/ui_loading.h b/projects/APPLaunch/main/ui/ui_loading.h index 8f6335ce..e89dc5c4 100644 --- a/projects/APPLaunch/main/ui/ui_loading.h +++ b/projects/APPLaunch/main/ui/ui_loading.h @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + /* * ui_loading.h * diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp index 7f4551ad..1777167e 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #include "zero_lvgl_os.h" #include "Launch.h" diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.h b/projects/APPLaunch/main/ui/zero_lvgl_os.h index 837e31ef..9f9949f5 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.h +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.h @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + #pragma once #include "lvgl/lvgl.h" From eff09bd705733397f38595d1a4340a26a0a0208e Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 18:39:52 +0800 Subject: [PATCH 42/70] feat(cp0_lvgl): publish datetime events on timer --- ext_components/cp0_lvgl/src/commount.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index 66fe2511..0dac425e 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -6,6 +6,17 @@ uint32_t lv_c_event[(2*CP0_C_EVENT_END)] = {0}; +static void datetime_timer_cb(lv_timer_t *timer) +{ + (void)timer; + if (lv_c_event[CP0_C_EVENT_DATATIME] == 0) + return; + + lv_obj_t *root = lv_display_get_screen_active(NULL); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_DATATIME], NULL); +} + static const char *getenv_default(const char *name, const char *dflt) { const char *value = getenv(name); @@ -44,5 +55,6 @@ void init_lvgl_event() { for (int i = 0; i < CP0_C_EVENT_END; i++) lv_c_event[i] = lv_event_register_id(); + lv_timer_create(datetime_timer_cb, 60000, NULL); init_lvgl_event_cpp(); } From ed05c2d7e418a974a920a90be0d06477227e0168 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:04:40 +0800 Subject: [PATCH 43/70] Sync APPLaunch docs with home refactor --- .../00-overview-and-reading-path.md | 2 +- ...ject-layout-and-module-responsibilities.md | 29 ++- .../02-runtime-framework-and-boot-flow.md | 57 +++-- .../03-ui-framework-and-home-carousel.md | 204 +++++++++++------- ...-application-model-and-launch-mechanism.md | 28 ++- .../05-built-in-page-framework.md | 18 +- .../06-resources-and-configuration.md | 4 +- .../07-input-system-and-key-mapping.md | 14 +- .../09-packaging-deployment-and-systemd.md | 16 +- .../10-extension-development-guide.md | 16 +- .../11-debugging-and-troubleshooting.md | 2 +- .../12-common-modification-entry-points.md | 40 ++-- ...05\350\257\273\350\267\257\347\272\277.md" | 2 +- ...41\345\235\227\350\201\214\350\264\243.md" | 29 ++- ...57\345\212\250\346\265\201\347\250\213.md" | 57 +++-- ...26\351\241\265\350\275\256\346\222\255.md" | 204 +++++++++++------- ...57\345\212\250\346\234\272\345\210\266.md" | 28 ++- ...65\351\235\242\346\241\206\346\236\266.md" | 18 +- ...15\347\275\256\347\263\273\347\273\237.md" | 4 +- ...11\351\224\256\346\230\240\345\260\204.md" | 14 +- ...203\250\347\275\262\344\270\216systemd.md" | 16 +- ...00\345\217\221\346\214\207\345\215\227.md" | 16 +- ...05\351\232\234\346\216\222\346\237\245.md" | 2 +- ...45\345\217\243\351\200\237\346\237\245.md" | 40 ++-- projects/APPLaunch/main/ui/UILaunchPage.cpp | 111 +++++----- projects/APPLaunch/main/ui/UILaunchPage.h | 5 + 26 files changed, 529 insertions(+), 447 deletions(-) diff --git a/docs/launcher-project-guide/00-overview-and-reading-path.md b/docs/launcher-project-guide/00-overview-and-reading-path.md index 6f69ac0d..3cf3c668 100644 --- a/docs/launcher-project-guide/00-overview-and-reading-path.md +++ b/docs/launcher-project-guide/00-overview-and-reading-path.md @@ -70,7 +70,7 @@ If you only want to complete a specific task: | `projects/APPLaunch/main/src/main.cpp` | APPLaunch entry point and LVGL main loop | | `projects/APPLaunch/main/ui/Launch.cpp` | Application list, launch logic, status bar refresh | | `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home UI, carousel, home key handling | -| `projects/APPLaunch/main/ui/components/page_app` | Built-in page implementations | +| `projects/APPLaunch/main/ui/page_app` | Built-in page implementations | | `projects/APPLaunch/APPLaunch` | Resource tree packaged into the runtime environment | | `ext_components/cp0_lvgl` | Platform adaptation layer that wraps file, process, input, and system interfaces | | `scripts/debian_packager.py` | Debian package build script | diff --git a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md index a8b90996..1a0e191f 100644 --- a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md +++ b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md @@ -162,22 +162,17 @@ projects/APPLaunch/main/ ```text main/ui/ -├── ui.c -├── ui.cpp -├── ui.h -├── ui_obj.h -├── Launch.cpp -├── Launch.h -├── UILaunchPage.cpp -├── UILaunchPage.h -├── ui_loading.cpp -├── ui_loading.h -├── ui_global_hint.cpp -├── ui_global_hint.h -├── zero_lvgl_os.cpp -├── zero_lvgl_os.h +├── ui.cpp / ui.h +├── Launch.cpp / Launch.h +├── UILaunchPage.cpp / UILaunchPage.h +├── ui_app_page.hpp +├── page_app.h +├── generate_page_app_includes.py +├── ui_loading.* +├── ui_global_hint.* +├── zero_lvgl_os.* ├── Animation/ -└── components/ +└── page_app/ ``` | File/Directory | Role | @@ -194,7 +189,7 @@ main/ui/ ### 2.5 `components/page_app/` Built-In Page Directory ```text -main/ui/components/page_app/ +main/ui/page_app/ ├── ui_app_camera.hpp ├── ui_app_compass.hpp ├── ui_app_console.hpp @@ -244,7 +239,7 @@ LaunchImpl APPLaunch currently has several clear code style characteristics: - Mixed C and C++: LVGL-generated/compatibility code is often C, while most business pages are C++. -- C/C++ bridge functions are exposed through `extern "C"`, such as `cpp_app_launch()`. +- LVGL callbacks remain C-style static functions, but page dispatch uses `lv_event_get_user_data()` to recover the owning C++ page instance. - Page classes usually construct LVGL objects directly without using an additional UI framework. - Hardware capabilities are preferably accessed through the unified interfaces wrapped by `cp0_lvgl`. - Resource access should preferably use `cp0_file_path()` to avoid path differences between the device and SDL environments. diff --git a/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md b/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md index 3e9ef5d9..6a946d99 100644 --- a/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md +++ b/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md @@ -199,39 +199,33 @@ void zero_lvgl_os::create_launcher_home() ### 6.1 Home Screen Creation -`UILaunchPage::create_screen()` creates the screen only once: +`UILaunchPage` inherits `home_base`, so the root screen, top status bar, content container, and input group are prepared by the shared page framework. `UILaunchPage::create_screen()` only fills the home content container and runs once: ```cpp void UILaunchPage::create_screen() { - if (ui_Screen1) + if (carousel_elements[kCardCenter]) return; - ui_Screen1 = lv_obj_create(NULL); - lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); - - create_top(ui_Screen1); - create_app_container(ui_Screen1); + create_app_container(content_container()); } ``` -It creates two areas: - -- `create_top()`: top-left logo plus WiFi, time, and battery status bar. -- `create_app_container()`: home carousel container, 5 cards, 5 titles, 5 page dots, and left/right arrows. +It creates the home carousel area: 5 cards, 5 titles, 5 page dots, and left/right arrows. The top-left logo, WiFi indicator, time label, and battery bar are created by `home_base::creat_Top_UI()`. ### 6.2 Input Group Binding -The home input group is created in `UILaunchPage::init_input_group()`: +The home input group comes from `AppPageRoot::input_group()`. `UILaunchPage::init_input_group()` stores it in the compatibility bridge and binds the active keyboard input device: ```cpp -home_input_group = lv_group_create(); -lv_group_add_obj(home_input_group, ui_Screen1); -lv_indev_set_group(indev, home_input_group); +void UILaunchPage::init_input_group() +{ + ::home_input_group = input_group(); + bind_home_input_group(); +} ``` -This allows keyboard events to be delivered to `ui_Screen1`, where `main_key_switch()` handles left/right switching and Enter launch. +This allows keyboard events to be delivered to `screen()`, where the LVGL callback `on_home_key()` dispatches to `handle_home_key()` for left/right switching and Enter launch. ### 6.3 Startup GIF and Home Display @@ -243,31 +237,30 @@ Check cp0_file_path_c("logo_output.gif") -> file does not exist: UILaunchPage::load_home_screen() ``` -`start_startup_gif()` creates an independent GIF screen: +`start_startup_gif()` creates an independent GIF screen and binds the callback with `this`: ```cpp -startup_gif = lv_gif_create(NULL); -lv_gif_set_src(startup_gif, gif_path); -lv_obj_center(startup_gif); -lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); -lv_disp_load_scr(startup_gif); +startup_gif_ = lv_gif_create(NULL); +lv_gif_set_src(startup_gif_, startup_gif_path_.data()); +lv_obj_center(startup_gif_); +lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); +lv_disp_load_scr(startup_gif_); ``` -When GIF playback finishes, it receives `LV_EVENT_READY`. The callback `ui_event_logo_over()` pauses the GIF and loads the home screen: +When GIF playback finishes, it receives `LV_EVENT_READY`. `on_startup_gif_event()` returns to the owning `UILaunchPage` instance and `handle_startup_gif_event()` pauses the GIF and loads the home screen once: ```cpp -if (event_code == LV_EVENT_READY && !done) { - if (startup_gif) lv_gif_pause(startup_gif); - UILaunchPage::load_home_screen(); +if (event_code == LV_EVENT_READY && !startup_gif_done_) { + startup_gif_done_ = true; + if (startup_gif_) lv_gif_pause(startup_gif_); + load_home_screen(); } ``` Responsibilities of `load_home_screen()`: ```cpp -ui____initial_actions0 = lv_obj_create(NULL); -lv_disp_load_scr(ui_Screen1); -UILaunchPage::bind_home_input_group(); +show_home_screen(); cp0_signal_audio_api_play_asset("startup.mp3"); ``` @@ -291,8 +284,8 @@ main() -> create_launcher_home() -> register LV_EVENT_GET_COMP_CHILD -> launch_page_->create_screen() - -> create_top() - -> create_app_container() + -> home_base::creat_Top_UI() + -> create_app_container(content_container()) -> launch_->bind_ui() -> new LaunchImpl -> Register fixed/dynamic applications and write them into home slots diff --git a/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md b/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md index 80d2ca8b..8796c38b 100644 --- a/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md +++ b/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md @@ -4,23 +4,25 @@ This chapter explains how the APPLaunch home UI is organized, how data flows thr ## 1. UI Framework Overview -APPLaunch does not use a traditional desktop framework. Instead, it builds the UI directly from an LVGL object tree: +APPLaunch does not use a traditional desktop framework. Instead, it builds the home page from the shared LVGL page base plus a carousel content area: ```text -ui_Screen1 -├── Top status bar create_top() -│ ├── ZERO / logo -│ ├── WiFi signal bars -│ ├── Time panel -│ └── Battery panel -└── ui_APP_Container create_app_container() +UILaunchPage : home_base +├── home_base/AppPageRoot root screen +│ ├── home_base::creat_Top_UI() +│ │ ├── ZERO / logo +│ │ ├── WiFi signal bars +│ │ ├── Time panel +│ │ └── Battery panel +│ └── content_container() +└── Home carousel inside content_container() ├── 5 carousel card panels ├── 5 title labels ├── Left/right arrow buttons └── 5 page dots ``` -The global entry points for home objects come from declarations in `ui_obj.h`, such as `ui_Screen1`, `ui_APP_Container`, `ui_timeLabel`, and `ui_Bar1`. Actual creation and styling are concentrated in `UILaunchPage.cpp`. +Home uses the common `home_base` / `AppPageRoot` page framework for the root screen, status bar, and input group. `UILaunchPage.cpp` fills the inherited content container with the carousel and wires the LVGL callbacks. ## 2. Key Source Paths @@ -31,7 +33,6 @@ The global entry points for home objects come from declarations in `ui_obj.h`, s | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | Carousel left/right switch animation | | `projects/APPLaunch/main/ui/Launch.cpp` | Fills new card content after switching, launches the current application, and refreshes the status bar | | `projects/APPLaunch/main/ui/ui.h` | Home layout constants such as `LABEL_Y_CENTER` and `BORDER_COLOR_CENTER` | -| `projects/APPLaunch/main/ui/ui_obj.h` | Global LVGL object declarations | ## 3. Responsibilities of `UILaunchPage` @@ -41,11 +42,15 @@ The global entry points for home objects come from declarations in `ui_obj.h`, s class UILaunchPage : public home_base { public: - static void load_home_screen(); - static void start_startup_gif(); - static void create_screen(); + explicit UILaunchPage(std::shared_ptr launch); + ~UILaunchPage(); + + void show_home_screen(); + void load_home_screen(); + void start_startup_gif(); + void create_screen(); + void init_input_group(); - static void init_input_group(); static void bind_home_input_group(); static lv_group_t *home_input_group(); static lv_obj_t *panel(size_t slot); @@ -55,29 +60,59 @@ public: void update_right_slot(lv_obj_t *panel, lv_obj_t *label); void launch_selected_app(); - static std::array carousel_elements; +private: + enum class PendingSwitch { None, Left, Right }; + + void switch_left(); + void switch_right(); + void finish_switch_animation(); + void run_pending_switch(); + void handle_home_key(lv_event_t *event); + void handle_startup_gif_event(lv_event_t *event); + + static void on_left_arrow_clicked(lv_event_t *event); + static void on_right_arrow_clicked(lv_event_t *event); + static void on_app_clicked(lv_event_t *event); + static void on_home_key(lv_event_t *event); + static void on_startup_gif_event(lv_event_t *event); + + bool is_animating_ = false; + PendingSwitch pending_switch_ = PendingSwitch::None; + int switch_current_pos_ = kPageDot2; }; ``` It has two categories of responsibilities: -- Static responsibilities: create the screen, maintain the home input group, and provide `panel()` / `label()` accessors. -- Instance responsibilities: hold the `Launch` pointer and forward carousel updates and application launch operations to `LaunchImpl`. +- Static compatibility responsibilities: keep the shared `carousel_elements` array, maintain the home input group bridge, and provide `panel()` / `label()` accessors used by `Launch.cpp`. +- Instance responsibilities: hold the `Launch` pointer, own per-page UI state, handle LVGL events, and forward carousel updates / app launches to `LaunchImpl`. -The current code stores the active home page instance in `active_launch_page` so static event callbacks can call it: +LVGL still requires C-style static callbacks, but the current code no longer relies on global state for normal event dispatch. Each callback receives the owning page instance through LVGL user data: ```cpp -namespace { -UILaunchPage *active_launch_page = nullptr; +static UILaunchPage *page_from_event(lv_event_t *event) +{ + return event ? static_cast(lv_event_get_user_data(event)) : nullptr; } -UILaunchPage::UILaunchPage(std::shared_ptr launch) - : home_base(), launch_(std::move(launch)) +void UILaunchPage::on_left_arrow_clicked(lv_event_t *event) { - active_launch_page = this; + if (UILaunchPage *self = page_from_event(event)) + self->switch_right(); } ``` +Callbacks are registered with `this`: + +```cpp +lv_obj_add_event_cb(left_arrow_button_, on_left_arrow_clicked, LV_EVENT_CLICKED, this); +lv_obj_add_event_cb(right_arrow_button_, on_right_arrow_clicked, LV_EVENT_CLICKED, this); +lv_obj_add_event_cb(screen(), on_home_key, (lv_event_code_t)LV_EVENT_KEYBOARD, this); +lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); +``` + +`active_launch_page` is kept only as a compatibility bridge for static external accessors such as `UILaunchPage::panel()`, `UILaunchPage::label()`, and `UILaunchPage::home_input_group()`. + ## 4. Carousel Element Array All core objects of the home carousel are stored in a fixed array: @@ -172,20 +207,21 @@ The hidden far-side slots are animation buffers: before switching, the card that ## 6. Home Creation Flow -`create_screen()` creates the root screen: +`home_base` constructs the root screen, top status bar, and content container. `UILaunchPage::create_screen()` only fills the home content area, and it avoids rebuilding the carousel if it already exists: ```cpp -ui_Screen1 = lv_obj_create(NULL); -lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); -lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); +void UILaunchPage::create_screen() +{ + if (carousel_elements[kCardCenter]) + return; -create_top(ui_Screen1); -create_app_container(ui_Screen1); + create_app_container(content_container()); +} ``` ### 6.1 Top Status Bar -`create_top()` contains: +The top status bar comes from `home_base::creat_Top_UI()` and contains: - The top-left `ZERO` text or `launcher_brand_logo.png`. - `ui_wifiPanel` and `ui_wifiBar1..4`, hidden by default and shown by signal strength during status refresh. @@ -208,14 +244,16 @@ status_timer = lv_timer_create(home_status_timer_cb, 5000, this); ### 6.2 Carousel Container -`create_app_container()` creates `ui_APP_Container`: +`create_app_container()` uses the inherited `content_container()` as the carousel container: ```cpp -ui_APP_Container = lv_obj_create(parent); -lv_obj_remove_style_all(ui_APP_Container); -lv_obj_set_width(ui_APP_Container, 320); -lv_obj_set_height(ui_APP_Container, 150); -lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); +lv_obj_t *app_container = parent; +if (!app_container) + return; + +lv_obj_set_size(app_container, 320, 150); +lv_obj_clear_flag(app_container, + (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); ``` It then creates, in order: @@ -233,24 +271,26 @@ Carousel switching is split into two parts: UI animation and application data up ### 7.1 Switching Right with `switch_right()` -`switch_right()` means the cards move right as a group, and the current selection becomes the previous application in the list: +`UILaunchPage::switch_right()` means the cards move right as a group, and the current selection becomes the previous application in the list: ```cpp -void switch_right(lv_event_t *e) +void UILaunchPage::switch_right() { - if (is_animating) { - pending_switch = &switch_right; + if (is_animating_) { + pending_switch_ = PendingSwitch::Right; return; } - is_animating = true; + is_animating_ = true; lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_right(carousel_elements.data(), snap_all_panels); + launcher_home_animation::animate_right( + carousel_elements.data(), + [this]() { finish_switch_animation(); }); snap_panel_to_slot(carousel_elements[4], 0); snap_label_to_slot(carousel_elements[9], 5); - active_launch_page->update_right_slot(carousel_elements[4], carousel_elements[9]); + update_right_slot(carousel_elements[4], carousel_elements[9]); rotate_carousel_right(0, 4); rotate_carousel_right(5, 9); } @@ -258,33 +298,35 @@ void switch_right(lv_event_t *e) Key steps: -1. If an animation is already running, store this request in `pending_switch` and execute it after the current animation finishes. +1. If an animation is already running, store `PendingSwitch::Right`; only the latest pending direction is kept. 2. Show the far-left hidden card as the side entering the viewport during the animation. -3. Call `launcher_home_animation::animate_right()` to start the animation. +3. Call `launcher_home_animation::animate_right()` and pass a lambda that captures `this`. 4. Pre-snap the far-right object to the far-left slot and fill it with the new application content that will enter. 5. Rotate `carousel_elements[0..4]` and `[5..9]` so the array order matches the new visual order. 6. Update page dot highlighting. ### 7.2 Switching Left with `switch_left()` -`switch_left()` means the cards move left as a group, and the current selection becomes the next application in the list: +`UILaunchPage::switch_left()` means the cards move left as a group, and the current selection becomes the next application in the list: ```cpp -void switch_left(lv_event_t *e) +void UILaunchPage::switch_left() { - if (is_animating) { - pending_switch = &switch_left; + if (is_animating_) { + pending_switch_ = PendingSwitch::Left; return; } - is_animating = true; + is_animating_ = true; lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_left(carousel_elements.data(), snap_all_panels); + launcher_home_animation::animate_left( + carousel_elements.data(), + [this]() { finish_switch_animation(); }); snap_panel_to_slot(carousel_elements[0], 4); snap_label_to_slot(carousel_elements[5], 9); - active_launch_page->update_left_slot(carousel_elements[0], carousel_elements[5]); + update_left_slot(carousel_elements[0], carousel_elements[5]); rotate_carousel_left(0, 4); rotate_carousel_left(5, 9); } @@ -294,10 +336,10 @@ It is symmetric with `switch_right()`: the far-right side enters the viewport, w ## 8. Snapping Back After Animation -The animation completion callback is `snap_all_panels()`: +The animation completion path is `UILaunchPage::finish_switch_animation()`: ```cpp -static void snap_all_panels() +void UILaunchPage::finish_switch_animation() { for (int i = 0; i < 5; i++) snap_panel_to_slot(carousel_elements[i], i); @@ -305,20 +347,30 @@ static void snap_all_panels() for (int i = 5; i < 10; i++) snap_label_to_slot(carousel_elements[i], i); - is_animating = false; + is_animating_ = false; + run_pending_switch(); +} +``` - if (pending_switch) { - switch_cb_t cb = pending_switch; - pending_switch = NULL; - cb(NULL); - } +`run_pending_switch()` consumes the enum state and invokes the corresponding instance method: + +```cpp +void UILaunchPage::run_pending_switch() +{ + PendingSwitch pending = pending_switch_; + pending_switch_ = PendingSwitch::None; + + if (pending == PendingSwitch::Left) + switch_left(); + else if (pending == PendingSwitch::Right) + switch_right(); } ``` It solves two problems: - Animation interpolation may introduce small errors, so objects are force-snapped to the standard slots after the animation ends. -- If the user repeatedly presses direction keys during the animation, only one pending switch is kept and executed after the animation completes. +- If the user repeatedly presses direction keys during the animation, only one pending switch enum is kept and executed after the animation completes. ## 9. How Application Data Is Written into the Carousel @@ -371,14 +423,14 @@ Press LEFT: ## 10. Input Events and Sound Effects -The home keyboard event is bound at the end of `create_app_container()`: +The home keyboard event is bound at the end of `create_app_container()` through the LVGL callback bridge: ```cpp -lv_obj_add_event_cb(ui_Screen1, main_key_switch, - (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); +lv_obj_add_event_cb(screen(), on_home_key, + (lv_event_code_t)LV_EVENT_KEYBOARD, this); ``` -`main_key_switch()` logic: +`on_home_key()` calls `handle_home_key()` on the owning `UILaunchPage` instance. Its logic is: ```text Press LEFT/Z @@ -391,7 +443,7 @@ Press RIGHT/C Release ENTER -> audio_play_enter() - -> app_launch() + -> launch_selected_app() Release F12 -> Toggle green test background lvping_lock @@ -422,10 +474,11 @@ cp0_signal_audio_api_play_asset("startup.mp3"); ## 11. Home Sequence Text ```text -UILaunchPage::create_screen() - -> create_top() +UILaunchPage constructed as home_base + -> home_base::creat_Top_UI() -> Create logo / WiFi / time / battery objects - -> create_app_container() +UILaunchPage::create_screen() + -> create_app_container(content_container()) -> Create page dots -> Create labels -> Create cards @@ -433,7 +486,7 @@ UILaunchPage::create_screen() -> Bind click and keyboard callbacks User presses RIGHT - -> main_key_switch() + -> on_home_key() -> handle_home_key() -> audio_play_switch() -> switch_left() -> is_animating=true @@ -441,15 +494,14 @@ User presses RIGHT -> update_left_slot() -> rotate cards / labels -> Update page dot - -> snap_all_panels() + -> finish_switch_animation() -> Snap objects to standard slots -> is_animating=false - -> If pending_switch exists, continue executing it + -> If pending_switch_ exists, continue executing it User presses ENTER - -> main_key_switch() + -> on_home_key() -> handle_home_key() -> audio_play_enter() - -> app_launch() -> UILaunchPage::launch_selected_app() -> Launch::launch_app() ``` @@ -458,7 +510,7 @@ User presses ENTER - `carousel_elements` stores LVGL object pointers; carousel switching rotates the pointer array instead of destroying and recreating objects. - The names `switch_left()` / `switch_right()` describe animation direction and are not necessarily identical to user key direction. Currently, `KEY_LEFT` calls `switch_right()`, and `KEY_RIGHT` calls `switch_left()`. -- During animation, only one `pending_switch` is recorded, so rapid repeated key presses do not create an unbounded queue. -- Home card click events are all bound to `app_launch()`, but normal interaction mainly uses center selection + Enter launch. If mouse/touch interaction is enabled, confirm whether clicking a non-center card matches expectations. +- During animation, only one `pending_switch_` enum value is recorded, so rapid repeated key presses do not create an unbounded queue. +- Home card click events are bound to `on_app_clicked()`, which bridges to `launch_selected_app()`, but normal interaction mainly uses center selection + Enter launch. If mouse/touch interaction is enabled, confirm whether clicking a non-center card matches expectations. - Status bar objects are created by `UILaunchPage`, but the refresh timer is created during `LaunchImpl` construction. If the home screen is created without executing `Launch::bind_ui()`, the application list and status bar refresh will not start. - When adding or adjusting carousel slots, update `CAROUSEL_SLOTS`, the initial positions in `create_app_container()`, and the slot definitions in the animation file together to avoid jumps after animation completion. diff --git a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md index f5ed9fa5..0b50f10d 100644 --- a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md +++ b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md @@ -1,6 +1,6 @@ # 04 - Application Model and Launch Mechanism -This chapter explains how APPLaunch unifies built-in pages, terminal commands, and external standalone programs into one application list, and how an application is launched after the user presses Enter. Key references are `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/Launch.h`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/components/page_app/*`. +This chapter explains how APPLaunch unifies built-in pages, terminal commands, and external standalone programs into one application list, and how an application is launched after the user presses Enter. Key references are `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/Launch.h`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/page_app/*`. ## 1. Application Model Overview @@ -33,8 +33,8 @@ Home center card | `projects/APPLaunch/main/ui/Launch.h` | Public facade for `Launch`, hiding `LaunchImpl` | | `projects/APPLaunch/main/ui/Launch.cpp` | `app`, `LaunchImpl`, application list, launch logic, `.desktop` scanning | | `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Forwards Enter / click events to `Launch::launch_app()` | -| `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | Terminal page `UIConsolePage` | -| `projects/APPLaunch/main/ui/components/page_app/*.hpp` | Built-in pages such as settings, music, file, camera, and LoRa | +| `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | Terminal page `UIConsolePage` | +| `projects/APPLaunch/main/ui/page_app/*.hpp` | Built-in pages such as settings, music, file, camera, and LoRa | | `projects/APPLaunch/APPLaunch/applications/` | Runtime `.desktop` application descriptor directory | | `ext_components/cp0_lvgl` | Lower-level capabilities such as process launch, PTY, directory watching, and path resolution | @@ -269,8 +269,9 @@ void launch_Exec(const std::string &exec, bool keep_root = false) lv_timer_enable(true); if (indev) lv_indev_set_group(indev, UILaunchPage::home_input_group()); - lv_disp_load_scr(ui_Screen1); - ui_loading_hide(); + if (launch_page_) + launch_page_->show_home_screen(); + ui_loading::hide(); lv_obj_invalidate(lv_screen_active()); lv_refr_now(disp); LVGL_RUN_FLAGE = 1; @@ -283,7 +284,7 @@ Key points: - Clears the APPLaunch input group so the home screen does not keep processing keys while the external process is running. - `lv_timer_enable(false)` pauses LVGL timers while the external program takes the foreground. - `cp0_process_exec_blocking()` blocks until the external program exits. -- After the external program exits, it restores the timer, input group, home screen, and `LVGL_RUN_FLAGE`. +- After the external program exits, it restores the timer, calls `launch_page_->show_home_screen()`, and restores `LVGL_RUN_FLAGE`. Sequence text: @@ -299,8 +300,7 @@ Enter external app -> APPLaunch main rendering is paused -> Wait for the external program to exit -> lv_timer_enable(true) - -> Bind home input group - -> lv_disp_load_scr(ui_Screen1) + -> launch_page_->show_home_screen() -> ui_loading_hide() -> lv_refr_now() -> LVGL_RUN_FLAGE=1 @@ -333,8 +333,8 @@ static void lv_go_back_home(void *arg) { auto self = (LaunchImpl *)arg; lv_timer_enable(true); - UILaunchPage::bind_home_input_group(); - lv_disp_load_scr(ui_Screen1); + if (self->launch_page_) + self->launch_page_->show_home_screen(); lv_refr_now(NULL); if (self->app_Page) self->app_Page.reset(); @@ -469,13 +469,11 @@ Fixed applications generally use `img_path("xxx.png")`. The `Icon` field of dyna ```text User releases ENTER - -> LV_EVENT_KEYBOARD is delivered to ui_Screen1 - -> main_key_switch() + -> LV_EVENT_KEYBOARD is delivered to UILaunchPage::screen() + -> UILaunchPage::on_home_key() + -> handle_home_key() -> code == KEY_ENTER and key_state == 0 -> audio_play_enter() - -> app_launch(NULL) - -> app_launch() - -> active_launch_page->launch_selected_app() -> UILaunchPage::launch_selected_app() -> launch_->launch_app() -> Launch::launch_app() diff --git a/docs/launcher-project-guide/05-built-in-page-framework.md b/docs/launcher-project-guide/05-built-in-page-framework.md index c9f11c23..09134115 100644 --- a/docs/launcher-project-guide/05-built-in-page-framework.md +++ b/docs/launcher-project-guide/05-built-in-page-framework.md @@ -1,6 +1,6 @@ # 05 - Built-in Page Framework -This chapter explains the class hierarchy, lifecycle, page list, page registration method, and conventions for adding built-in APPLaunch pages. Key source files are `projects/APPLaunch/main/ui/components/ui_app_page.hpp`, `projects/APPLaunch/main/ui/components/page_app/*.hpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/UILaunchPage.cpp`. +This chapter explains the class hierarchy, lifecycle, page list, page registration method, and conventions for adding built-in APPLaunch pages. Key source files are `projects/APPLaunch/main/ui/ui_app_page.hpp`, `projects/APPLaunch/main/ui/page_app/*.hpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/UILaunchPage.cpp`. ## 1. What a Built-in Page Is @@ -9,7 +9,7 @@ A built-in page is an LVGL page class compiled into the APPLaunch process. It is - A built-in page directly creates an `lv_obj_t *root_screen_` and switches to its own screen through `lv_disp_load_scr(page->screen())`. - The page object is stored in `LaunchImpl::app_Page`, and is released asynchronously by the `navigate_home` callback when exiting. - The page shares the APPLaunch process, LVGL main loop, input thread, resource resolution, and `cp0_lvgl_app.h` system interfaces with the home screen. -- Pages are usually header-only and placed under `projects/APPLaunch/main/ui/components/page_app/`, then aggregated by `components/page_app.h`. +- Pages are usually header-only and placed under `projects/APPLaunch/main/ui/page_app/`, then aggregated by `components/page_app.h`. Simplified relationship: @@ -31,7 +31,7 @@ LaunchImpl::launch_app() ### 2.1 `AppPageRoot` -`AppPageRoot` is the root base class for all built-in pages. It is located in `projects/APPLaunch/main/ui/components/ui_app_page.hpp`. It creates an independent screen and an LVGL input group. +`AppPageRoot` is the root base class for all built-in pages. It is located in `projects/APPLaunch/main/ui/ui_app_page.hpp`. It creates an independent screen and an LVGL input group. ```cpp class AppPageRoot @@ -60,7 +60,7 @@ public: Key points: -- `root_screen_` is the page's own top-level screen, not a child of the home `ui_Screen1`. +- `root_screen_` is the page's own top-level screen, not a child of the home `UILaunchPage::screen()`. - By default, `input_group_` only contains `root_screen_`. When the page is launched, it is bound to the current `lv_indev_t`. - `navigate_home` is injected by `LaunchImpl`; a page calls it to return home after ESC or after finishing a task. - The destructor deletes `root_screen_` and `input_group_`, so LVGL child objects created inside the page are released with the screen. @@ -106,7 +106,7 @@ The common top bar is implemented by `UIAppTopBar` and contains: Key source paths: -- `projects/APPLaunch/main/ui/components/ui_app_page.hpp`: `UIAppTopBar`, `AppTopBarRegion`. +- `projects/APPLaunch/main/ui/ui_app_page.hpp`: `UIAppTopBar`, `AppTopBarRegion`. - `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: declarations for interfaces such as `cp0_wifi_get_status()`, `cp0_time_str()`, and `cp0_battery_read()`. Top-bar resources use `cp0_file_path_c()`: @@ -142,7 +142,7 @@ app::app(std::string name, std::string icon, page_t) In the actual code in `projects/APPLaunch/main/ui/Launch.cpp`, the core flow is: -1. After the user presses ENTER on the home screen, `cpp_app_launch()` is called. +1. After the user releases ENTER on the home screen, `UILaunchPage::handle_home_key()` calls `launch_selected_app()`. 2. `UILaunchPage::launch_selected_app()` forwards to `Launch::launch_app()`. 3. `LaunchImpl::launch_app()` finds the current app and executes that app's `launch` function. 4. The built-in page object is created, the screen is loaded, and the input group is switched. @@ -162,7 +162,7 @@ if (navigate_home) - `lv_timer_enable(true)` to restore LVGL timers. - `UILaunchPage::bind_home_input_group()` to bind the home input group. -- `lv_disp_load_scr(ui_Screen1)` to load the home screen. +- `launch_page_->show_home_screen()` to load the home screen and bind the home input group. - `app_Page.reset()` to release the current page object. Notes: @@ -173,7 +173,7 @@ Notes: ## 5. Current Built-in Page List -Page implementations are concentrated in `projects/APPLaunch/main/ui/components/page_app/`. +Page implementations are concentrated in `projects/APPLaunch/main/ui/page_app/`. | Page class | File | Launcher name | Inheritance | Description | | --- | --- | --- | --- | --- | @@ -319,7 +319,7 @@ The home carousel itself is managed by `UILaunchPage.cpp`: - `carousel_elements` stores 5 cards, 5 titles, and 5 page dots. - When switching left/right, `switch_left()` / `switch_right()` are called. After the animation finishes, the array is rotated and `LaunchImpl` updates the far-side slot content. -- ENTER triggers `app_launch()`, which ultimately calls the current app's `launch()`. +- ENTER triggers `UILaunchPage::launch_selected_app()`, which ultimately calls the current app's `launch()`. Built-in pages do not directly manipulate the home carousel. After returning home, the carousel state is preserved by `LaunchImpl`. diff --git a/docs/launcher-project-guide/06-resources-and-configuration.md b/docs/launcher-project-guide/06-resources-and-configuration.md index ae7a2a38..0477a941 100644 --- a/docs/launcher-project-guide/06-resources-and-configuration.md +++ b/docs/launcher-project-guide/06-resources-and-configuration.md @@ -1,6 +1,6 @@ # 06 - Resources and Configuration System -This chapter explains APPLaunch runtime resource directories, path resolution rules, `.desktop` dynamic application files, configuration APIs, settings-page configuration keys, and resource usage notes. Key source files are `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`. +This chapter explains APPLaunch runtime resource directories, path resolution rules, `.desktop` dynamic application files, configuration APIs, settings-page configuration keys, and resource usage notes. Key source files are `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`. ## 1. Resource System Overview @@ -22,7 +22,7 @@ cp0_lvgl_file.cpp / sdl_lvgl_file.cpp +-- Special paths such as keyboard_device / keyboard_map / lock_file ``` -Common wrapper functions for pages are located in `projects/APPLaunch/main/ui/components/ui_app_page.hpp`: +Common wrapper functions for pages are located in `projects/APPLaunch/main/ui/ui_app_page.hpp`: ```cpp static inline std::string img_path(const char *name) diff --git a/docs/launcher-project-guide/07-input-system-and-key-mapping.md b/docs/launcher-project-guide/07-input-system-and-key-mapping.md index 2fcae328..70114213 100644 --- a/docs/launcher-project-guide/07-input-system-and-key-mapping.md +++ b/docs/launcher-project-guide/07-input-system-and-key-mapping.md @@ -1,6 +1,6 @@ # 07 - Input System and Key Mapping -This chapter explains APPLaunch's keyboard input thread, the `key_item` event structure, LVGL event dispatch, key mappings on the home screen and built-in pages, terminal input escaping, and debugging notes. The key source files are `projects/APPLaunch/main/include/keyboard_input.h`, `projects/APPLaunch/main/ui/ui.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/components/page_app/*.hpp`. +This chapter explains APPLaunch's keyboard input thread, the `key_item` event structure, LVGL event dispatch, key mappings on the home screen and built-in pages, terminal input escaping, and debugging notes. The key source files are `projects/APPLaunch/main/include/keyboard_input.h`, `projects/APPLaunch/main/ui/ui.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/page_app/*.hpp`. ## 1. Input System Overview @@ -230,7 +230,7 @@ If a page handles `LV_EVENT_KEYBOARD` directly, it usually uses the raw `KEY_*` ## 6. Home Screen Key Mapping -Home screen key handling is in `projects/APPLaunch/main/ui/UILaunchPage.cpp::main_key_switch()`. +Home screen key handling is in `UILaunchPage::handle_home_key()`; the LVGL C callback entry is `UILaunchPage::on_home_key()` in `projects/APPLaunch/main/ui/UILaunchPage.cpp`. First, the commonly used `F/X/Z/C` keys on CardputerZero are mapped to arrow keys: @@ -257,7 +257,7 @@ Home screen behavior: | `KEY_F12` | released | Toggle the green full-screen debug overlay and set `lvping_lock` | | `KEY_UP` / `KEY_DOWN` or `F` / `X` | pressed/repeat | No action is currently defined on the home screen | -Note: `main_key_switch()` handles left/right keys on press, so a long press may generate repeat events and switch continuously. ENTER launches on release to avoid repeated launches while the key is held down. +Note: `handle_home_key()` handles left/right keys on press, so a long press may generate repeat events and switch continuously. ENTER launches on release to avoid repeated launches while the key is held down. The log tag still contains `main_key_switch` for compatibility with older debugging output. ## 7. Built-In Page Key Mapping Overview @@ -346,8 +346,7 @@ lv_timer_enable(false); int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); lv_timer_enable(true); -lv_indev_set_group(indev, UILaunchPage::home_input_group()); -lv_disp_load_scr(ui_Screen1); +launch_page_->show_home_screen(); LVGL_RUN_FLAGE = 1; ``` @@ -377,10 +376,11 @@ lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); Returning to the home screen: ```cpp -UILaunchPage::bind_home_input_group(); -lv_disp_load_scr(ui_Screen1); +launch_page_->show_home_screen(); ``` +`show_home_screen()` loads the home screen and calls `UILaunchPage::bind_home_input_group()`. + If the screen has switched but the group still points to the old page, the following can occur: - The visible page does not respond to keys. diff --git a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md index 49207a55..dac9bd4c 100644 --- a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md +++ b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md @@ -233,17 +233,17 @@ The same clean commands accept `--project`, `--project-dir`, `--app-name`, and ` ### 6.1 Main Program Lookup -The script looks for the main program under `src_folder`, which defaults to `../dist`. +The script looks for the main program under `src_folder`, which defaults to `dist` relative to `projects/APPLaunch` for the default APPLaunch target. Lookup order: -1. `../dist/M5CardputerZero-APPLaunch` -2. `../dist/bin/M5CardputerZero-APPLaunch` +1. `projects/APPLaunch/dist/M5CardputerZero-APPLaunch` +2. `projects/APPLaunch/dist/bin/M5CardputerZero-APPLaunch` If neither exists, it raises: ```text -FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist +PackError: binary M5CardputerZero-APPLaunch not found in /dist or /dist/bin ``` ### 6.2 Additional Apps and Backends @@ -251,9 +251,9 @@ FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist The script attempts to include these optional files: ```text -../dist/bin/M5CardputerZero-AppStore -../dist/bin/appstore.py -../dist/bin/M5CardputerZero-Calculator +projects/APPLaunch/dist/bin/M5CardputerZero-AppStore +projects/APPLaunch/dist/bin/appstore.py +projects/APPLaunch/dist/bin/M5CardputerZero-Calculator ``` If present, they are copied to: @@ -281,7 +281,7 @@ usr/share/APPLaunch If the source resource tree does not exist, it tries: ```text -../dist/APPLaunch +projects/APPLaunch/dist/APPLaunch ``` This means packaging usually does not rely only on `dist/APPLaunch`; it also copies the `APPLaunch/` resource tree from the project source directory. diff --git a/docs/launcher-project-guide/10-extension-development-guide.md b/docs/launcher-project-guide/10-extension-development-guide.md index 74308988..aba09ebb 100644 --- a/docs/launcher-project-guide/10-extension-development-guide.md +++ b/docs/launcher-project-guide/10-extension-development-guide.md @@ -7,8 +7,8 @@ This chapter explains how to extend APPLaunch, focusing on four common change ty | Entry point | Purpose | | --- | --- | | `projects/APPLaunch/main/ui/Launch.cpp` | Fixed app list, dynamic `.desktop` scanning, launching built-in pages or external processes | -| `projects/APPLaunch/main/ui/components/page_app/` | Built-in page implementation directory; pages are usually header-only `.hpp` files | -| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | Shared page capabilities such as `AppPage`, top bar, `img_path()`, and `audio_path()` | +| `projects/APPLaunch/main/ui/page_app/` | Built-in page implementation directory; pages are usually header-only `.hpp` files | +| `projects/APPLaunch/main/ui/ui_app_page.hpp` | Shared page capabilities such as `AppPage`, top bar, `img_path()`, and `audio_path()` | | `projects/APPLaunch/main/ui/components/generate_page_app_includes.py` | Automatically generates `page_app.h` before build and includes every `page_app/*.hpp` file | | `projects/APPLaunch/APPLaunch/` | Runtime asset tree; after packaging it maps to `/usr/share/APPLaunch/` on the device | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | Device-side `cp0_file_path()` path rules | @@ -26,7 +26,7 @@ Built-in pages are suitable for features that run in the same process as Launche ### 2.1 Create the Page File -Create a new `.hpp` under `projects/APPLaunch/main/ui/components/page_app/`. The recommended naming style is `ui_app_xxx.hpp`. The page class should inherit from `AppPage`; set the title, create the UI, and bind key events in the constructor. +Create a new `.hpp` under `projects/APPLaunch/main/ui/page_app/`. The recommended naming style is `ui_app_xxx.hpp`. The page class should inherit from `AppPage`; set the title, create the UI, and bind key events in the constructor. Minimal skeleton: @@ -80,7 +80,7 @@ private: Notes: - The page must inherit from `AppPage` so it can reuse mechanisms such as `screen()`, `input_group()`, and `navigate_home`. -- Prefer calling `navigate_home()` to return to the home page. Do not call `lv_disp_load_scr(ui_Screen1)` directly, or `LaunchImpl` will not be able to release the current page object correctly. +- Prefer calling `navigate_home()` to return to the home page. Do not load the home screen directly, or `LaunchImpl` will not be able to release the current page object correctly. - If the page creates LVGL timers, file descriptors, threads, or peripheral handles, release them in the destructor. - Use 320x170 as the baseline page size. A common layout is a 20 px top bar and a 320x150 body. - Do not hard-code absolute asset paths. Use `img_path("xxx.png")` for images and `audio_path("xxx.wav")` for audio. @@ -93,7 +93,7 @@ Notes: ui/components/generate_page_app_includes.py ``` -The script scans `projects/APPLaunch/main/ui/components/page_app/*.hpp` and generates `projects/APPLaunch/main/ui/components/page_app.h`. In most cases, as long as the file suffix is `.hpp`, it will be included automatically during the build. +The script scans `projects/APPLaunch/main/ui/page_app/*.hpp` and generates `projects/APPLaunch/main/ui/page_app.h`. In most cases, as long as the file suffix is `.hpp`, it will be included automatically during the build. If you check manually, `page_app.h` should contain: @@ -270,7 +270,7 @@ Type=Application Returning from non-terminal external apps depends on these behaviors: -- If the child process exits normally, APPLaunch restores `ui_Screen1`. +- If the child process exits normally, APPLaunch calls `launch_page_->show_home_screen()` to restore the home screen and input group. - On the device, holding ESC for about 3 seconds sends SIGTERM to the external app process group; if it still has not exited after another 3 seconds, SIGKILL is sent. - `cp0_process_exec_blocking()` pauses the Launcher keyboard thread so the external program can read evdev input directly. @@ -356,7 +356,7 @@ After adding a font, verify that FreeType is enabled in both SDL2 and device bui ## 5. Changing Settings Toggles -The Settings page is centralized in `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`. Current settings include Launcher app visibility toggles, Boot, Screen, WiFi, Speaker, Camera, Info, About, Help, ExtPort, and others. +The Settings page is centralized in `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`. Current settings include Launcher app visibility toggles, Boot, Screen, WiFi, Speaker, Camera, Info, About, Help, ExtPort, and others. ### 5.1 Add a Launcher App Toggle @@ -411,7 +411,7 @@ If you add many configuration items, remember that the current maximum is 32 ent | Check item | Method | | --- | --- | -| Files are placed only in the correct directories | Built-in pages in `main/ui/components/page_app/`, assets in `APPLaunch/share/`, `.desktop` files in `APPLaunch/applications/` | +| Files are placed only in the correct directories | Built-in pages in `main/ui/page_app/`, assets in `APPLaunch/share/`, `.desktop` files in `APPLaunch/applications/` | | SDL2 builds successfully | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | | Device cross build succeeds | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | | Icons display correctly | Check logs for `set panel icon missing/unreadable` | diff --git a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md index 3e343e04..4de058b5 100644 --- a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md +++ b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md @@ -324,7 +324,7 @@ External apps usually fail to return because the child process does not exit, th 3. Unbinds the LVGL input group. 4. Calls `lv_timer_enable(false)` to pause the LVGL timer. 5. Calls `cp0_process_exec_blocking(exec, &LVGL_HOME_KEY_FLAG, keep_root)`. -6. After the child process exits, re-enables the timer, binds the home input group, loads `ui_Screen1`, and hides Loading. +6. After the child process exits, re-enables the timer, binds the home input group, calls `launch_page_->show_home_screen()` to restore the home screen, and hides Loading. ### 6.2 First Confirm Whether the Child Process Is Still Running diff --git a/docs/launcher-project-guide/12-common-modification-entry-points.md b/docs/launcher-project-guide/12-common-modification-entry-points.md index 4178c901..d256327a 100644 --- a/docs/launcher-project-guide/12-common-modification-entry-points.md +++ b/docs/launcher-project-guide/12-common-modification-entry-points.md @@ -11,9 +11,9 @@ git status --short | Task | Main files/directories | Key points | Verification | | --- | --- | --- | --- | -| Add a built-in page | `projects/APPLaunch/main/ui/components/page_app/` | Create `ui_app_xxx.hpp` and inherit from `AppPage` | Build with SDL2 and open the page | +| Add a built-in page | `projects/APPLaunch/main/ui/page_app/` | Create `ui_app_xxx.hpp` and inherit from `AppPage` | Build with SDL2 and open the page | | Register a built-in page on home | `projects/APPLaunch/main/ui/Launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | Icon appears in the home carousel | -| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/Launch.cpp` | Settings page writes `app_Key`, Launcher reads `APP_ENABLED("Key")` | Toggle in Settings, then restart or refresh home | +| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/Launch.cpp` | Settings page writes `app_Key`, Launcher reads `APP_ENABLED("Key")` | Toggle in Settings, then restart or refresh home | | Add external `.desktop` app | `projects/APPLaunch/APPLaunch/applications/` | Filename must end in `.desktop` and include `Name` and `Exec` | No skip logs; app appears on home | | Add icon | `projects/APPLaunch/APPLaunch/share/images/` | Built-in pages use `img_path()`, `.desktop` uses `Icon=share/images/xxx.png` | No `missing/unreadable` logs | | Add sound effect | `projects/APPLaunch/APPLaunch/share/audio/` | Pages use `audio_path()` and `cp0_signal_audio_api()` | Sound plays on device | @@ -21,11 +21,11 @@ git status --short | Change home carousel layout | `projects/APPLaunch/main/ui/UILaunchPage.cpp`, `projects/APPLaunch/main/ui/UILaunchPage.h` | 5 slots, left/right switching, center card | Check animation and input in SDL2 | | Change carousel animation | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | Card movement, scale, opacity, and other animations | Switch left/right repeatedly in SDL2 | | Change home status bar | `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/ui.c` | `update_home_status_bar()` refreshes WiFi/time/battery | Check `[HOME_STATUS]` logs | -| Change Settings menu | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | Add `MenuItem`/`SubItem` in `menu_init()` | Enter the SETTING page and test | +| Change Settings menu | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | Add `MenuItem`/`SubItem` in `menu_init()` | Enter the SETTING page and test | | Change configuration saving logic | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Currently saves to `/var/lib/applaunch/settings`, max 32 entries | Inspect the settings file | | Change asset path rules | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | Consider device and SDL2 consistently | Check assets on both SDL2 and device | | Change external app launch/return | `projects/APPLaunch/main/ui/Launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | External app starts, ESC returns | -| Change terminal apps | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY, command execution, input/output | Verify with a `Terminal=true` app | +| Change terminal apps | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY, command execution, input/output | Verify with a `Terminal=true` app | | Change input mapping | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | Device and SDL2 input differences | `evtest` + SDL2 keyboard | | Change startup flow | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`, `cp0_lvgl_init()`, `ui_init()`, main loop | Check `[BOOT]` logs | | Change build dependencies | `projects/APPLaunch/main/SConstruct` | `SRCS`, `INCLUDE`, `REQUIREMENTS`, `STATIC_FILES` | scons build | @@ -49,28 +49,28 @@ git status --short | `projects/APPLaunch/main/ui/ui_global_hint.cpp` | Global hint overlay | | `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | LVGL OS/thread helpers | | `projects/APPLaunch/main/ui/Animation/` | Home carousel animation implementation | -| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | Built-in page base class, top bar, shared asset path helpers | -| `projects/APPLaunch/main/ui/components/page_app.h` | Auto-generated built-in page include aggregate | -| `projects/APPLaunch/main/ui/components/page_app/` | Built-in page implementation directory | +| `projects/APPLaunch/main/ui/ui_app_page.hpp` | Built-in page base class, top bar, shared asset path helpers | +| `projects/APPLaunch/main/ui/page_app.h` | Auto-generated built-in page include aggregate | +| `projects/APPLaunch/main/ui/page_app/` | Built-in page implementation directory | | `projects/APPLaunch/main/include/` | APPLaunch private headers and compatible input headers | ## 3. Built-in Page Entry Table | Page/feature | File | Registered name or icon | Description | | --- | --- | --- | --- | -| GAME | `projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | -| SETTING | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings page, including app toggles, brightness, volume, WiFi, camera, etc. | -| MUSIC | `projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | Music page | -| Compass | `projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass page | -| IP_PANEL | `projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP information panel, enabled on device | -| FILE | `projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | File page, enabled on device | -| SSH | `projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH page, enabled on device | -| MESH | `projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh page, enabled on device | -| REC | `projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | Recording page, enabled on device | -| CAMERA | `projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | Camera page, enabled on device | -| LORA | `projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa page, enabled on device | -| TANK | `projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game, enabled on device | -| CLI/terminal | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`, used by bash, python, and `Terminal=true` apps | +| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | +| SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings page, including app toggles, brightness, volume, WiFi, camera, etc. | +| MUSIC | `projects/APPLaunch/main/ui/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | Music page | +| Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass page | +| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP information panel, enabled on device | +| FILE | `projects/APPLaunch/main/ui/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | File page, enabled on device | +| SSH | `projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH page, enabled on device | +| MESH | `projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh page, enabled on device | +| REC | `projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | Recording page, enabled on device | +| CAMERA | `projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | Camera page, enabled on device | +| LORA | `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa page, enabled on device | +| TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game, enabled on device | +| CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`, used by bash, python, and `Terminal=true` apps | Fixed registration entry in `LaunchImpl::LaunchImpl()`: diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" index 599b263f..8589a3a7 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" @@ -70,7 +70,7 @@ APPLaunch 首页、状态栏、轮播、应用管理器 | `projects/APPLaunch/main/src/main.cpp` | APPLaunch 入口和 LVGL 主循环 | | `projects/APPLaunch/main/ui/Launch.cpp` | 应用列表、启动逻辑、状态栏刷新 | | `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 UI、轮播、首页按键 | -| `projects/APPLaunch/main/ui/components/page_app` | 内置页面实现 | +| `projects/APPLaunch/main/ui/page_app` | 内置页面实现 | | `projects/APPLaunch/APPLaunch` | 打包进运行环境的资源树 | | `ext_components/cp0_lvgl` | 平台适配层,封装文件、进程、输入、系统接口 | | `scripts/debian_packager.py` | Debian 包打包脚本 | diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" index 545eb8fb..4b251b90 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" @@ -162,22 +162,17 @@ projects/APPLaunch/main/ ```text main/ui/ -├── ui.c -├── ui.cpp -├── ui.h -├── ui_obj.h -├── Launch.cpp -├── Launch.h -├── UILaunchPage.cpp -├── UILaunchPage.h -├── ui_loading.cpp -├── ui_loading.h -├── ui_global_hint.cpp -├── ui_global_hint.h -├── zero_lvgl_os.cpp -├── zero_lvgl_os.h +├── ui.cpp / ui.h +├── Launch.cpp / Launch.h +├── UILaunchPage.cpp / UILaunchPage.h +├── ui_app_page.hpp +├── page_app.h +├── generate_page_app_includes.py +├── ui_loading.* +├── ui_global_hint.* +├── zero_lvgl_os.* ├── Animation/ -└── components/ +└── page_app/ ``` | 文件/目录 | 作用 | @@ -194,7 +189,7 @@ main/ui/ ### 2.5 `components/page_app/` 内置页面目录 ```text -main/ui/components/page_app/ +main/ui/page_app/ ├── ui_app_camera.hpp ├── ui_app_compass.hpp ├── ui_app_console.hpp @@ -244,7 +239,7 @@ LaunchImpl APPLaunch 当前代码有几个明显特征: - C 和 C++ 混合:LVGL 自动生成/兼容代码常是 C,业务页面多为 C++。 -- 通过 `extern "C"` 暴露 C/C++ 桥接函数,例如 `cpp_app_launch()`。 +- LVGL 回调仍是 C 风格静态函数,但页面分发通过 `lv_event_get_user_data()` 取回所属 C++ 页面实例。 - 页面类通常直接构造 LVGL 对象,不使用额外 UI 框架。 - 硬件能力优先通过 `cp0_lvgl` 的统一接口封装。 - 资源访问优先使用 `cp0_file_path()`,避免设备端和 SDL 端路径差异。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" index c7f61275..f773c372 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" @@ -199,39 +199,33 @@ void zero_lvgl_os::create_launcher_home() ### 6.1 首页 screen 创建 -`UILaunchPage::create_screen()` 只创建一次: +`UILaunchPage` 继承 `home_base`,根 screen、顶部状态栏、内容容器和输入 group 都由共享页面框架准备。`UILaunchPage::create_screen()` 只填充首页内容容器,并且只执行一次: ```cpp void UILaunchPage::create_screen() { - if (ui_Screen1) + if (carousel_elements[kCardCenter]) return; - ui_Screen1 = lv_obj_create(NULL); - lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); - - create_top(ui_Screen1); - create_app_container(ui_Screen1); + create_app_container(content_container()); } ``` -它会创建两个区域: - -- `create_top()`:左上角 logo、WiFi、时间、电量状态栏。 -- `create_app_container()`:首页轮播容器、5 个卡片、5 个标题、5 个页点、左右箭头。 +它会创建首页轮播区域:5 个卡片、5 个标题、5 个页点和左右箭头。左上角 logo、WiFi、时间、电量状态栏由 `home_base::creat_Top_UI()` 创建。 ### 6.2 输入 group 绑定 -首页的输入 group 在 `UILaunchPage::init_input_group()` 中创建: +首页输入 group 来自 `AppPageRoot::input_group()`。`UILaunchPage::init_input_group()` 把它保存到兼容桥,并绑定当前键盘输入设备: ```cpp -home_input_group = lv_group_create(); -lv_group_add_obj(home_input_group, ui_Screen1); -lv_indev_set_group(indev, home_input_group); +void UILaunchPage::init_input_group() +{ + ::home_input_group = input_group(); + bind_home_input_group(); +} ``` -这使键盘事件可以投递到 `ui_Screen1`,再由 `main_key_switch()` 处理左右切换和 Enter 启动。 +这使键盘事件可以投递到 `screen()`,再由 LVGL 回调 `on_home_key()` 分发到 `handle_home_key()` 处理左右切换和 Enter 启动。 ### 6.3 启动 GIF 与首页显示 @@ -243,31 +237,30 @@ lv_indev_set_group(indev, home_input_group); -> 文件不存在:UILaunchPage::load_home_screen() ``` -`start_startup_gif()` 创建一个独立的 GIF screen: +`start_startup_gif()` 创建独立 GIF screen,并绑定携带 `this` 的回调: ```cpp -startup_gif = lv_gif_create(NULL); -lv_gif_set_src(startup_gif, gif_path); -lv_obj_center(startup_gif); -lv_obj_add_event_cb(startup_gif, ui_event_logo_over, LV_EVENT_ALL, NULL); -lv_disp_load_scr(startup_gif); +startup_gif_ = lv_gif_create(NULL); +lv_gif_set_src(startup_gif_, startup_gif_path_.data()); +lv_obj_center(startup_gif_); +lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); +lv_disp_load_scr(startup_gif_); ``` -GIF 播放完成时收到 `LV_EVENT_READY`,回调 `ui_event_logo_over()` 会暂停 GIF 并加载首页: +GIF 播放完成时收到 `LV_EVENT_READY`。`on_startup_gif_event()` 会回到所属 `UILaunchPage` 实例,`handle_startup_gif_event()` 暂停 GIF 并确保只加载一次首页: ```cpp -if (event_code == LV_EVENT_READY && !done) { - if (startup_gif) lv_gif_pause(startup_gif); - UILaunchPage::load_home_screen(); +if (event_code == LV_EVENT_READY && !startup_gif_done_) { + startup_gif_done_ = true; + if (startup_gif_) lv_gif_pause(startup_gif_); + load_home_screen(); } ``` `load_home_screen()` 的职责: ```cpp -ui____initial_actions0 = lv_obj_create(NULL); -lv_disp_load_scr(ui_Screen1); -UILaunchPage::bind_home_input_group(); +show_home_screen(); cp0_signal_audio_api_play_asset("startup.mp3"); ``` @@ -291,8 +284,8 @@ main() -> create_launcher_home() -> register LV_EVENT_GET_COMP_CHILD -> launch_page_->create_screen() - -> create_top() - -> create_app_container() + -> home_base::creat_Top_UI() + -> create_app_container(content_container()) -> launch_->bind_ui() -> new LaunchImpl -> 注册固定/动态应用并写入首页槽位 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" index a3518dc5..d6e81ad5 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" @@ -4,23 +4,25 @@ ## 1. UI 框架概览 -APPLaunch 的 UI 不是传统桌面框架,而是直接基于 LVGL 对象树构建: +APPLaunch 的 UI 不是传统桌面框架,而是用共享 LVGL 页面基类加首页轮播内容区构建: ```text -ui_Screen1 -├── 顶部状态栏 create_top() -│ ├── ZERO / logo -│ ├── WiFi 信号条 -│ ├── 时间面板 -│ └── 电量面板 -└── ui_APP_Container create_app_container() +UILaunchPage : home_base +├── home_base/AppPageRoot 根 screen +│ ├── home_base::creat_Top_UI() +│ │ ├── ZERO / logo +│ │ ├── WiFi 信号条 +│ │ ├── 时间面板 +│ │ └── 电量面板 +│ └── content_container() +└── content_container() 内的首页轮播 ├── 5 个轮播卡片 panel ├── 5 个标题 label ├── 左右箭头按钮 └── 5 个页点 dot ``` -首页对象的全局入口来自 `ui_obj.h` 中的 `ui_Screen1`、`ui_APP_Container`、`ui_timeLabel`、`ui_Bar1` 等对象声明;实际创建和样式设置集中在 `UILaunchPage.cpp`。 +首页复用 `home_base` / `AppPageRoot` 页面框架创建根 screen、状态栏和输入 group。`UILaunchPage.cpp` 负责在继承来的内容容器中填充轮播,并绑定 LVGL 回调。 ## 2. 关键源码路径 @@ -31,7 +33,6 @@ ui_Screen1 | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | 轮播左右切换动画 | | `projects/APPLaunch/main/ui/Launch.cpp` | 切换后填充新卡片内容、启动当前应用、刷新状态栏 | | `projects/APPLaunch/main/ui/ui.h` | 首页布局常量,例如 `LABEL_Y_CENTER`、`BORDER_COLOR_CENTER` | -| `projects/APPLaunch/main/ui/ui_obj.h` | 全局 LVGL 对象声明 | ## 3. `UILaunchPage` 的职责 @@ -41,11 +42,15 @@ ui_Screen1 class UILaunchPage : public home_base { public: - static void load_home_screen(); - static void start_startup_gif(); - static void create_screen(); + explicit UILaunchPage(std::shared_ptr launch); + ~UILaunchPage(); + + void show_home_screen(); + void load_home_screen(); + void start_startup_gif(); + void create_screen(); + void init_input_group(); - static void init_input_group(); static void bind_home_input_group(); static lv_group_t *home_input_group(); static lv_obj_t *panel(size_t slot); @@ -55,29 +60,59 @@ public: void update_right_slot(lv_obj_t *panel, lv_obj_t *label); void launch_selected_app(); - static std::array carousel_elements; +private: + enum class PendingSwitch { None, Left, Right }; + + void switch_left(); + void switch_right(); + void finish_switch_animation(); + void run_pending_switch(); + void handle_home_key(lv_event_t *event); + void handle_startup_gif_event(lv_event_t *event); + + static void on_left_arrow_clicked(lv_event_t *event); + static void on_right_arrow_clicked(lv_event_t *event); + static void on_app_clicked(lv_event_t *event); + static void on_home_key(lv_event_t *event); + static void on_startup_gif_event(lv_event_t *event); + + bool is_animating_ = false; + PendingSwitch pending_switch_ = PendingSwitch::None; + int switch_current_pos_ = kPageDot2; }; ``` 它有两类职责: -- 静态职责:创建 screen、维护首页输入 group、提供 `panel()` / `label()` 访问器。 -- 实例职责:持有 `Launch` 指针,把轮播更新和应用启动转发给 `LaunchImpl`。 +- 静态兼容职责:保留共享的 `carousel_elements` 数组、维护首页输入 group 桥接、提供 `Launch.cpp` 使用的 `panel()` / `label()` 访问器。 +- 实例职责:持有 `Launch` 指针,拥有页面级 UI 状态,处理 LVGL 事件,并把轮播更新和应用启动转发给 `LaunchImpl`。 -当前代码通过 `active_launch_page` 保存活动首页实例,供静态事件回调调用: +LVGL 仍然要求 C 风格静态回调,但当前代码不再依赖全局状态做常规事件分发。每个回调都通过 LVGL user data 取回所属页面实例: ```cpp -namespace { -UILaunchPage *active_launch_page = nullptr; +static UILaunchPage *page_from_event(lv_event_t *event) +{ + return event ? static_cast(lv_event_get_user_data(event)) : nullptr; } -UILaunchPage::UILaunchPage(std::shared_ptr launch) - : home_base(), launch_(std::move(launch)) +void UILaunchPage::on_left_arrow_clicked(lv_event_t *event) { - active_launch_page = this; + if (UILaunchPage *self = page_from_event(event)) + self->switch_right(); } ``` +注册回调时传入 `this`: + +```cpp +lv_obj_add_event_cb(left_arrow_button_, on_left_arrow_clicked, LV_EVENT_CLICKED, this); +lv_obj_add_event_cb(right_arrow_button_, on_right_arrow_clicked, LV_EVENT_CLICKED, this); +lv_obj_add_event_cb(screen(), on_home_key, (lv_event_code_t)LV_EVENT_KEYBOARD, this); +lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); +``` + +`active_launch_page` 只作为静态外部访问器的兼容桥保留,例如 `UILaunchPage::panel()`、`UILaunchPage::label()` 和 `UILaunchPage::home_input_group()`。 + ## 4. 轮播元素数组 首页轮播的所有核心对象都存放在一个固定数组里: @@ -172,20 +207,21 @@ static const CarouselSlot CAROUSEL_SLOTS[] = { ## 6. 首页创建流程 -`create_screen()` 创建根 screen: +`home_base` 负责构造根 screen、顶部状态栏和内容容器。`UILaunchPage::create_screen()` 只填充首页内容区,并在轮播已存在时直接返回: ```cpp -ui_Screen1 = lv_obj_create(NULL); -lv_obj_clear_flag(ui_Screen1, LV_OBJ_FLAG_SCROLLABLE); -lv_obj_set_style_bg_color(ui_Screen1, lv_color_hex(0x000000), LV_PART_MAIN); +void UILaunchPage::create_screen() +{ + if (carousel_elements[kCardCenter]) + return; -create_top(ui_Screen1); -create_app_container(ui_Screen1); + create_app_container(content_container()); +} ``` ### 6.1 顶部状态栏 -`create_top()` 包含: +顶部状态栏来自 `home_base::creat_Top_UI()`,包含: - 左上角 `ZERO` 文本或 `launcher_brand_logo.png`。 - `ui_wifiPanel` 与 `ui_wifiBar1..4`,默认隐藏,状态刷新时按信号强度显示。 @@ -208,14 +244,16 @@ status_timer = lv_timer_create(home_status_timer_cb, 5000, this); ### 6.2 轮播容器 -`create_app_container()` 创建 `ui_APP_Container`: +`create_app_container()` 使用继承来的 `content_container()` 作为轮播容器: ```cpp -ui_APP_Container = lv_obj_create(parent); -lv_obj_remove_style_all(ui_APP_Container); -lv_obj_set_width(ui_APP_Container, 320); -lv_obj_set_height(ui_APP_Container, 150); -lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); +lv_obj_t *app_container = parent; +if (!app_container) + return; + +lv_obj_set_size(app_container, 320, 150); +lv_obj_clear_flag(app_container, + (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); ``` 随后依次创建: @@ -233,24 +271,26 @@ lv_obj_set_align(ui_APP_Container, LV_ALIGN_CENTER); ### 7.1 向右切换 `switch_right()` -`switch_right()` 的含义是卡片整体向右移动,当前选择变为列表中的前一个应用: +`UILaunchPage::switch_right()` 的含义是卡片整体向右移动,当前选择变为列表中的前一个应用: ```cpp -void switch_right(lv_event_t *e) +void UILaunchPage::switch_right() { - if (is_animating) { - pending_switch = &switch_right; + if (is_animating_) { + pending_switch_ = PendingSwitch::Right; return; } - is_animating = true; + is_animating_ = true; lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_right(carousel_elements.data(), snap_all_panels); + launcher_home_animation::animate_right( + carousel_elements.data(), + [this]() { finish_switch_animation(); }); snap_panel_to_slot(carousel_elements[4], 0); snap_label_to_slot(carousel_elements[9], 5); - active_launch_page->update_right_slot(carousel_elements[4], carousel_elements[9]); + update_right_slot(carousel_elements[4], carousel_elements[9]); rotate_carousel_right(0, 4); rotate_carousel_right(5, 9); } @@ -258,33 +298,35 @@ void switch_right(lv_event_t *e) 关键步骤: -1. 如果正在动画中,把本次请求放入 `pending_switch`,等待当前动画结束后执行。 +1. 如果正在动画中,记录 `PendingSwitch::Right`;待执行方向只保留最后一次。 2. 显示远左隐藏卡片,作为动画进入画面的一侧。 -3. 调用 `launcher_home_animation::animate_right()` 启动动画。 +3. 调用 `launcher_home_animation::animate_right()`,并传入捕获 `this` 的 lambda。 4. 把远右对象预先吸附到远左槽位,填充即将进入的新应用内容。 5. 旋转 `carousel_elements[0..4]` 和 `[5..9]`,让数组顺序匹配新的视觉顺序。 6. 更新页点高亮。 ### 7.2 向左切换 `switch_left()` -`switch_left()` 的含义是卡片整体向左移动,当前选择变为列表中的后一个应用: +`UILaunchPage::switch_left()` 的含义是卡片整体向左移动,当前选择变为列表中的后一个应用: ```cpp -void switch_left(lv_event_t *e) +void UILaunchPage::switch_left() { - if (is_animating) { - pending_switch = &switch_left; + if (is_animating_) { + pending_switch_ = PendingSwitch::Left; return; } - is_animating = true; + is_animating_ = true; lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_left(carousel_elements.data(), snap_all_panels); + launcher_home_animation::animate_left( + carousel_elements.data(), + [this]() { finish_switch_animation(); }); snap_panel_to_slot(carousel_elements[0], 4); snap_label_to_slot(carousel_elements[5], 9); - active_launch_page->update_left_slot(carousel_elements[0], carousel_elements[5]); + update_left_slot(carousel_elements[0], carousel_elements[5]); rotate_carousel_left(0, 4); rotate_carousel_left(5, 9); } @@ -294,10 +336,10 @@ void switch_left(lv_event_t *e) ## 8. 动画结束后的归位 -动画结束回调是 `snap_all_panels()`: +动画结束路径是 `UILaunchPage::finish_switch_animation()`: ```cpp -static void snap_all_panels() +void UILaunchPage::finish_switch_animation() { for (int i = 0; i < 5; i++) snap_panel_to_slot(carousel_elements[i], i); @@ -305,20 +347,30 @@ static void snap_all_panels() for (int i = 5; i < 10; i++) snap_label_to_slot(carousel_elements[i], i); - is_animating = false; + is_animating_ = false; + run_pending_switch(); +} +``` - if (pending_switch) { - switch_cb_t cb = pending_switch; - pending_switch = NULL; - cb(NULL); - } +`run_pending_switch()` 会消费枚举状态,并调用对应的实例方法: + +```cpp +void UILaunchPage::run_pending_switch() +{ + PendingSwitch pending = pending_switch_; + pending_switch_ = PendingSwitch::None; + + if (pending == PendingSwitch::Left) + switch_left(); + else if (pending == PendingSwitch::Right) + switch_right(); } ``` 它解决两个问题: - 动画可能有插值误差,结束后强制把对象吸附到标准槽位。 -- 如果动画期间用户连续按方向键,只保留一个待执行切换,动画完成后继续执行。 +- 如果动画期间用户连续按方向键,只保留一个待执行枚举方向,动画完成后继续执行。 ## 9. 应用数据如何写入轮播 @@ -371,14 +423,14 @@ void update_right_slot(lv_obj_t *panel, lv_obj_t *label) ## 10. 输入事件与音效 -首页键盘事件绑定在 `create_app_container()` 末尾: +首页键盘事件在 `create_app_container()` 末尾通过 LVGL 回调桥绑定: ```cpp -lv_obj_add_event_cb(ui_Screen1, main_key_switch, - (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); +lv_obj_add_event_cb(screen(), on_home_key, + (lv_event_code_t)LV_EVENT_KEYBOARD, this); ``` -`main_key_switch()` 处理逻辑: +`on_home_key()` 会回到所属 `UILaunchPage` 实例的 `handle_home_key()`,处理逻辑: ```text 按下 LEFT/Z @@ -391,7 +443,7 @@ lv_obj_add_event_cb(ui_Screen1, main_key_switch, 释放 ENTER -> audio_play_enter() - -> app_launch() + -> launch_selected_app() 释放 F12 -> 开关绿色测试背景 lvping_lock @@ -422,10 +474,11 @@ cp0_signal_audio_api_play_asset("startup.mp3"); ## 11. 首页时序文本 ```text -UILaunchPage::create_screen() - -> create_top() +构造 UILaunchPage/home_base + -> home_base::creat_Top_UI() -> 创建 logo / WiFi / 时间 / 电量对象 - -> create_app_container() +UILaunchPage::create_screen() + -> create_app_container(content_container()) -> 创建 page dots -> 创建 labels -> 创建 cards @@ -433,7 +486,7 @@ UILaunchPage::create_screen() -> 绑定 click 和 keyboard callbacks 用户按 RIGHT - -> main_key_switch() + -> on_home_key() -> handle_home_key() -> audio_play_switch() -> switch_left() -> is_animating=true @@ -441,15 +494,14 @@ UILaunchPage::create_screen() -> update_left_slot() -> rotate cards / labels -> 更新 page dot - -> snap_all_panels() + -> finish_switch_animation() -> 吸附对象到标准槽位 -> is_animating=false - -> 如有 pending_switch,继续执行 + -> 如有 pending_switch_,继续执行 用户按 ENTER - -> main_key_switch() + -> on_home_key() -> handle_home_key() -> audio_play_enter() - -> app_launch() -> UILaunchPage::launch_selected_app() -> Launch::launch_app() ``` @@ -458,7 +510,7 @@ UILaunchPage::create_screen() - `carousel_elements` 保存的是 LVGL 对象指针;轮播切换通过旋转指针数组实现,不是销毁重建对象。 - `switch_left()` / `switch_right()` 的命名描述动画方向,不一定等同于用户按键方向;当前 `KEY_LEFT` 调用 `switch_right()`,`KEY_RIGHT` 调用 `switch_left()`。 -- 动画期间只记录一个 `pending_switch`,连续快速按键不会无限排队。 -- 首页卡片点击事件都绑定到 `app_launch()`,但正常交互主要通过中心选择 + Enter 启动;如果开放鼠标/触摸,需要确认点击非中心卡片时是否符合预期。 +- 动画期间只记录一个 `pending_switch_` 枚举值,连续快速按键不会无限排队。 +- 首页卡片点击事件都绑定到 `on_app_clicked()`,再桥接到 `launch_selected_app()`,但正常交互主要通过中心选择 + Enter 启动;如果开放鼠标/触摸,需要确认点击非中心卡片时是否符合预期。 - 状态栏对象由 `UILaunchPage` 创建,但刷新 timer 在 `LaunchImpl` 构造时创建;如果只创建首页但没有执行 `Launch::bind_ui()`,应用列表和状态栏刷新不会启动。 - 新增或调整轮播槽位时,要同步修改 `CAROUSEL_SLOTS`、`create_app_container()` 初始位置、动画文件中的槽位定义,避免动画结束跳变。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" index b9e4bc9a..cfe6f8a2 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" @@ -1,6 +1,6 @@ # 04 - 应用模型与启动机制 -本章说明 APPLaunch 如何把内置页面、终端命令、外部独立程序统一成一个应用列表,以及用户按 Enter 后如何启动应用。重点参考 `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/Launch.h`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/components/page_app/*`。 +本章说明 APPLaunch 如何把内置页面、终端命令、外部独立程序统一成一个应用列表,以及用户按 Enter 后如何启动应用。重点参考 `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/Launch.h`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/page_app/*`。 ## 1. 应用模型概览 @@ -33,8 +33,8 @@ app | `projects/APPLaunch/main/ui/Launch.h` | `Launch` 对外门面,隐藏 `LaunchImpl` | | `projects/APPLaunch/main/ui/Launch.cpp` | `app`、`LaunchImpl`、应用列表、启动逻辑、`.desktop` 扫描 | | `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Enter / click 事件转发到 `Launch::launch_app()` | -| `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | 终端页面 `UIConsolePage` | -| `projects/APPLaunch/main/ui/components/page_app/*.hpp` | 各内置页面,例如设置、音乐、文件、相机、LoRa | +| `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | 终端页面 `UIConsolePage` | +| `projects/APPLaunch/main/ui/page_app/*.hpp` | 各内置页面,例如设置、音乐、文件、相机、LoRa | | `projects/APPLaunch/APPLaunch/applications/` | 运行时 `.desktop` 应用描述目录 | | `ext_components/cp0_lvgl` | 进程启动、PTY、目录监听、路径解析等底层能力 | @@ -269,8 +269,9 @@ void launch_Exec(const std::string &exec, bool keep_root = false) lv_timer_enable(true); if (indev) lv_indev_set_group(indev, UILaunchPage::home_input_group()); - lv_disp_load_scr(ui_Screen1); - ui_loading_hide(); + if (launch_page_) + launch_page_->show_home_screen(); + ui_loading::hide(); lv_obj_invalidate(lv_screen_active()); lv_refr_now(disp); LVGL_RUN_FLAGE = 1; @@ -283,7 +284,7 @@ void launch_Exec(const std::string &exec, bool keep_root = false) - 关闭 APPLaunch 输入 group,避免外部进程运行时首页继续处理按键。 - `lv_timer_enable(false)` 暂停 LVGL timer,外部程序接管前台。 - `cp0_process_exec_blocking()` 阻塞等待外部程序退出。 -- 外部程序退出后恢复 timer、输入 group、首页 screen 和 `LVGL_RUN_FLAGE`。 +- 外部程序退出后恢复 timer,调用 `launch_page_->show_home_screen()` 恢复首页和输入组,并恢复 `LVGL_RUN_FLAGE`。 时序文本: @@ -299,8 +300,7 @@ Enter 外部应用 -> APPLaunch 主渲染暂停 -> 等待外部程序退出 -> lv_timer_enable(true) - -> 绑定首页 input group - -> lv_disp_load_scr(ui_Screen1) + -> launch_page_->show_home_screen() -> ui_loading_hide() -> lv_refr_now() -> LVGL_RUN_FLAGE=1 @@ -333,8 +333,8 @@ static void lv_go_back_home(void *arg) { auto self = (LaunchImpl *)arg; lv_timer_enable(true); - UILaunchPage::bind_home_input_group(); - lv_disp_load_scr(ui_Screen1); + if (self->launch_page_) + self->launch_page_->show_home_screen(); lv_refr_now(NULL); if (self->app_Page) self->app_Page.reset(); @@ -469,13 +469,11 @@ static void panel_set_icon(lv_obj_t *panel, const char *src) ```text 用户释放 ENTER - -> LV_EVENT_KEYBOARD 投递给 ui_Screen1 - -> main_key_switch() + -> LV_EVENT_KEYBOARD 投递给 UILaunchPage::screen() + -> UILaunchPage::on_home_key() + -> handle_home_key() -> code == KEY_ENTER 且 key_state == 0 -> audio_play_enter() - -> app_launch(NULL) - -> app_launch() - -> active_launch_page->launch_selected_app() -> UILaunchPage::launch_selected_app() -> launch_->launch_app() -> Launch::launch_app() diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" index 06d92a90..b3086f5b 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" @@ -1,6 +1,6 @@ # 05 - 内置页面框架 -本章说明 APPLaunch 内置页面的类层次、生命周期、页面列表、页面注册方式以及新增页面时应遵守的约定。重点源码在 `projects/APPLaunch/main/ui/components/ui_app_page.hpp`、`projects/APPLaunch/main/ui/components/page_app/*.hpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`。 +本章说明 APPLaunch 内置页面的类层次、生命周期、页面列表、页面注册方式以及新增页面时应遵守的约定。重点源码在 `projects/APPLaunch/main/ui/ui_app_page.hpp`、`projects/APPLaunch/main/ui/page_app/*.hpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`。 ## 1. 内置页面是什么 @@ -9,7 +9,7 @@ - 内置页面直接创建 `lv_obj_t *root_screen_`,通过 `lv_disp_load_scr(page->screen())` 切换到自己的 screen。 - 页面对象保存在 `LaunchImpl::app_Page`,退出时由 `navigate_home` 回调异步释放。 - 页面与首页共享 APPLaunch 进程、LVGL 主循环、输入线程、资源解析和 `cp0_lvgl_app.h` 系统接口。 -- 页面通常是 header-only,放在 `projects/APPLaunch/main/ui/components/page_app/`,由 `components/page_app.h` 汇总包含。 +- 页面通常是 header-only,放在 `projects/APPLaunch/main/ui/page_app/`,由 `components/page_app.h` 汇总包含。 简化关系: @@ -31,7 +31,7 @@ LaunchImpl::launch_app() ### 2.1 `AppPageRoot` -`AppPageRoot` 是所有内置页面的根基类,位于 `projects/APPLaunch/main/ui/components/ui_app_page.hpp`。它负责创建独立 screen 和 LVGL 输入组。 +`AppPageRoot` 是所有内置页面的根基类,位于 `projects/APPLaunch/main/ui/ui_app_page.hpp`。它负责创建独立 screen 和 LVGL 输入组。 ```cpp class AppPageRoot @@ -60,7 +60,7 @@ public: 关键点: -- `root_screen_` 是页面自己的顶层 screen,不挂在首页 `ui_Screen1` 下。 +- `root_screen_` 是页面自己的顶层 screen,不挂在首页 `UILaunchPage::screen()` 下。 - `input_group_` 默认只加入 `root_screen_`,启动页面时会被绑定到当前 `lv_indev_t`。 - `navigate_home` 由 `LaunchImpl` 注入,页面按 ESC 或完成任务后调用它返回首页。 - 析构函数删除 `root_screen_` 和 `input_group_`,因此页面内创建的 LVGL 子对象会随 screen 一起释放。 @@ -106,7 +106,7 @@ public: 关键源码路径: -- `projects/APPLaunch/main/ui/components/ui_app_page.hpp`:`UIAppTopBar`、`AppTopBarRegion`。 +- `projects/APPLaunch/main/ui/ui_app_page.hpp`:`UIAppTopBar`、`AppTopBarRegion`。 - `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`:`cp0_wifi_get_status()`、`cp0_time_str()`、`cp0_battery_read()` 等接口声明。 顶栏资源使用 `cp0_file_path_c()`: @@ -142,7 +142,7 @@ app::app(std::string name, std::string icon, page_t) 实际代码在 `projects/APPLaunch/main/ui/Launch.cpp` 中,核心逻辑是: -1. 首页按 ENTER 后调用 `cpp_app_launch()`。 +1. 首页释放 ENTER 后由 `UILaunchPage::handle_home_key()` 调用 `launch_selected_app()`。 2. `UILaunchPage::launch_selected_app()` 转到 `Launch::launch_app()`。 3. `LaunchImpl::launch_app()` 找到当前 app,并执行该 app 的 `launch` 函数。 4. 内置页面创建对象、加载 screen、切换输入组。 @@ -162,7 +162,7 @@ if (navigate_home) - `lv_timer_enable(true)` 恢复 LVGL timer。 - `UILaunchPage::bind_home_input_group()` 绑定首页输入组。 -- `lv_disp_load_scr(ui_Screen1)` 加载首页 screen。 +- `launch_page_->show_home_screen()` 加载首页 screen 并绑定首页输入组。 - `app_Page.reset()` 释放当前页面对象。 注意事项: @@ -173,7 +173,7 @@ if (navigate_home) ## 5. 当前内置页面列表 -页面实现集中在 `projects/APPLaunch/main/ui/components/page_app/`。 +页面实现集中在 `projects/APPLaunch/main/ui/page_app/`。 | 页面类 | 文件 | 启动器名称 | 继承 | 说明 | | --- | --- | --- | --- | --- | @@ -319,7 +319,7 @@ lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); - `carousel_elements` 保存 5 张卡片、5 个标题和 5 个页点。 - 左右切换时调用 `switch_left()` / `switch_right()`,动画完成后旋转数组并让 `LaunchImpl` 更新远端槽位内容。 -- ENTER 触发 `app_launch()`,最终调用当前 app 的 `launch()`。 +- ENTER 触发 `UILaunchPage::launch_selected_app()`,最终调用当前 app 的 `launch()`。 内置页面不直接操作首页轮播;返回首页后轮播状态由 `LaunchImpl` 保持。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" index bbbc2443..45b2f1e5 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" @@ -1,6 +1,6 @@ # 06 - 资源与配置系统 -本章说明 APPLaunch 的运行时资源目录、路径解析规则、`.desktop` 动态应用文件、配置 API、设置页配置 key 以及资源使用注意事项。重点源码在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`。 +本章说明 APPLaunch 的运行时资源目录、路径解析规则、`.desktop` 动态应用文件、配置 API、设置页配置 key 以及资源使用注意事项。重点源码在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`。 ## 1. 资源系统概览 @@ -22,7 +22,7 @@ cp0_lvgl_file.cpp / sdl_lvgl_file.cpp +-- keyboard_device / keyboard_map / lock_file 等特殊路径 ``` -页面常用包装函数位于 `projects/APPLaunch/main/ui/components/ui_app_page.hpp`: +页面常用包装函数位于 `projects/APPLaunch/main/ui/ui_app_page.hpp`: ```cpp static inline std::string img_path(const char *name) diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" index 459369b6..5699d5a8 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" @@ -1,6 +1,6 @@ # 07 - 输入系统与按键映射 -本章说明 APPLaunch 的键盘输入线程、`key_item` 事件结构、LVGL 事件投递、首页和内置页面按键映射、终端输入转义以及调试注意事项。重点源码在 `projects/APPLaunch/main/include/keyboard_input.h`、`projects/APPLaunch/main/ui/ui.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`、`projects/APPLaunch/main/ui/UILaunchPage.cpp` 和 `projects/APPLaunch/main/ui/components/page_app/*.hpp`。 +本章说明 APPLaunch 的键盘输入线程、`key_item` 事件结构、LVGL 事件投递、首页和内置页面按键映射、终端输入转义以及调试注意事项。重点源码在 `projects/APPLaunch/main/include/keyboard_input.h`、`projects/APPLaunch/main/ui/ui.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`、`projects/APPLaunch/main/ui/UILaunchPage.cpp` 和 `projects/APPLaunch/main/ui/page_app/*.hpp`。 ## 1. 输入系统概览 @@ -230,7 +230,7 @@ free(elm); ## 6. 首页按键映射 -首页按键处理在 `projects/APPLaunch/main/ui/UILaunchPage.cpp::main_key_switch()`。 +首页按键处理在 `UILaunchPage::handle_home_key()`;LVGL C 回调入口是 `projects/APPLaunch/main/ui/UILaunchPage.cpp` 中的 `UILaunchPage::on_home_key()`。 先将 CardputerZero 上常用的 `F/X/Z/C` 映射为方向键: @@ -257,7 +257,7 @@ static uint32_t fzxc_to_arrow(uint32_t key) | `KEY_F12` | released | 开关绿色全屏调试遮罩,设置 `lvping_lock` | | `KEY_UP` / `KEY_DOWN` 或 `F` / `X` | pressed/repeat | 当前首页未定义动作 | -注意:`main_key_switch()` 对左右键在 press 阶段处理,因此长按可能产生 repeat 并连续切换;ENTER 在 release 阶段启动,避免按下期间重复启动。 +注意:`handle_home_key()` 对左右键在 press 阶段处理,因此长按可能产生 repeat 并连续切换;ENTER 在 release 阶段启动,避免按下期间重复启动。日志 tag 仍保留 `main_key_switch` 字样,方便兼容旧调试输出。 ## 7. 内置页面按键映射总览 @@ -346,8 +346,7 @@ lv_timer_enable(false); int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); lv_timer_enable(true); -lv_indev_set_group(indev, UILaunchPage::home_input_group()); -lv_disp_load_scr(ui_Screen1); +launch_page_->show_home_screen(); LVGL_RUN_FLAGE = 1; ``` @@ -377,10 +376,11 @@ lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); 返回首页: ```cpp -UILaunchPage::bind_home_input_group(); -lv_disp_load_scr(ui_Screen1); +launch_page_->show_home_screen(); ``` +`show_home_screen()` 会加载首页 screen 并调用 `UILaunchPage::bind_home_input_group()`。 + 如果 screen 已切换但 group 仍指向旧页面,可能出现: - 页面看得见但按键无效。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" index 741ce7b4..c1484f27 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" @@ -233,17 +233,17 @@ python3 scripts/debian_packager.py distclean ### 6.1 主程序查找 -脚本从 `src_folder` 查找主程序,默认 `src_folder='../dist'`。 +脚本从 `src_folder` 查找主程序;默认 APPLaunch 目标下,`src_folder` 是相对 `projects/APPLaunch` 的 `dist`。 查找顺序: -1. `../dist/M5CardputerZero-APPLaunch` -2. `../dist/bin/M5CardputerZero-APPLaunch` +1. `projects/APPLaunch/dist/M5CardputerZero-APPLaunch` +2. `projects/APPLaunch/dist/bin/M5CardputerZero-APPLaunch` 如果两个位置都不存在,会抛出: ```text -FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist +PackError: binary M5CardputerZero-APPLaunch not found in /dist or /dist/bin ``` ### 6.2 附加应用和后端 @@ -251,9 +251,9 @@ FileNotFoundError: Binary M5CardputerZero-APPLaunch not found in ../dist 脚本会尝试包含以下可选文件: ```text -../dist/bin/M5CardputerZero-AppStore -../dist/bin/appstore.py -../dist/bin/M5CardputerZero-Calculator +projects/APPLaunch/dist/bin/M5CardputerZero-AppStore +projects/APPLaunch/dist/bin/appstore.py +projects/APPLaunch/dist/bin/M5CardputerZero-Calculator ``` 如果存在则复制到: @@ -281,7 +281,7 @@ usr/share/APPLaunch 如果源码资源树不存在,则尝试使用: ```text -../dist/APPLaunch +projects/APPLaunch/dist/APPLaunch ``` 这意味着打包时通常不只依赖 `dist/APPLaunch`,也会把工程源码目录中的 `APPLaunch/` 资源树复制进去。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" index a380591d..596bddec 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -7,8 +7,8 @@ | 入口 | 作用 | | --- | --- | | `projects/APPLaunch/main/ui/Launch.cpp` | 固定应用列表、动态 `.desktop` 扫描、启动内置页面或外部进程 | -| `projects/APPLaunch/main/ui/components/page_app/` | 内置页面实现目录,页面通常为 header-only `.hpp` | -| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | `AppPage`、顶部栏、`img_path()`、`audio_path()` 等页面公共能力 | +| `projects/APPLaunch/main/ui/page_app/` | 内置页面实现目录,页面通常为 header-only `.hpp` | +| `projects/APPLaunch/main/ui/ui_app_page.hpp` | `AppPage`、顶部栏、`img_path()`、`audio_path()` 等页面公共能力 | | `projects/APPLaunch/main/ui/components/generate_page_app_includes.py` | 构建前自动生成 `page_app.h`,把 `page_app/*.hpp` 全部 include 进来 | | `projects/APPLaunch/APPLaunch/` | 运行时资源树,打包后对应设备端 `/usr/share/APPLaunch/` | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | 设备端 `cp0_file_path()` 路径规则 | @@ -26,7 +26,7 @@ APPLaunch 的应用来源分两类: ### 2.1 新建页面文件 -在 `projects/APPLaunch/main/ui/components/page_app/` 下新增一个 `.hpp`,命名建议使用 `ui_app_xxx.hpp`。页面类继承 `AppPage`,构造函数中设置标题、创建 UI、绑定按键事件。 +在 `projects/APPLaunch/main/ui/page_app/` 下新增一个 `.hpp`,命名建议使用 `ui_app_xxx.hpp`。页面类继承 `AppPage`,构造函数中设置标题、创建 UI、绑定按键事件。 最小骨架: @@ -80,7 +80,7 @@ private: 注意事项: - 页面必须继承 `AppPage`,这样才能复用 `screen()`、`input_group()`、`navigate_home` 等机制。 -- 返回首页优先调用 `navigate_home()`,不要直接 `lv_disp_load_scr(ui_Screen1)`,否则 `LaunchImpl` 无法正确释放当前页面对象。 +- 返回首页优先调用 `navigate_home()`,不要直接加载首页 screen,否则 `LaunchImpl` 无法正确释放当前页面对象。 - 页面内如果创建 LVGL timer、文件描述符、线程或外设句柄,要在析构函数里释放。 - 页面尺寸以 320x170 为基准;常见布局是顶部栏 20px,正文 320x150。 - 资源路径不要硬编码绝对路径;图片用 `img_path("xxx.png")`,音频用 `audio_path("xxx.wav")`。 @@ -93,7 +93,7 @@ private: ui/components/generate_page_app_includes.py ``` -该脚本会扫描 `projects/APPLaunch/main/ui/components/page_app/*.hpp`,生成 `projects/APPLaunch/main/ui/components/page_app.h`。通常只要文件后缀是 `.hpp`,构建时就会自动被 include。 +该脚本会扫描 `projects/APPLaunch/main/ui/page_app/*.hpp`,生成 `projects/APPLaunch/main/ui/page_app.h`。通常只要文件后缀是 `.hpp`,构建时就会自动被 include。 如果你手动检查,`page_app.h` 中应出现: @@ -270,7 +270,7 @@ Type=Application 非终端外部应用返回首页依赖以下行为: -- 子进程正常退出,APPLaunch 会恢复 `ui_Screen1`。 +- 子进程正常退出,APPLaunch 会调用 `launch_page_->show_home_screen()` 恢复首页和输入组。 - 设备端长按 ESC 约 3 秒会向外部应用进程组发送 SIGTERM,若 3 秒未退出再 SIGKILL。 - `cp0_process_exec_blocking()` 会暂停 Launcher 键盘线程,让外部程序直接读取 evdev 输入。 @@ -356,7 +356,7 @@ lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); ## 5. 修改设置开关 -设置页集中在 `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`。当前设置包括 Launcher 应用显示开关、Boot、Screen、WiFi、Speaker、Camera、Info、About、Help、ExtPort 等。 +设置页集中在 `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`。当前设置包括 Launcher 应用显示开关、Boot、Screen、WiFi、Speaker、Camera、Info、About、Help、ExtPort 等。 ### 5.1 新增 Launcher 应用开关 @@ -411,7 +411,7 @@ sudo systemctl restart APPLaunch.service | 检查项 | 方法 | | --- | --- | -| 文件只放在正确目录 | 内置页面放 `main/ui/components/page_app/`,资源放 `APPLaunch/share/`,`.desktop` 放 `APPLaunch/applications/` | +| 文件只放在正确目录 | 内置页面放 `main/ui/page_app/`,资源放 `APPLaunch/share/`,`.desktop` 放 `APPLaunch/applications/` | | SDL2 能编译 | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | | 设备交叉编译能过 | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | | 图标能显示 | 观察日志是否有 `set panel icon missing/unreadable` | diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" index 76b5687f..5c7a967e 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" @@ -324,7 +324,7 @@ APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/ 3. 解绑 LVGL 输入组。 4. `lv_timer_enable(false)` 暂停 LVGL timer。 5. 调用 `cp0_process_exec_blocking(exec, &LVGL_HOME_KEY_FLAG, keep_root)`。 -6. 子进程退出后重新启用 timer、绑定首页 input group、加载 `ui_Screen1`、隐藏 Loading。 +6. 子进程退出后重新启用 timer、绑定首页 input group、调用 `launch_page_->show_home_screen()` 恢复首页 screen、隐藏 Loading。 ### 6.2 先确认子进程是否还在 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" index aad86546..fee2680d 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" @@ -11,9 +11,9 @@ git status --short | 任务 | 主要文件/目录 | 关键点 | 验证方式 | | --- | --- | --- | --- | -| 新增内置页面 | `projects/APPLaunch/main/ui/components/page_app/` | 新建 `ui_app_xxx.hpp`,继承 `AppPage` | SDL2 编译并打开页面 | +| 新增内置页面 | `projects/APPLaunch/main/ui/page_app/` | 新建 `ui_app_xxx.hpp`,继承 `AppPage` | SDL2 编译并打开页面 | | 注册内置页面到首页 | `projects/APPLaunch/main/ui/Launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | 首页轮播出现图标 | -| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/Launch.cpp` | 设置页写 `app_Key`,Launcher 读 `APP_ENABLED("Key")` | 切换设置后重启或刷新首页 | +| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/Launch.cpp` | 设置页写 `app_Key`,Launcher 读 `APP_ENABLED("Key")` | 切换设置后重启或刷新首页 | | 新增外部 `.desktop` 应用 | `projects/APPLaunch/APPLaunch/applications/` | 文件名必须以 `.desktop` 结尾,包含 `Name` 和 `Exec` | 日志无 skip,首页出现应用 | | 新增图标 | `projects/APPLaunch/APPLaunch/share/images/` | 内置页用 `img_path()`,`.desktop` 用 `Icon=share/images/xxx.png` | 日志无 `missing/unreadable` | | 新增音效 | `projects/APPLaunch/APPLaunch/share/audio/` | 页面用 `audio_path()` 和 `cp0_signal_audio_api()` | 设备端播放声音 | @@ -21,11 +21,11 @@ git status --short | 修改首页轮播布局 | `projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.h` | 5 个 slot、左右切换、中心卡片 | SDL2 查看动画和输入 | | 修改轮播动画 | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | 卡片移动、缩放、透明度等动画 | SDL2 连续左右切换 | | 修改首页状态栏 | `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/ui.c` | `update_home_status_bar()` 刷新 WiFi/时间/电量 | 看 `[HOME_STATUS]` 日志 | -| 修改设置页菜单 | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | `menu_init()` 增加 `MenuItem`/`SubItem` | 进入 SETTING 页面测试 | +| 修改设置页菜单 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `menu_init()` 增加 `MenuItem`/`SubItem` | 进入 SETTING 页面测试 | | 修改配置保存逻辑 | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 当前保存到 `/var/lib/applaunch/settings`,最多 32 项 | 查看 settings 文件 | | 修改资源路径规则 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | 设备端和 SDL2 要同步考虑 | SDL2 + 设备端都检查资源 | | 修改外部应用启动/返回 | `projects/APPLaunch/main/ui/Launch.cpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`、`cp0_process_exec_blocking()` | 外部应用启动、ESC 返回 | -| 修改终端应用 | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、命令执行、输入输出 | `Terminal=true` 应用验证 | +| 修改终端应用 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、命令执行、输入输出 | `Terminal=true` 应用验证 | | 修改输入映射 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | 设备端和 SDL2 输入差异 | `evtest` + SDL2 键盘 | | 修改启动流程 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、主循环 | 看 `[BOOT]` 日志 | | 修改构建依赖 | `projects/APPLaunch/main/SConstruct` | `SRCS`、`INCLUDE`、`REQUIREMENTS`、`STATIC_FILES` | scons 编译 | @@ -49,28 +49,28 @@ git status --short | `projects/APPLaunch/main/ui/ui_global_hint.cpp` | 全局提示浮层 | | `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | LVGL OS/线程辅助 | | `projects/APPLaunch/main/ui/Animation/` | 首页轮播动画实现 | -| `projects/APPLaunch/main/ui/components/ui_app_page.hpp` | 内置页面基类、顶部栏、公共资源路径辅助 | -| `projects/APPLaunch/main/ui/components/page_app.h` | 自动生成的内置页面 include 汇总 | -| `projects/APPLaunch/main/ui/components/page_app/` | 内置页面实现目录 | +| `projects/APPLaunch/main/ui/ui_app_page.hpp` | 内置页面基类、顶部栏、公共资源路径辅助 | +| `projects/APPLaunch/main/ui/page_app.h` | 自动生成的内置页面 include 汇总 | +| `projects/APPLaunch/main/ui/page_app/` | 内置页面实现目录 | | `projects/APPLaunch/main/include/` | APPLaunch 私有头和兼容输入头 | ## 3. 内置页面入口表 | 页面/功能 | 文件 | 注册名或图标 | 说明 | | --- | --- | --- | --- | -| GAME | `projects/APPLaunch/main/ui/components/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | -| SETTING | `projects/APPLaunch/main/ui/components/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | 设置页,包含应用开关、亮度、音量、WiFi、相机等 | -| MUSIC | `projects/APPLaunch/main/ui/components/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | 音乐页 | -| Compass | `projects/APPLaunch/main/ui/components/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | 指南针页 | -| IP_PANEL | `projects/APPLaunch/main/ui/components/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP 信息面板,设备端启用 | -| FILE | `projects/APPLaunch/main/ui/components/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | 文件页,设备端启用 | -| SSH | `projects/APPLaunch/main/ui/components/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH 页面,设备端启用 | -| MESH | `projects/APPLaunch/main/ui/components/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh 页面,设备端启用 | -| REC | `projects/APPLaunch/main/ui/components/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | 录音页,设备端启用 | -| CAMERA | `projects/APPLaunch/main/ui/components/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | 相机页,设备端启用 | -| LORA | `projects/APPLaunch/main/ui/components/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa 页面,设备端启用 | -| TANK | `projects/APPLaunch/main/ui/components/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | 坦克游戏,设备端启用 | -| CLI/终端 | `projects/APPLaunch/main/ui/components/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`,用于 bash、python、Terminal=true 应用 | +| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | +| SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | 设置页,包含应用开关、亮度、音量、WiFi、相机等 | +| MUSIC | `projects/APPLaunch/main/ui/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | 音乐页 | +| Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | 指南针页 | +| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP 信息面板,设备端启用 | +| FILE | `projects/APPLaunch/main/ui/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | 文件页,设备端启用 | +| SSH | `projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH 页面,设备端启用 | +| MESH | `projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh 页面,设备端启用 | +| REC | `projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | 录音页,设备端启用 | +| CAMERA | `projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | 相机页,设备端启用 | +| LORA | `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa 页面,设备端启用 | +| TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | 坦克游戏,设备端启用 | +| CLI/终端 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`,用于 bash、python、Terminal=true 应用 | 固定注册入口在 `LaunchImpl::LaunchImpl()`: diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 71704336..3eb17f08 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -18,72 +18,25 @@ std::array UILaunchPage::carousel_elements = {}; -static void rotate_carousel_left(size_t start, size_t end) +void UILaunchPage::rotate_carousel_left(size_t start, size_t end) { - auto &items = UILaunchPage::carousel_elements; + auto &items = carousel_elements; std::rotate(items.begin() + start, items.begin() + start + 1, items.begin() + end + 1); } -static void rotate_carousel_right(size_t start, size_t end) +void UILaunchPage::rotate_carousel_right(size_t start, size_t end) { - auto &items = UILaunchPage::carousel_elements; + auto &items = carousel_elements; std::rotate(items.begin() + start, items.begin() + end, items.begin() + end + 1); } -namespace { - -UILaunchPage *active_launch_page = nullptr; -lv_group_t *home_input_group = nullptr; - -// ==================== standard layout for carousel slots ==================== - -struct CarouselSlot { - lv_coord_t x; - lv_coord_t y; - lv_coord_t width; - lv_coord_t height; - bool hidden; -}; - -static const CarouselSlot CAROUSEL_SLOTS[] = { - {-177, 4, 61, 61, true}, - {-99, -6, 80, 80, false}, - {0, -16, 100, 100, false}, - {99, -6, 80, 80, false}, - {177, 4, 61, 61, true}, - {-177, LABEL_Y_SIDE, 0, 0, true}, - {-99, LABEL_Y_SIDE, 0, 0, false}, - {0, LABEL_Y_CENTER, 0, 0, false}, - {99, LABEL_Y_SIDE, 0, 0, false}, - {177, LABEL_Y_SIDE, 0, 0, true}, -}; - -// ============================================================ -// audio -// ============================================================ - -static void audio_play_ui_asset(const char *name) -{ - cp0_signal_system_play_asset(name); -} - -static void audio_play_switch(void) -{ - audio_play_ui_asset("switch.wav"); -} - -static void audio_play_enter(void) -{ - audio_play_ui_asset("enter.wav"); -} - // ============================================================ // switch panel style // ============================================================ -static void switchpanleEnable(int obj_index, int enable) +void UILaunchPage::switchpanleEnable(int obj_index, int enable) { - lv_obj_t *obj = UILaunchPage::carousel_elements[obj_index]; + lv_obj_t *obj = carousel_elements[obj_index]; if (enable) { @@ -114,9 +67,9 @@ static void switchpanleEnable(int obj_index, int enable) } -static void switchpanleEnableClick(int obj_index, int enable) +void UILaunchPage::switchpanleEnableClick(int obj_index, int enable) { - lv_obj_t *obj = UILaunchPage::carousel_elements[obj_index]; + lv_obj_t *obj = carousel_elements[obj_index]; if (enable) { @@ -129,6 +82,54 @@ static void switchpanleEnableClick(int obj_index, int enable) } + +namespace { + +UILaunchPage *active_launch_page = nullptr; +lv_group_t *home_input_group = nullptr; + +// ==================== standard layout for carousel slots ==================== + +struct CarouselSlot { + lv_coord_t x; + lv_coord_t y; + lv_coord_t width; + lv_coord_t height; + bool hidden; +}; + +static const CarouselSlot CAROUSEL_SLOTS[] = { + {-177, 4, 61, 61, true}, + {-99, -6, 80, 80, false}, + {0, -16, 100, 100, false}, + {99, -6, 80, 80, false}, + {177, 4, 61, 61, true}, + {-177, LABEL_Y_SIDE, 0, 0, true}, + {-99, LABEL_Y_SIDE, 0, 0, false}, + {0, LABEL_Y_CENTER, 0, 0, false}, + {99, LABEL_Y_SIDE, 0, 0, false}, + {177, LABEL_Y_SIDE, 0, 0, true}, +}; + +// ============================================================ +// audio +// ============================================================ + +static void audio_play_ui_asset(const char *name) +{ + cp0_signal_system_play_asset(name); +} + +static void audio_play_switch(void) +{ + audio_play_ui_asset("switch.wav"); +} + +static void audio_play_enter(void) +{ + audio_play_ui_asset("enter.wav"); +} + // ============================================================ // Force the panel to the specified slot // ============================================================ diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index b0c23c71..51ab3e40 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -51,6 +51,7 @@ class UILaunchPage : public home_base void update_right_slot(lv_obj_t *panel, lv_obj_t *label); void launch_selected_app(); +protected: static std::array carousel_elements; private: @@ -68,6 +69,10 @@ class UILaunchPage : public home_base void handle_home_key(lv_event_t *event); void handle_startup_gif_event(lv_event_t *event); + static void rotate_carousel_left(size_t start, size_t end); + static void rotate_carousel_right(size_t start, size_t end); + static void switchpanleEnable(int obj_index, int enable); + static void switchpanleEnableClick(int obj_index, int enable); static void on_left_arrow_clicked(lv_event_t *event); static void on_right_arrow_clicked(lv_event_t *event); static void on_app_clicked(lv_event_t *event); From fe84f08b031439a241637d070ed9ac0a1e7fd670 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:05:29 +0800 Subject: [PATCH 44/70] refactor(APPLaunch): remove Launch impl wrapper --- projects/APPLaunch/main/ui/Launch.cpp | 146 +++++--------------------- projects/APPLaunch/main/ui/Launch.h | 52 ++++++++- 2 files changed, 75 insertions(+), 123 deletions(-) diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 45336482..47390d40 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -71,80 +71,11 @@ Terminal=true Icon=share/images/e-Mail_80.png */ -// Forward declarations -class LaunchImpl; - // ============================================================ -// Type tag +// Launch // ============================================================ -template -struct page_t -{ - using type = PageT; -}; - -template -inline constexpr page_t page_v{}; - -// ============================================================ -// app:unified app descriptor + launcher -// ============================================================ -struct app -{ - std::string Name; - std::string Icon; - std::string Exec; - - std::function launch; - - // ① External command - app(std::string name, - std::string icon, - std::string exec, - bool terminal); - - // ① External command - app(std::string name, - std::string icon, - std::string exec, - bool terminal, - bool sysplause); - - // ① External command (with run_as_root) - app(std::string name, - std::string icon, - std::string exec, - bool terminal, - bool sysplause, - bool run_as_root); - - // ② Built-in UI page - template - app(std::string name, - std::string icon, - page_t /*tag*/); -}; - -// ============================================================ -// LaunchImpl -// ============================================================ -class LaunchImpl +void Launch::bind_ui() { -private: - std::shared_ptr launch_page_; - int current_app = 2; - cp0_watcher_t dir_watcher = NULL; - lv_timer_t *watch_timer = nullptr; // LVGL 3s timer - int fixed_count; - -public: - std::list app_list; - std::shared_ptr app_Page; - std::shared_ptr home_Page; -public: - explicit LaunchImpl(std::shared_ptr launch_page) - : launch_page_(std::move(launch_page)) - { // Fixed icon; users cannot modify it app_list.emplace_back("Python", cp0_file_path("python_100.png"), "python3", true, false); @@ -243,15 +174,15 @@ class LaunchImpl } - void launch_app() +void Launch::launch_app() { auto it = std::next(app_list.begin(), current_app); it->launch(this); } - static void lv_go_back_home(void *arg) +void Launch::lv_go_back_home(void *arg) { - auto self = (LaunchImpl *)arg; + auto self = (Launch *)arg; SLOGI("[HOME] lv_go_back_home executing (page=%p)", self->app_Page.get()); lv_timer_enable(true); if (self->launch_page_) @@ -262,14 +193,14 @@ class LaunchImpl SLOGI("[HOME] lv_go_back_home done, on launcher home"); } - void go_back_home() +void Launch::go_back_home() { SLOGI("[HOME] go_back_home() requested, scheduling async call (page=%p)", app_Page.get()); lv_async_call(lv_go_back_home, this); } // Changed to accept std::string and no longer depend on app::Exec - void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) +void Launch::launch_Exec_in_terminal(const std::string &exec, bool sysplause) { SLOGI("Launching terminal app: %s", exec.c_str()); /* Instant visual feedback; paint before the (potentially slow) @@ -280,7 +211,7 @@ class LaunchImpl app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); - p->navigate_home = std::bind(&LaunchImpl::go_back_home, this); + p->navigate_home = std::bind(&Launch::go_back_home, this); p->terminal_sysplause = sysplause; /* Console page fully covers APP_Container; safe to hide now. * The heavy exec() call below will still run while the terminal @@ -289,7 +220,7 @@ class LaunchImpl p->exec(exec); } - void launch_Exec(const std::string &exec, bool keep_root = false) +void Launch::launch_Exec(const std::string &exec, bool keep_root) { SLOGI("Launching external app: %s (keep_root=%d)", exec.c_str(), keep_root); /* Show overlay BEFORE we tear down LVGL input/timers so the user @@ -320,7 +251,7 @@ class LaunchImpl LVGL_RUN_FLAGE = 1; } - void update_left_slot(lv_obj_t *panel, lv_obj_t *label) +void Launch::update_left_slot(lv_obj_t *panel, lv_obj_t *label) { current_app = current_app == (int)app_list.size() - 1 ? 0 : current_app + 1; int next_app = current_app; @@ -331,7 +262,7 @@ class LaunchImpl panel_set_icon(panel, it->Icon.c_str()); } - void update_right_slot(lv_obj_t *panel, lv_obj_t *label) +void Launch::update_right_slot(lv_obj_t *panel, lv_obj_t *label) { current_app = current_app == 0 ? (int)app_list.size() - 1 : current_app - 1; int next_app = current_app; @@ -342,7 +273,7 @@ class LaunchImpl panel_set_icon(panel, it->Icon.c_str()); } - void applications_load() +void Launch::applications_load() { const std::string app_dir_path = cp0_file_path("applications"); const char *app_dir = app_dir_path.c_str(); @@ -465,7 +396,7 @@ class LaunchImpl // ============================================================ // Initialize inotify in non-blocking mode and watch the applications directory // ============================================================ - void inotify_init_watch() +void Launch::inotify_init_watch() { const std::string app_dir_path = cp0_file_path("applications"); dir_watcher = cp0_dir_watch_start(app_dir_path.c_str()); @@ -474,7 +405,7 @@ class LaunchImpl // ============================================================ // Refresh UI panels (update 5 slots from current_app) // ============================================================ - void refresh_ui_panels() +void Launch::refresh_ui_panels() { int sz = (int)app_list.size(); if (sz == 0) @@ -526,7 +457,7 @@ class LaunchImpl // ============================================================ // Reload the dynamic app list (keep fixed entries and rescan applications directory) // ============================================================ - void applications_reload() +void Launch::applications_reload() { int sz = (int)app_list.size(); if (sz > fixed_count) @@ -541,9 +472,9 @@ class LaunchImpl // ============================================================ // LVGL timer callback: check inotify events and refresh the list on changes // ============================================================ - static void app_dir_watch_cb(lv_timer_t *timer) +void Launch::app_dir_watch_cb(lv_timer_t *timer) { - auto *self = static_cast(lv_timer_get_user_data(timer)); + auto *self = static_cast(lv_timer_get_user_data(timer)); if (!self || !self->dir_watcher) return; @@ -554,18 +485,16 @@ class LaunchImpl } } - ~LaunchImpl(); -}; // ============================================================ -// app constructor implementation (placed after LaunchImpl definition) +// app constructor implementation (placed after Launch definition) // ============================================================ inline app::app(std::string name, std::string icon, std::string exec, bool terminal) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [exec = std::move(exec), terminal](LaunchImpl *ctx) + launch = [exec = std::move(exec), terminal](Launch *ctx) { if (terminal) ctx->launch_Exec_in_terminal(exec); @@ -580,7 +509,7 @@ inline app::app(std::string name, bool terminal, bool sysplause) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [exec = std::move(exec), terminal, sysplause](LaunchImpl *ctx) + launch = [exec = std::move(exec), terminal, sysplause](Launch *ctx) { if (terminal) ctx->launch_Exec_in_terminal(exec, sysplause); @@ -596,7 +525,7 @@ inline app::app(std::string name, bool sysplause, bool run_as_root) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [exec = std::move(exec), terminal, sysplause, run_as_root](LaunchImpl *ctx) + launch = [exec = std::move(exec), terminal, sysplause, run_as_root](Launch *ctx) { if (terminal) ctx->launch_Exec_in_terminal(exec, sysplause); @@ -610,7 +539,7 @@ app::app(std::string name, std::string icon, page_t /*tag*/) : Name(std::move(name)), Icon(std::move(icon)){ - launch = [](LaunchImpl *self) + launch = [](Launch *self) { /* Instant feedback: show the overlay, then force an immediate * redraw so it actually paints BEFORE the (sometimes slow) page @@ -625,7 +554,7 @@ app::app(std::string name, lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); p->navigate_home = - std::bind(&LaunchImpl::go_back_home, self); + std::bind(&Launch::go_back_home, self); /* Page is now attached and drawable; hide the overlay. The * next LVGL frame will paint the new page without it. */ ui_loading::hide(); @@ -633,9 +562,9 @@ app::app(std::string name, } // ============================================================ -// LaunchImpl destructor implementation +// Launch destructor implementation // ============================================================ -LaunchImpl::~LaunchImpl() +Launch::~Launch() { if (watch_timer) { @@ -651,32 +580,7 @@ LaunchImpl::~LaunchImpl() Launch::Launch() = default; -Launch::~Launch() = default; - void Launch::set_launch_page(std::shared_ptr launch_page) { launch_page_ = std::move(launch_page); } - -void Launch::bind_ui() -{ - impl_ = std::make_unique(launch_page_); -} - -void Launch::update_left_slot(lv_obj_t *panel, lv_obj_t *label) -{ - if (impl_) - impl_->update_left_slot(panel, label); -} - -void Launch::update_right_slot(lv_obj_t *panel, lv_obj_t *label) -{ - if (impl_) - impl_->update_right_slot(panel, label); -} - -void Launch::launch_app() -{ - if (impl_) - impl_->launch_app(); -} diff --git a/projects/APPLaunch/main/ui/Launch.h b/projects/APPLaunch/main/ui/Launch.h index 6215358b..1ce66e7b 100644 --- a/projects/APPLaunch/main/ui/Launch.h +++ b/projects/APPLaunch/main/ui/Launch.h @@ -7,11 +7,40 @@ #pragma once #include "lvgl/lvgl.h" +#include "cp0_lvgl_app.h" + +#include +#include #include +#include -class LaunchImpl; +class Launch; class UILaunchPage; +template +struct page_t +{ + using type = PageT; +}; + +template +inline constexpr page_t page_v{}; + +struct app +{ + std::string Name; + std::string Icon; + std::string Exec; + std::function launch; + + app(std::string name, std::string icon, std::string exec, bool terminal); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause, bool run_as_root); + + template + app(std::string name, std::string icon, page_t tag); +}; + class Launch { public: @@ -25,6 +54,25 @@ class Launch void launch_app(); private: - std::unique_ptr impl_; + friend struct app; + + void go_back_home(); + void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true); + void launch_Exec(const std::string &exec, bool keep_root = false); + void applications_load(); + void inotify_init_watch(); + void refresh_ui_panels(); + void applications_reload(); + + static void lv_go_back_home(void *arg); + static void app_dir_watch_cb(lv_timer_t *timer); + std::shared_ptr launch_page_; + int current_app = 2; + cp0_watcher_t dir_watcher = NULL; + lv_timer_t *watch_timer = nullptr; + int fixed_count = 0; + std::list app_list; + std::shared_ptr app_Page; + std::shared_ptr home_Page; }; From a6d46a05b6fed982c1eb2004342fd2915beb773d Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:10:07 +0800 Subject: [PATCH 45/70] Make carousel elements instance state --- projects/APPLaunch/main/ui/Launch.cpp | 40 +- projects/APPLaunch/main/ui/UILaunchPage.cpp | 436 ++++++++++---------- projects/APPLaunch/main/ui/UILaunchPage.h | 14 +- 3 files changed, 244 insertions(+), 246 deletions(-) diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 47390d40..77f78325 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -92,32 +92,32 @@ void Launch::bind_ui() { auto it = std::next(app_list.begin(), 0); - lv_label_set_text(UILaunchPage::label(0), it->Name.c_str()); - panel_set_icon(UILaunchPage::panel(0), it->Icon.c_str()); + lv_label_set_text(launch_page_->label(0), it->Name.c_str()); + panel_set_icon(launch_page_->panel(0), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 1); - lv_label_set_text(UILaunchPage::label(1), it->Name.c_str()); - panel_set_icon(UILaunchPage::panel(1), it->Icon.c_str()); + lv_label_set_text(launch_page_->label(1), it->Name.c_str()); + panel_set_icon(launch_page_->panel(1), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 2); - lv_label_set_text(UILaunchPage::label(2), it->Name.c_str()); - panel_set_icon(UILaunchPage::panel(2), it->Icon.c_str()); + lv_label_set_text(launch_page_->label(2), it->Name.c_str()); + panel_set_icon(launch_page_->panel(2), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 3); - lv_label_set_text(UILaunchPage::label(3), it->Name.c_str()); - panel_set_icon(UILaunchPage::panel(3), it->Icon.c_str()); + lv_label_set_text(launch_page_->label(3), it->Name.c_str()); + panel_set_icon(launch_page_->panel(3), it->Icon.c_str()); } { auto it = std::next(app_list.begin(), 4); - lv_label_set_text(UILaunchPage::label(4), it->Name.c_str()); - panel_set_icon(UILaunchPage::panel(4), it->Icon.c_str()); + lv_label_set_text(launch_page_->label(4), it->Name.c_str()); + panel_set_icon(launch_page_->panel(4), it->Icon.c_str()); } // Dynamic icons filtered by Settings configuration @@ -424,32 +424,32 @@ void Launch::refresh_ui_panels() // far left outside (hidden) { auto &a = app_at(current_app - 2); - lv_label_set_text(UILaunchPage::label(0), a.Name.c_str()); - panel_set_icon(UILaunchPage::panel(0), a.Icon.c_str()); + lv_label_set_text(launch_page_->label(0), a.Name.c_str()); + panel_set_icon(launch_page_->panel(0), a.Icon.c_str()); } // left { auto &a = app_at(current_app - 1); - lv_label_set_text(UILaunchPage::label(1), a.Name.c_str()); - panel_set_icon(UILaunchPage::panel(1), a.Icon.c_str()); + lv_label_set_text(launch_page_->label(1), a.Name.c_str()); + panel_set_icon(launch_page_->panel(1), a.Icon.c_str()); } // center { auto &a = app_at(current_app); - lv_label_set_text(UILaunchPage::label(2), a.Name.c_str()); - panel_set_icon(UILaunchPage::panel(2), a.Icon.c_str()); + lv_label_set_text(launch_page_->label(2), a.Name.c_str()); + panel_set_icon(launch_page_->panel(2), a.Icon.c_str()); } // right { auto &a = app_at(current_app + 1); - lv_label_set_text(UILaunchPage::label(3), a.Name.c_str()); - panel_set_icon(UILaunchPage::panel(3), a.Icon.c_str()); + lv_label_set_text(launch_page_->label(3), a.Name.c_str()); + panel_set_icon(launch_page_->panel(3), a.Icon.c_str()); } // far right outside (hidden) { auto &a = app_at(current_app + 2); - lv_label_set_text(UILaunchPage::label(4), a.Name.c_str()); - panel_set_icon(UILaunchPage::panel(4), a.Icon.c_str()); + lv_label_set_text(launch_page_->label(4), a.Name.c_str()); + panel_set_icon(launch_page_->panel(4), a.Icon.c_str()); } } diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 3eb17f08..51c4b70e 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -16,17 +16,15 @@ #include -std::array UILaunchPage::carousel_elements = {}; - void UILaunchPage::rotate_carousel_left(size_t start, size_t end) { - auto &items = carousel_elements; + auto &items = carousel_elements_; std::rotate(items.begin() + start, items.begin() + start + 1, items.begin() + end + 1); } void UILaunchPage::rotate_carousel_right(size_t start, size_t end) { - auto &items = carousel_elements; + auto &items = carousel_elements_; std::rotate(items.begin() + start, items.begin() + end, items.begin() + end + 1); } @@ -36,7 +34,7 @@ void UILaunchPage::rotate_carousel_right(size_t start, size_t end) void UILaunchPage::switchpanleEnable(int obj_index, int enable) { - lv_obj_t *obj = carousel_elements[obj_index]; + lv_obj_t *obj = carousel_elements_[obj_index]; if (enable) { @@ -69,7 +67,7 @@ void UILaunchPage::switchpanleEnable(int obj_index, int enable) void UILaunchPage::switchpanleEnableClick(int obj_index, int enable) { - lv_obj_t *obj = carousel_elements[obj_index]; + lv_obj_t *obj = carousel_elements_[obj_index]; if (enable) { @@ -258,12 +256,12 @@ lv_group_t *UILaunchPage::home_input_group() lv_obj_t *UILaunchPage::panel(size_t slot) { - return carousel_elements[kCardFarLeft + slot]; + return carousel_elements_[kCardFarLeft + slot]; } lv_obj_t *UILaunchPage::label(size_t slot) { - return carousel_elements[kTitleFarLeft + slot]; + return carousel_elements_[kTitleFarLeft + slot]; } void UILaunchPage::bind_home_input_group() @@ -343,23 +341,23 @@ void UILaunchPage::finish_switch_animation() { for (int i = 0; i < 5; i++) { - snap_panel_to_slot(carousel_elements[i], i); + snap_panel_to_slot(carousel_elements_[i], i); } for (int i = 5; i < 10; i++) { - snap_label_to_slot(carousel_elements[i], i); + snap_label_to_slot(carousel_elements_[i], i); } is_animating_ = false; for (int i = 0; i < 5; i++) { uint32_t color = (i == 2) ? BORDER_COLOR_CENTER : BORDER_COLOR_SIDE; - lv_obj_set_style_border_color(carousel_elements[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[i], lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); } for (int i = 5; i < 10; i++) { - lv_obj_set_style_text_font(carousel_elements[i], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements_[i], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); } run_pending_switch(); @@ -392,17 +390,17 @@ void UILaunchPage::switch_right() is_animating_ = true; - lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(carousel_elements_[0], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_right(carousel_elements.data(), [this]() { finish_switch_animation(); }); + launcher_home_animation::animate_right(carousel_elements_.data(), [this]() { finish_switch_animation(); }); - snap_panel_to_slot(carousel_elements[4], 0); + snap_panel_to_slot(carousel_elements_[4], 0); - lv_obj_clear_flag(carousel_elements[5], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(carousel_elements_[5], LV_OBJ_FLAG_HIDDEN); - snap_label_to_slot(carousel_elements[9], 5); + snap_label_to_slot(carousel_elements_[9], 5); - update_right_slot(carousel_elements[4], carousel_elements[9]); + update_right_slot(carousel_elements_[4], carousel_elements_[9]); switchpanleEnableClick(2, 0); rotate_carousel_right(0, 4); @@ -427,17 +425,17 @@ void UILaunchPage::switch_left() is_animating_ = true; - lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(carousel_elements_[4], LV_OBJ_FLAG_HIDDEN); - launcher_home_animation::animate_left(carousel_elements.data(), [this]() { finish_switch_animation(); }); + launcher_home_animation::animate_left(carousel_elements_.data(), [this]() { finish_switch_animation(); }); - snap_panel_to_slot(carousel_elements[0], 4); + snap_panel_to_slot(carousel_elements_[0], 4); - lv_obj_clear_flag(carousel_elements[9], LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(carousel_elements_[9], LV_OBJ_FLAG_HIDDEN); - snap_label_to_slot(carousel_elements[5], 9); + snap_label_to_slot(carousel_elements_[5], 9); - update_left_slot(carousel_elements[0], carousel_elements[5]); + update_left_slot(carousel_elements_[0], carousel_elements_[5]); switchpanleEnableClick(2, 0); rotate_carousel_left(0, 4); @@ -581,7 +579,7 @@ void UILaunchPage::on_startup_gif_event(lv_event_t *event) void UILaunchPage::create_screen() { - if (carousel_elements[kCardCenter]) + if (carousel_elements_[kCardCenter]) return; create_app_container(content_container()); @@ -598,157 +596,157 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_size(app_container, 320, 150); lv_obj_clear_flag(app_container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - carousel_elements[kPageDot0] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kPageDot0], 5); - lv_obj_set_height(carousel_elements[kPageDot0], 5); - lv_obj_set_x(carousel_elements[kPageDot0], -20); - lv_obj_set_y(carousel_elements[kPageDot0], 70); - lv_obj_set_align(carousel_elements[kPageDot0], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kPageDot0], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kPageDot1] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kPageDot1], 5); - lv_obj_set_height(carousel_elements[kPageDot1], 5); - lv_obj_set_x(carousel_elements[kPageDot1], -10); - lv_obj_set_y(carousel_elements[kPageDot1], 70); - lv_obj_set_align(carousel_elements[kPageDot1], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kPageDot1], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kPageDot2] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kPageDot2], 10); - lv_obj_set_height(carousel_elements[kPageDot2], 10); - lv_obj_set_x(carousel_elements[kPageDot2], 0); - lv_obj_set_y(carousel_elements[kPageDot2], 70); - lv_obj_set_align(carousel_elements[kPageDot2], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kPageDot2], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(carousel_elements[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot2], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kPageDot3] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kPageDot3], 5); - lv_obj_set_height(carousel_elements[kPageDot3], 5); - lv_obj_set_x(carousel_elements[kPageDot3], 10); - lv_obj_set_y(carousel_elements[kPageDot3], 70); - lv_obj_set_align(carousel_elements[kPageDot3], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kPageDot3], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kPageDot4] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kPageDot4], 5); - lv_obj_set_height(carousel_elements[kPageDot4], 5); - lv_obj_set_x(carousel_elements[kPageDot4], 20); - lv_obj_set_y(carousel_elements[kPageDot4], 70); - lv_obj_set_align(carousel_elements[kPageDot4], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kPageDot4], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_grad_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kTitleCenter] = lv_label_create(app_container); - lv_obj_set_width(carousel_elements[kTitleCenter], LV_SIZE_CONTENT); - lv_obj_set_height(carousel_elements[kTitleCenter], LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(carousel_elements[kTitleCenter], 0); - lv_obj_set_y(carousel_elements[kTitleCenter], LABEL_Y_CENTER); - lv_obj_set_align(carousel_elements[kTitleCenter], LV_ALIGN_CENTER); - lv_label_set_text(carousel_elements[kTitleCenter], "CLI"); - lv_obj_set_style_text_font(carousel_elements[kTitleCenter], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(carousel_elements[kTitleCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(carousel_elements[kTitleCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kTitleRight] = lv_label_create(app_container); - lv_obj_set_width(carousel_elements[kTitleRight], LV_SIZE_CONTENT); - lv_obj_set_height(carousel_elements[kTitleRight], LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(carousel_elements[kTitleRight], 99); - lv_obj_set_y(carousel_elements[kTitleRight], LABEL_Y_SIDE); - lv_obj_set_align(carousel_elements[kTitleRight], LV_ALIGN_CENTER); - lv_label_set_text(carousel_elements[kTitleRight], "GAME"); - lv_obj_set_style_text_color(carousel_elements[kTitleRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(carousel_elements[kTitleRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(carousel_elements[kTitleRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kTitleLeft] = lv_label_create(app_container); - lv_obj_set_width(carousel_elements[kTitleLeft], LV_SIZE_CONTENT); - lv_obj_set_height(carousel_elements[kTitleLeft], LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(carousel_elements[kTitleLeft], -99); - lv_obj_set_y(carousel_elements[kTitleLeft], LABEL_Y_SIDE); - lv_obj_set_align(carousel_elements[kTitleLeft], LV_ALIGN_CENTER); - lv_label_set_text(carousel_elements[kTitleLeft], "STORE"); - lv_obj_set_style_text_color(carousel_elements[kTitleLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(carousel_elements[kTitleLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(carousel_elements[kTitleLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kCardLeft] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kCardLeft], 80); - lv_obj_set_height(carousel_elements[kCardLeft], 80); - lv_obj_set_x(carousel_elements[kCardLeft], -99); - lv_obj_set_y(carousel_elements[kCardLeft], -6); - lv_obj_set_align(carousel_elements[kCardLeft], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kCardLeft], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(carousel_elements[kCardLeft], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(carousel_elements[kCardLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kCardLeft], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kCardLeft], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kCardLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kCardCenter] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kCardCenter], 100); - lv_obj_set_height(carousel_elements[kCardCenter], 100); - lv_obj_set_x(carousel_elements[kCardCenter], 0); - lv_obj_set_y(carousel_elements[kCardCenter], -16); - lv_obj_set_align(carousel_elements[kCardCenter], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kCardCenter], LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(carousel_elements[kCardCenter], 22, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(carousel_elements[kCardCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kCardCenter], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kCardCenter], lv_color_hex(0x444444), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kCardCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(carousel_elements[kCardCenter], 2, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kCardRight] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kCardRight], 80); - lv_obj_set_height(carousel_elements[kCardRight], 80); - lv_obj_set_x(carousel_elements[kCardRight], 99); - lv_obj_set_y(carousel_elements[kCardRight], -6); - lv_obj_set_align(carousel_elements[kCardRight], LV_ALIGN_CENTER); - lv_obj_clear_flag(carousel_elements[kCardRight], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(carousel_elements[kCardRight], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(carousel_elements[kCardRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kCardRight], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kCardRight], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kCardRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kCardFarRight] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kCardFarRight], 61); - lv_obj_set_height(carousel_elements[kCardFarRight], 61); - lv_obj_set_x(carousel_elements[kCardFarRight], 177); - lv_obj_set_y(carousel_elements[kCardFarRight], 4); - lv_obj_set_align(carousel_elements[kCardFarRight], LV_ALIGN_CENTER); - lv_obj_add_flag(carousel_elements[kCardFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_clear_flag(carousel_elements[kCardFarRight], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(carousel_elements[kCardFarRight], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(carousel_elements[kCardFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kCardFarRight], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kCardFarRight], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kCardFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + carousel_elements_[kPageDot0] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kPageDot0], 5); + lv_obj_set_height(carousel_elements_[kPageDot0], 5); + lv_obj_set_x(carousel_elements_[kPageDot0], -20); + lv_obj_set_y(carousel_elements_[kPageDot0], 70); + lv_obj_set_align(carousel_elements_[kPageDot0], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kPageDot0], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements_[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements_[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kPageDot0], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kPageDot0], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kPageDot1] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kPageDot1], 5); + lv_obj_set_height(carousel_elements_[kPageDot1], 5); + lv_obj_set_x(carousel_elements_[kPageDot1], -10); + lv_obj_set_y(carousel_elements_[kPageDot1], 70); + lv_obj_set_align(carousel_elements_[kPageDot1], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kPageDot1], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements_[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements_[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kPageDot1], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kPageDot1], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kPageDot2] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kPageDot2], 10); + lv_obj_set_height(carousel_elements_[kPageDot2], 10); + lv_obj_set_x(carousel_elements_[kPageDot2], 0); + lv_obj_set_y(carousel_elements_[kPageDot2], 70); + lv_obj_set_align(carousel_elements_[kPageDot2], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kPageDot2], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements_[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements_[kPageDot2], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kPageDot2], lv_color_hex(0xCCCC33), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kPageDot2], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kPageDot3] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kPageDot3], 5); + lv_obj_set_height(carousel_elements_[kPageDot3], 5); + lv_obj_set_x(carousel_elements_[kPageDot3], 10); + lv_obj_set_y(carousel_elements_[kPageDot3], 70); + lv_obj_set_align(carousel_elements_[kPageDot3], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kPageDot3], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements_[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements_[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kPageDot3], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kPageDot3], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kPageDot4] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kPageDot4], 5); + lv_obj_set_height(carousel_elements_[kPageDot4], 5); + lv_obj_set_x(carousel_elements_[kPageDot4], 20); + lv_obj_set_y(carousel_elements_[kPageDot4], 70); + lv_obj_set_align(carousel_elements_[kPageDot4], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kPageDot4], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(carousel_elements_[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_grad_color(carousel_elements_[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kPageDot4], lv_color_hex(0x4A4C4A), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kPageDot4], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kTitleCenter] = lv_label_create(app_container); + lv_obj_set_width(carousel_elements_[kTitleCenter], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements_[kTitleCenter], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements_[kTitleCenter], 0); + lv_obj_set_y(carousel_elements_[kTitleCenter], LABEL_Y_CENTER); + lv_obj_set_align(carousel_elements_[kTitleCenter], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements_[kTitleCenter], "CLI"); + lv_obj_set_style_text_font(carousel_elements_[kTitleCenter], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(carousel_elements_[kTitleCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements_[kTitleCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kTitleRight] = lv_label_create(app_container); + lv_obj_set_width(carousel_elements_[kTitleRight], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements_[kTitleRight], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements_[kTitleRight], 99); + lv_obj_set_y(carousel_elements_[kTitleRight], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements_[kTitleRight], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements_[kTitleRight], "GAME"); + lv_obj_set_style_text_color(carousel_elements_[kTitleRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements_[kTitleRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements_[kTitleRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kTitleLeft] = lv_label_create(app_container); + lv_obj_set_width(carousel_elements_[kTitleLeft], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements_[kTitleLeft], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements_[kTitleLeft], -99); + lv_obj_set_y(carousel_elements_[kTitleLeft], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements_[kTitleLeft], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements_[kTitleLeft], "STORE"); + lv_obj_set_style_text_color(carousel_elements_[kTitleLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements_[kTitleLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(carousel_elements_[kTitleLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kCardLeft] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kCardLeft], 80); + lv_obj_set_height(carousel_elements_[kCardLeft], 80); + lv_obj_set_x(carousel_elements_[kCardLeft], -99); + lv_obj_set_y(carousel_elements_[kCardLeft], -6); + lv_obj_set_align(carousel_elements_[kCardLeft], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kCardLeft], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements_[kCardLeft], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements_[kCardLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kCardLeft], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kCardLeft], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kCardLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kCardCenter] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kCardCenter], 100); + lv_obj_set_height(carousel_elements_[kCardCenter], 100); + lv_obj_set_x(carousel_elements_[kCardCenter], 0); + lv_obj_set_y(carousel_elements_[kCardCenter], -16); + lv_obj_set_align(carousel_elements_[kCardCenter], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kCardCenter], LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(carousel_elements_[kCardCenter], 22, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements_[kCardCenter], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kCardCenter], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kCardCenter], lv_color_hex(0x444444), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kCardCenter], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(carousel_elements_[kCardCenter], 2, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kCardRight] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kCardRight], 80); + lv_obj_set_height(carousel_elements_[kCardRight], 80); + lv_obj_set_x(carousel_elements_[kCardRight], 99); + lv_obj_set_y(carousel_elements_[kCardRight], -6); + lv_obj_set_align(carousel_elements_[kCardRight], LV_ALIGN_CENTER); + lv_obj_clear_flag(carousel_elements_[kCardRight], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements_[kCardRight], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements_[kCardRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kCardRight], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kCardRight], lv_color_hex(0x222222), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kCardRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kCardFarRight] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kCardFarRight], 61); + lv_obj_set_height(carousel_elements_[kCardFarRight], 61); + lv_obj_set_x(carousel_elements_[kCardFarRight], 177); + lv_obj_set_y(carousel_elements_[kCardFarRight], 4); + lv_obj_set_align(carousel_elements_[kCardFarRight], LV_ALIGN_CENTER); + lv_obj_add_flag(carousel_elements_[kCardFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_clear_flag(carousel_elements_[kCardFarRight], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements_[kCardFarRight], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements_[kCardFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kCardFarRight], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kCardFarRight], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kCardFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); left_arrow_button_ = lv_btn_create(app_container); lv_obj_set_width(left_arrow_button_, 17); @@ -780,51 +778,51 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_set_style_shadow_color(right_arrow_button_, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_shadow_opa(right_arrow_button_, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - carousel_elements[kCardFarLeft] = lv_obj_create(app_container); - lv_obj_set_width(carousel_elements[kCardFarLeft], 61); - lv_obj_set_height(carousel_elements[kCardFarLeft], 61); - lv_obj_set_x(carousel_elements[kCardFarLeft], -177); - lv_obj_set_y(carousel_elements[kCardFarLeft], 4); - lv_obj_set_align(carousel_elements[kCardFarLeft], LV_ALIGN_CENTER); - lv_obj_add_flag(carousel_elements[kCardFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_clear_flag(carousel_elements[kCardFarLeft], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags - lv_obj_set_style_radius(carousel_elements[kCardFarLeft], 17, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(carousel_elements[kCardFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(carousel_elements[kCardFarLeft], 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(carousel_elements[kCardFarLeft], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(carousel_elements[kCardFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kTitleFarLeft] = lv_label_create(app_container); - lv_obj_set_width(carousel_elements[kTitleFarLeft], LV_SIZE_CONTENT); - lv_obj_set_height(carousel_elements[kTitleFarLeft], LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(carousel_elements[kTitleFarLeft], -177); - lv_obj_set_y(carousel_elements[kTitleFarLeft], LABEL_Y_SIDE); - lv_obj_set_align(carousel_elements[kTitleFarLeft], LV_ALIGN_CENTER); - lv_label_set_text(carousel_elements[kTitleFarLeft], "one"); - lv_obj_add_flag(carousel_elements[kTitleFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_set_style_text_font(carousel_elements[kTitleFarLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(carousel_elements[kTitleFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(carousel_elements[kTitleFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - carousel_elements[kTitleFarRight] = lv_label_create(app_container); - lv_obj_set_width(carousel_elements[kTitleFarRight], LV_SIZE_CONTENT); - lv_obj_set_height(carousel_elements[kTitleFarRight], LV_SIZE_CONTENT); /// 1 - lv_obj_set_x(carousel_elements[kTitleFarRight], 177); - lv_obj_set_y(carousel_elements[kTitleFarRight], LABEL_Y_SIDE); - lv_obj_set_align(carousel_elements[kTitleFarRight], LV_ALIGN_CENTER); - lv_label_set_text(carousel_elements[kTitleFarRight], "three"); - lv_obj_add_flag(carousel_elements[kTitleFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags - lv_obj_set_style_text_font(carousel_elements[kTitleFarRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(carousel_elements[kTitleFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(carousel_elements[kTitleFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_add_event_cb(carousel_elements[kCardLeft], on_app_clicked, LV_EVENT_CLICKED, this); - lv_obj_add_event_cb(carousel_elements[kCardCenter], on_app_clicked, LV_EVENT_CLICKED, this); - lv_obj_add_event_cb(carousel_elements[kCardRight], on_app_clicked, LV_EVENT_CLICKED, this); - lv_obj_add_event_cb(carousel_elements[kCardFarRight], on_app_clicked, LV_EVENT_CLICKED, this); + carousel_elements_[kCardFarLeft] = lv_obj_create(app_container); + lv_obj_set_width(carousel_elements_[kCardFarLeft], 61); + lv_obj_set_height(carousel_elements_[kCardFarLeft], 61); + lv_obj_set_x(carousel_elements_[kCardFarLeft], -177); + lv_obj_set_y(carousel_elements_[kCardFarLeft], 4); + lv_obj_set_align(carousel_elements_[kCardFarLeft], LV_ALIGN_CENTER); + lv_obj_add_flag(carousel_elements_[kCardFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_clear_flag(carousel_elements_[kCardFarLeft], (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); /// Flags + lv_obj_set_style_radius(carousel_elements_[kCardFarLeft], 17, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(carousel_elements_[kCardFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(carousel_elements_[kCardFarLeft], 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(carousel_elements_[kCardFarLeft], lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(carousel_elements_[kCardFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kTitleFarLeft] = lv_label_create(app_container); + lv_obj_set_width(carousel_elements_[kTitleFarLeft], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements_[kTitleFarLeft], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements_[kTitleFarLeft], -177); + lv_obj_set_y(carousel_elements_[kTitleFarLeft], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements_[kTitleFarLeft], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements_[kTitleFarLeft], "one"); + lv_obj_add_flag(carousel_elements_[kTitleFarLeft], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_set_style_text_font(carousel_elements_[kTitleFarLeft], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(carousel_elements_[kTitleFarLeft], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements_[kTitleFarLeft], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + carousel_elements_[kTitleFarRight] = lv_label_create(app_container); + lv_obj_set_width(carousel_elements_[kTitleFarRight], LV_SIZE_CONTENT); + lv_obj_set_height(carousel_elements_[kTitleFarRight], LV_SIZE_CONTENT); /// 1 + lv_obj_set_x(carousel_elements_[kTitleFarRight], 177); + lv_obj_set_y(carousel_elements_[kTitleFarRight], LABEL_Y_SIDE); + lv_obj_set_align(carousel_elements_[kTitleFarRight], LV_ALIGN_CENTER); + lv_label_set_text(carousel_elements_[kTitleFarRight], "three"); + lv_obj_add_flag(carousel_elements_[kTitleFarRight], LV_OBJ_FLAG_HIDDEN); /// Flags + lv_obj_set_style_text_font(carousel_elements_[kTitleFarRight], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(carousel_elements_[kTitleFarRight], lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(carousel_elements_[kTitleFarRight], 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_add_event_cb(carousel_elements_[kCardLeft], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements_[kCardCenter], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements_[kCardRight], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements_[kCardFarRight], on_app_clicked, LV_EVENT_CLICKED, this); lv_obj_add_event_cb(left_arrow_button_, on_left_arrow_clicked, LV_EVENT_CLICKED, this); lv_obj_add_event_cb(right_arrow_button_, on_right_arrow_clicked, LV_EVENT_CLICKED, this); - lv_obj_add_event_cb(carousel_elements[kCardFarLeft], on_app_clicked, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(carousel_elements_[kCardFarLeft], on_app_clicked, LV_EVENT_CLICKED, this); lv_obj_add_event_cb(screen(), on_home_key, (lv_event_code_t)LV_EVENT_KEYBOARD, this); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index 51ab3e40..494abf9d 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -44,15 +44,15 @@ class UILaunchPage : public home_base void init_input_group(); static void bind_home_input_group(); static lv_group_t *home_input_group(); - static lv_obj_t *panel(size_t slot); - static lv_obj_t *label(size_t slot); + lv_obj_t *panel(size_t slot); + lv_obj_t *label(size_t slot); void update_left_slot(lv_obj_t *panel, lv_obj_t *label); void update_right_slot(lv_obj_t *panel, lv_obj_t *label); void launch_selected_app(); protected: - static std::array carousel_elements; + std::array carousel_elements_ = {}; private: enum class PendingSwitch { @@ -69,10 +69,10 @@ class UILaunchPage : public home_base void handle_home_key(lv_event_t *event); void handle_startup_gif_event(lv_event_t *event); - static void rotate_carousel_left(size_t start, size_t end); - static void rotate_carousel_right(size_t start, size_t end); - static void switchpanleEnable(int obj_index, int enable); - static void switchpanleEnableClick(int obj_index, int enable); + void rotate_carousel_left(size_t start, size_t end); + void rotate_carousel_right(size_t start, size_t end); + void switchpanleEnable(int obj_index, int enable); + void switchpanleEnableClick(int obj_index, int enable); static void on_left_arrow_clicked(lv_event_t *event); static void on_right_arrow_clicked(lv_event_t *event); static void on_app_clicked(lv_event_t *event); From 0cad0ed959df2155efb342c99b09d19139960b21 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:15:01 +0800 Subject: [PATCH 46/70] Align datetime event to minute boundary --- ext_components/cp0_lvgl/src/commount.c | 38 +++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index 0dac425e..c85b731d 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -3,18 +3,42 @@ #include "cp0_lvgl_app.h" #include "lvgl/lvgl.h" #include +#include uint32_t lv_c_event[(2*CP0_C_EVENT_END)] = {0}; +static uint32_t datetime_ms_to_next_minute(void) +{ + struct timeval now; + if (gettimeofday(&now, NULL) != 0) + return 60000; + + uint32_t second_ms = (uint32_t)(now.tv_sec % 60) * 1000U; + uint32_t usecond_ms = (uint32_t)(now.tv_usec / 1000U); + uint32_t elapsed_ms = second_ms + usecond_ms; + if (elapsed_ms >= 60000U) + return 1; + + uint32_t delay_ms = 60000U - elapsed_ms; + return delay_ms > 0 ? delay_ms : 1; +} + +static void datetime_schedule_next_minute(lv_timer_t *timer) +{ + lv_timer_set_period(timer, datetime_ms_to_next_minute()); + lv_timer_reset(timer); +} + static void datetime_timer_cb(lv_timer_t *timer) { - (void)timer; - if (lv_c_event[CP0_C_EVENT_DATATIME] == 0) - return; + if (lv_c_event[CP0_C_EVENT_DATATIME] != 0) + { + lv_obj_t *root = lv_display_get_screen_active(NULL); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_DATATIME], NULL); + } - lv_obj_t *root = lv_display_get_screen_active(NULL); - if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)lv_c_event[CP0_C_EVENT_DATATIME], NULL); + datetime_schedule_next_minute(timer); } static const char *getenv_default(const char *name, const char *dflt) @@ -55,6 +79,6 @@ void init_lvgl_event() { for (int i = 0; i < CP0_C_EVENT_END; i++) lv_c_event[i] = lv_event_register_id(); - lv_timer_create(datetime_timer_cb, 60000, NULL); + lv_timer_create(datetime_timer_cb, datetime_ms_to_next_minute(), NULL); init_lvgl_event_cpp(); } From cf3df441031a4e3ed33eabf201a8cdf03a79774d Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:26:45 +0800 Subject: [PATCH 47/70] Refactor launcher carousel data flow --- projects/APPLaunch/main/ui/Launch.cpp | 184 ++++++-------------- projects/APPLaunch/main/ui/Launch.h | 11 +- projects/APPLaunch/main/ui/UILaunchPage.cpp | 74 +++++++- projects/APPLaunch/main/ui/UILaunchPage.h | 8 +- 4 files changed, 135 insertions(+), 142 deletions(-) diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 77f78325..40d9ee92 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -14,48 +14,18 @@ #include "cp0_lvgl_file.hpp" #include "sample_log.h" -#include #include -#include #include #include -#include #include -#include #include #include #include -#include -#include #include -#define PANEL_BORDER_CENTER 0x444444 -#define PANEL_BORDER_SIDE 0x222222 -#define PANEL_PAD_CENTER 0 -#define PANEL_PAD_SIDE 0 - - -static void panel_set_icon(lv_obj_t *panel, const char *src) -{ - const char *icon_src = src ? src : ""; - if (icon_src[0] == '\0') { - SLOGW("[LAUNCHER] set panel icon with empty path"); - } else if (access(icon_src, R_OK) == 0) { - SLOGI("[LAUNCHER] set panel icon: %s", icon_src); - } else { - SLOGW("[LAUNCHER] set panel icon missing/unreadable: %s", icon_src); - } - - lv_obj_set_style_pad_all(panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *img = lv_obj_get_child(panel, 0); - if (!img || !lv_obj_check_type(img, &lv_image_class)) { - img = lv_image_create(panel); - lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); - lv_obj_set_align(img, LV_ALIGN_CENTER); - lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); - } - lv_image_set_src(img, icon_src); +namespace { +constexpr size_t kHomeCarouselSlotCount = 5; +constexpr int kHomeCarouselCenterSlot = 2; } // ============================================================ @@ -76,6 +46,12 @@ Icon=share/images/e-Mail_80.png // ============================================================ void Launch::bind_ui() { + if (bound_) { + refresh_home_carousel(); + return; + } + bound_ = true; + // Fixed icon; users cannot modify it app_list.emplace_back("Python", cp0_file_path("python_100.png"), "python3", true, false); @@ -90,35 +66,6 @@ void Launch::bind_ui() app_list.emplace_back("SETTING", cp0_file_path("setting_100.png"), page_v); - { - auto it = std::next(app_list.begin(), 0); - lv_label_set_text(launch_page_->label(0), it->Name.c_str()); - panel_set_icon(launch_page_->panel(0), it->Icon.c_str()); - } - - { - auto it = std::next(app_list.begin(), 1); - lv_label_set_text(launch_page_->label(1), it->Name.c_str()); - panel_set_icon(launch_page_->panel(1), it->Icon.c_str()); - } - - { - auto it = std::next(app_list.begin(), 2); - lv_label_set_text(launch_page_->label(2), it->Name.c_str()); - panel_set_icon(launch_page_->panel(2), it->Icon.c_str()); - } - - { - auto it = std::next(app_list.begin(), 3); - lv_label_set_text(launch_page_->label(3), it->Name.c_str()); - panel_set_icon(launch_page_->panel(3), it->Icon.c_str()); - } - - { - auto it = std::next(app_list.begin(), 4); - lv_label_set_text(launch_page_->label(4), it->Name.c_str()); - panel_set_icon(launch_page_->panel(4), it->Icon.c_str()); - } // Dynamic icons filtered by Settings configuration #define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) @@ -165,6 +112,7 @@ void Launch::bind_ui() fixed_count = app_list.size(); applications_load(); + refresh_home_carousel(); // Initialize inotify and watch the applications directory inotify_init_watch(); @@ -176,8 +124,9 @@ void Launch::bind_ui() void Launch::launch_app() { - auto it = std::next(app_list.begin(), current_app); - it->launch(this); + const app *selected = app_at_index(current_app); + if (selected) + selected->launch(this); } void Launch::lv_go_back_home(void *arg) @@ -251,28 +200,27 @@ void Launch::launch_Exec(const std::string &exec, bool keep_root) LVGL_RUN_FLAGE = 1; } -void Launch::update_left_slot(lv_obj_t *panel, lv_obj_t *label) +void Launch::select_next_app() { - current_app = current_app == (int)app_list.size() - 1 ? 0 : current_app + 1; - int next_app = current_app; - next_app = next_app == (int)app_list.size() - 1 ? 0 : next_app + 1; - next_app = next_app == (int)app_list.size() - 1 ? 0 : next_app + 1; - auto it = std::next(app_list.begin(), next_app); - lv_label_set_text(label, it->Name.c_str()); - panel_set_icon(panel, it->Icon.c_str()); + int next = normalized_app_index(current_app + 1); + if (next >= 0) + current_app = next; } -void Launch::update_right_slot(lv_obj_t *panel, lv_obj_t *label) +void Launch::select_previous_app() { - current_app = current_app == 0 ? (int)app_list.size() - 1 : current_app - 1; - int next_app = current_app; - next_app = next_app == 0 ? (int)app_list.size() - 1 : next_app - 1; - next_app = next_app == 0 ? (int)app_list.size() - 1 : next_app - 1; - auto it = std::next(app_list.begin(), next_app); - lv_label_set_text(label, it->Name.c_str()); - panel_set_icon(panel, it->Icon.c_str()); + int previous = normalized_app_index(current_app - 1); + if (previous >= 0) + current_app = previous; } +const app *Launch::carousel_slot_app(size_t slot) const +{ + if (slot >= kHomeCarouselSlotCount) + return nullptr; + return app_at_index(current_app + static_cast(slot) - kHomeCarouselCenterSlot); +} + void Launch::applications_load() { const std::string app_dir_path = cp0_file_path("applications"); @@ -373,7 +321,7 @@ void Launch::applications_load() continue; } bool in_list = false; - for (auto it : app_list) + for (const auto &it : app_list) { if (it.Exec == app_exec) { @@ -403,55 +351,16 @@ void Launch::inotify_init_watch() } // ============================================================ - // Refresh UI panels (update 5 slots from current_app) + // Refresh home carousel slots from current_app // ============================================================ -void Launch::refresh_ui_panels() +void Launch::refresh_home_carousel() { - int sz = (int)app_list.size(); - if (sz == 0) + int normalized = normalized_app_index(current_app); + if (normalized < 0) return; - - // Ensure current_app is in range - if (current_app >= sz) - current_app = sz - 1; - - auto app_at = [&](int idx) -> app & - { - idx = ((idx % sz) + sz) % sz; - return *std::next(app_list.begin(), idx); - }; - - // far left outside (hidden) - { - auto &a = app_at(current_app - 2); - lv_label_set_text(launch_page_->label(0), a.Name.c_str()); - panel_set_icon(launch_page_->panel(0), a.Icon.c_str()); - } - // left - { - auto &a = app_at(current_app - 1); - lv_label_set_text(launch_page_->label(1), a.Name.c_str()); - panel_set_icon(launch_page_->panel(1), a.Icon.c_str()); - } - // center - { - auto &a = app_at(current_app); - lv_label_set_text(launch_page_->label(2), a.Name.c_str()); - panel_set_icon(launch_page_->panel(2), a.Icon.c_str()); - } - // right - { - auto &a = app_at(current_app + 1); - lv_label_set_text(launch_page_->label(3), a.Name.c_str()); - panel_set_icon(launch_page_->panel(3), a.Icon.c_str()); - } - // far right outside (hidden) - { - auto &a = app_at(current_app + 2); - lv_label_set_text(launch_page_->label(4), a.Name.c_str()); - panel_set_icon(launch_page_->panel(4), a.Icon.c_str()); - } - + current_app = normalized; + if (launch_page_) + launch_page_->refresh_carousel(); } // ============================================================ @@ -466,9 +375,28 @@ void Launch::applications_reload() app_list.erase(it, app_list.end()); } applications_load(); - refresh_ui_panels(); + refresh_home_carousel(); } +int Launch::normalized_app_index(int index) const +{ + int size = static_cast(app_list.size()); + if (size == 0) + return -1; + + index %= size; + return index < 0 ? index + size : index; +} + +const app *Launch::app_at_index(int index) const +{ + int normalized = normalized_app_index(index); + if (normalized < 0) + return nullptr; + + return &*std::next(app_list.begin(), normalized); +} + // ============================================================ // LVGL timer callback: check inotify events and refresh the list on changes // ============================================================ diff --git a/projects/APPLaunch/main/ui/Launch.h b/projects/APPLaunch/main/ui/Launch.h index 1ce66e7b..6fff870c 100644 --- a/projects/APPLaunch/main/ui/Launch.h +++ b/projects/APPLaunch/main/ui/Launch.h @@ -49,8 +49,9 @@ class Launch void bind_ui(); void set_launch_page(std::shared_ptr launch_page); - void update_left_slot(lv_obj_t *panel, lv_obj_t *label); - void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void select_next_app(); + void select_previous_app(); + const app *carousel_slot_app(size_t slot) const; void launch_app(); private: @@ -61,8 +62,10 @@ class Launch void launch_Exec(const std::string &exec, bool keep_root = false); void applications_load(); void inotify_init_watch(); - void refresh_ui_panels(); + void refresh_home_carousel(); void applications_reload(); + int normalized_app_index(int index) const; + const app *app_at_index(int index) const; static void lv_go_back_home(void *arg); static void app_dir_watch_cb(lv_timer_t *timer); @@ -72,7 +75,7 @@ class Launch cp0_watcher_t dir_watcher = NULL; lv_timer_t *watch_timer = nullptr; int fixed_count = 0; + bool bound_ = false; std::list app_list; std::shared_ptr app_Page; - std::shared_ptr home_Page; }; diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 51c4b70e..80f13135 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -15,6 +15,7 @@ #include "Animation/ui_launcher_animation.h" #include +#include void UILaunchPage::rotate_carousel_left(size_t start, size_t end) { @@ -79,6 +80,32 @@ void UILaunchPage::switchpanleEnableClick(int obj_index, int enable) } } +void UILaunchPage::set_panel_icon(lv_obj_t *panel, const char *src) +{ + if (!panel) + return; + + const char *icon_src = src ? src : ""; + if (icon_src[0] == '\0') { + SLOGW("[LAUNCHER] set panel icon with empty path"); + } else if (access(icon_src, R_OK) == 0) { + SLOGI("[LAUNCHER] set panel icon: %s", icon_src); + } else { + SLOGW("[LAUNCHER] set panel icon missing/unreadable: %s", icon_src); + } + + lv_obj_set_style_pad_all(panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_obj_set_align(img, LV_ALIGN_CENTER); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, icon_src); +} + namespace { @@ -319,16 +346,47 @@ UILaunchPage::~UILaunchPage() active_launch_page = nullptr; } -void UILaunchPage::update_left_slot(lv_obj_t *panel, lv_obj_t *label) +void UILaunchPage::fill_right_entering_slot(lv_obj_t *panel, lv_obj_t *label) { - if (launch_) - launch_->update_left_slot(panel, label); + if (!launch_) + return; + + launch_->select_next_app(); + if (const app *item = launch_->carousel_slot_app(kCardFarRight)) + update_carousel_item(panel, label, item->Name.c_str(), item->Icon.c_str()); } -void UILaunchPage::update_right_slot(lv_obj_t *panel, lv_obj_t *label) +void UILaunchPage::fill_left_entering_slot(lv_obj_t *panel, lv_obj_t *label) { - if (launch_) - launch_->update_right_slot(panel, label); + if (!launch_) + return; + + launch_->select_previous_app(); + if (const app *item = launch_->carousel_slot_app(kCardFarLeft)) + update_carousel_item(panel, label, item->Name.c_str(), item->Icon.c_str()); +} + +void UILaunchPage::refresh_carousel() +{ + if (!launch_) + return; + + for (size_t slot = 0; slot < 5; ++slot) { + if (const app *item = launch_->carousel_slot_app(slot)) + update_carousel_slot(slot, item->Name.c_str(), item->Icon.c_str()); + } +} + +void UILaunchPage::update_carousel_slot(size_t slot, const char *title, const char *icon) +{ + update_carousel_item(panel(slot), label(slot), title, icon); +} + +void UILaunchPage::update_carousel_item(lv_obj_t *panel, lv_obj_t *label, const char *title, const char *icon) +{ + if (label) + lv_label_set_text(label, title ? title : ""); + set_panel_icon(panel, icon); } void UILaunchPage::launch_selected_app() @@ -400,7 +458,7 @@ void UILaunchPage::switch_right() snap_label_to_slot(carousel_elements_[9], 5); - update_right_slot(carousel_elements_[4], carousel_elements_[9]); + fill_left_entering_slot(carousel_elements_[4], carousel_elements_[9]); switchpanleEnableClick(2, 0); rotate_carousel_right(0, 4); @@ -435,7 +493,7 @@ void UILaunchPage::switch_left() snap_label_to_slot(carousel_elements_[5], 9); - update_left_slot(carousel_elements_[0], carousel_elements_[5]); + fill_right_entering_slot(carousel_elements_[0], carousel_elements_[5]); switchpanleEnableClick(2, 0); rotate_carousel_left(0, 4); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index 494abf9d..f881c14f 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -47,8 +47,9 @@ class UILaunchPage : public home_base lv_obj_t *panel(size_t slot); lv_obj_t *label(size_t slot); - void update_left_slot(lv_obj_t *panel, lv_obj_t *label); - void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void refresh_carousel(); + void update_carousel_slot(size_t slot, const char *title, const char *icon); + void update_carousel_item(lv_obj_t *panel, lv_obj_t *label, const char *title, const char *icon); void launch_selected_app(); protected: @@ -64,6 +65,8 @@ class UILaunchPage : public home_base void create_app_container(lv_obj_t *parent); void switch_left(); void switch_right(); + void fill_left_entering_slot(lv_obj_t *panel, lv_obj_t *label); + void fill_right_entering_slot(lv_obj_t *panel, lv_obj_t *label); void finish_switch_animation(); void run_pending_switch(); void handle_home_key(lv_event_t *event); @@ -73,6 +76,7 @@ class UILaunchPage : public home_base void rotate_carousel_right(size_t start, size_t end); void switchpanleEnable(int obj_index, int enable); void switchpanleEnableClick(int obj_index, int enable); + static void set_panel_icon(lv_obj_t *panel, const char *src); static void on_left_arrow_clicked(lv_event_t *event); static void on_right_arrow_clicked(lv_event_t *event); static void on_app_clicked(lv_event_t *event); From c5c2c15faeb903351aa944667259b674656ecb15 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:44:44 +0800 Subject: [PATCH 48/70] Use Kconfig choice for APPLaunch build target --- projects/APPLaunch/config_defaults.mk | 1 + projects/APPLaunch/darwin_config_defaults.mk | 1 + .../linux_x86_cross_cp0_config_defaults.mk | 1 + .../linux_x86_sdl2_config_defaults.mk | 1 + .../mac_cross_cp0_config_defaults.mk | 1 + projects/APPLaunch/main/Kconfig | 24 +++++++++++++++++++ projects/APPLaunch/main/SConstruct | 10 ++++---- 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/projects/APPLaunch/config_defaults.mk b/projects/APPLaunch/config_defaults.mk index 311de281..e37c2f65 100644 --- a/projects/APPLaunch/config_defaults.mk +++ b/projects/APPLaunch/config_defaults.mk @@ -93,3 +93,4 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_APPLAUNCH_LINUX_CP0=y diff --git a/projects/APPLaunch/darwin_config_defaults.mk b/projects/APPLaunch/darwin_config_defaults.mk index 05bb85bb..677bdef4 100644 --- a/projects/APPLaunch/darwin_config_defaults.mk +++ b/projects/APPLaunch/darwin_config_defaults.mk @@ -94,3 +94,4 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_APPLAUNCH_DARWIN_SDL=y diff --git a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk index 09e47e01..3eb6b4c6 100644 --- a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk @@ -98,3 +98,4 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_APPLAUNCH_LINUX_X86_CROSS_CP0=y diff --git a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk index e0288e9c..a789806f 100644 --- a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk @@ -94,3 +94,4 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_APPLAUNCH_LINUX_X86_SDL2=y diff --git a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk index 3d8d1663..9de2f861 100644 --- a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk @@ -99,3 +99,4 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_APPLAUNCH_MAC_CROSS_CP0=y diff --git a/projects/APPLaunch/main/Kconfig b/projects/APPLaunch/main/Kconfig index e7e7458e..80fe33d8 100644 --- a/projects/APPLaunch/main/Kconfig +++ b/projects/APPLaunch/main/Kconfig @@ -2,3 +2,27 @@ # # SPDX-License-Identifier: MIT +menu "APPLaunch" + +choice APPLAUNCH_BUILD_TARGET + prompt "APPLaunch build target" + default APPLAUNCH_LINUX_X86_SDL2 + +config APPLAUNCH_LINUX_X86_SDL2 + bool "Linux x86 SDL2" + +config APPLAUNCH_LINUX_X86_CROSS_CP0 + bool "Linux x86 cross CP0" + +config APPLAUNCH_LINUX_CP0 + bool "Linux CP0" + +config APPLAUNCH_MAC_CROSS_CP0 + bool "macOS cross CP0" + +config APPLAUNCH_DARWIN_SDL + bool "Darwin SDL" + +endchoice + +endmenu diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 8b88d84e..67f15cfd 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -88,7 +88,7 @@ lvgl_component['SRCS'] = list(filter( )) # x86 -if 'linux_x86_sdl2_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): +if 'CONFIG_APPLAUNCH_LINUX_X86_SDL2' in os.environ: lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") lvgl_component['REQUIREMENTS'] += pkg_config_ldflags("freetype2") @@ -98,7 +98,7 @@ if 'linux_x86_sdl2_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", REQUIREMENTS += ['input', 'xkbcommon', 'udev'] # x86 cross -if 'linux_x86_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): +if 'CONFIG_APPLAUNCH_LINUX_X86_CROSS_CP0' in os.environ: lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] lvgl_component['INCLUDE'] += [f'{rootfs_path}/usr/include/freetype2', f'{rootfs_path}/usr/include/libpng16', @@ -111,11 +111,11 @@ if 'linux_x86_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FI LDFLAGS += [f'{rootfs_path}/usr/lib/aarch64-linux-gnu/libstdc++.so.6'] DEFINITIONS += ['-Wno-format-truncation'] -if os.environ.get("CONFIG_DEFAULT_FILE", '') == 'config_defaults.mk': +if 'CONFIG_APPLAUNCH_LINUX_CP0' in os.environ: REQUIREMENTS += ['cp0_lvgl'] REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'freetype'] -if 'mac_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): +if 'CONFIG_APPLAUNCH_MAC_CROSS_CP0' in os.environ: lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] lvgl_component['INCLUDE'] += [f'{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/include/freetype2', f'{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/include/libpng16'] @@ -136,7 +136,7 @@ if 'mac_cross_cp0_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ' LDFLAGS += [f'{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/lib/aarch64-linux-gnu/libstdc++.so.6',f'-Wl,-rpath-link,{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/lib/aarch64-linux-gnu',f'-B{os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")}/usr/lib/aarch64-linux-gnu'] DEFINITIONS += ['-Wno-format-truncation'] # macOS -if 'darwin_config_defaults.mk' in os.environ.get("CONFIG_DEFAULT_FILE", ''): +if 'CONFIG_APPLAUNCH_DARWIN_SDL' in os.environ: # macOS Use the SDL backend REQUIREMENTS += ['cp0_lvgl'] From d0a8ea0b39a4a80d605798ddf1659105427f1b77 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:45:59 +0800 Subject: [PATCH 49/70] Move RadioLib into external component --- .../08-build-and-compilation-guide.md | 8 +-- ...26\350\257\221\346\214\207\345\215\227.md" | 8 +-- ext_components/RadioLib/Kconfig | 6 +++ ext_components/RadioLib/SConstruct | 49 +++++++++++++++++++ projects/APPLaunch/config_defaults.mk | 1 + projects/APPLaunch/darwin_config_defaults.mk | 1 + .../linux_x86_cross_cp0_config_defaults.mk | 1 + .../linux_x86_sdl2_config_defaults.mk | 1 + .../mac_cross_cp0_config_defaults.mk | 1 + projects/APPLaunch/main/SConstruct | 18 +------ 10 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 ext_components/RadioLib/Kconfig create mode 100644 ext_components/RadioLib/SConstruct diff --git a/docs/launcher-project-guide/08-build-and-compilation-guide.md b/docs/launcher-project-guide/08-build-and-compilation-guide.md index 0cf3b273..9d5fd2f7 100644 --- a/docs/launcher-project-guide/08-build-and-compilation-guide.md +++ b/docs/launcher-project-guide/08-build-and-compilation-guide.md @@ -279,7 +279,7 @@ Meaning: | Variable | Default value | Purpose | | --- | --- | --- | | `SDK_PATH` | `SDK` under the repository root | Lets the SDK build system find Kconfig, SCons tools, and built-in components | -| `EXT_COMPONENTS_PATH` | `ext_components` under the repository root | Lets the build system load extension components such as `cp0_lvgl`, `Miniaudio`, and `Sigslot` | +| `EXT_COMPONENTS_PATH` | `ext_components` under the repository root | Lets the build system load extension components such as `cp0_lvgl`, `Miniaudio`, `Sigslot`, and `RadioLib` | Usually do not override these variables manually unless you are actually testing an external SDK or component directory. @@ -634,10 +634,10 @@ This file registers the APPLaunch main-program component: - Reads the current short git hash and injects compile macro `LAUNCHER_GIT_COMMIT_RAW`. - Collects `src/*.c*` and all source files under the `ui` directory. - Adds includes: `main`, `main/include`, `ext_components/cp0_lvgl/include`, and `SDK/components/utilities/include`. -- Depends on components: `cp0_lvgl`, `eventpp`, `lvgl_component`, `pthread`, and `Miniaudio`. +- Depends on components: `cp0_lvgl`, `eventpp`, `lvgl_component`, `pthread`, `Miniaudio`, and `RadioLib`. - Optional dependency: `Backward_cpp`. - Adds SDL2, FreeType, libinput, xkbcommon, udev, libcamera, jpeg, and other dependencies according to different configuration files. -- Pulls RadioLib through `wget_github('https://github.com/jgromes/RadioLib.git')` and directly compiles SX1262-related source files. +- Uses `ext_components/RadioLib` as a static component; the RadioLib component owns the `wget_github('https://github.com/jgromes/RadioLib.git')` source cache and SX1262-related source list. - Adds the `../APPLaunch` resource tree and `doc/store_cache_sync.py` to `STATIC_FILES`. - Registers project target: `M5CardputerZero-APPLaunch`. @@ -865,7 +865,7 @@ For manual deployment, make sure you copied the contents of `dist/APPLaunch`, no ### 13.12 RadioLib Download Failure -`main/SConstruct` uses `wget_github('https://github.com/jgromes/RadioLib.git')` to fetch RadioLib. The first build may need network access. +`ext_components/RadioLib/SConstruct` uses `wget_github('https://github.com/jgromes/RadioLib.git')` to fetch RadioLib when `CONFIG_RADIOLIB_COMPONENT_ENABLED=y`. The first build may need network access. Fix: diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" index c33d1a3f..22943358 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" @@ -279,7 +279,7 @@ os.environ["EXT_COMPONENTS_PATH"] = str(sdk_path.parent / "ext_components") | 变量 | 默认值 | 作用 | | --- | --- | --- | | `SDK_PATH` | 仓库根目录下的 `SDK` | 让 SDK 构建系统找到 Kconfig、SCons 工具和内置组件 | -| `EXT_COMPONENTS_PATH` | 仓库根目录下的 `ext_components` | 让构建系统加载 `cp0_lvgl`、`Miniaudio`、`Sigslot` 等扩展组件 | +| `EXT_COMPONENTS_PATH` | 仓库根目录下的 `ext_components` | 让构建系统加载 `cp0_lvgl`、`Miniaudio`、`Sigslot`、`RadioLib` 等扩展组件 | 通常不要手工覆盖这两个变量,除非你确实在测试外部 SDK 或组件目录。 @@ -634,10 +634,10 @@ SDK 构建系统做以下事情: - 读取当前 git 短 hash,注入编译宏 `LAUNCHER_GIT_COMMIT_RAW`。 - 收集 `src/*.c*` 和整个 `ui` 目录源码。 - 添加 include:`main`、`main/include`、`ext_components/cp0_lvgl/include`、`SDK/components/utilities/include`。 -- 依赖组件:`cp0_lvgl`、`eventpp`、`lvgl_component`、`pthread`、`Miniaudio`。 +- 依赖组件:`cp0_lvgl`、`eventpp`、`lvgl_component`、`pthread`、`Miniaudio`、`RadioLib`。 - 可选依赖:`Backward_cpp`。 - 按不同配置文件追加 SDL2、FreeType、libinput、xkbcommon、udev、libcamera、jpeg 等依赖。 -- 通过 `wget_github('https://github.com/jgromes/RadioLib.git')` 拉取 RadioLib,并直接编译 SX1262 相关源码。 +- 使用 `ext_components/RadioLib` 静态组件;RadioLib 组件负责 `wget_github('https://github.com/jgromes/RadioLib.git')` 源码缓存和 SX1262 相关源码列表。 - 把 `../APPLaunch` 资源树和 `doc/store_cache_sync.py` 加入 `STATIC_FILES`。 - 注册项目 target:`M5CardputerZero-APPLaunch`。 @@ -865,7 +865,7 @@ ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch ### 13.12 RadioLib 下载失败 -`main/SConstruct` 使用 `wget_github('https://github.com/jgromes/RadioLib.git')` 获取 RadioLib。首次构建可能需要网络。 +`ext_components/RadioLib/SConstruct` 在 `CONFIG_RADIOLIB_COMPONENT_ENABLED=y` 时使用 `wget_github('https://github.com/jgromes/RadioLib.git')` 获取 RadioLib。首次构建可能需要网络。 处理: diff --git a/ext_components/RadioLib/Kconfig b/ext_components/RadioLib/Kconfig new file mode 100644 index 00000000..3fe6d5f1 --- /dev/null +++ b/ext_components/RadioLib/Kconfig @@ -0,0 +1,6 @@ +menuconfig RADIOLIB_COMPONENT_ENABLED + bool "Enable RadioLib" + default n + help + Enable the RadioLib external component and expose RadioLib headers and + the SX126x/SX1262 implementation used by LoRa applications. diff --git a/ext_components/RadioLib/SConstruct b/ext_components/RadioLib/SConstruct new file mode 100644 index 00000000..8565ae60 --- /dev/null +++ b/ext_components/RadioLib/SConstruct @@ -0,0 +1,49 @@ +# component/SConstruct +Import("env") +import os + +with open(env["PROJECT_TOOL_S"]) as f: + exec(f.read()) + +if "CONFIG_RADIOLIB_COMPONENT_ENABLED" in os.environ: + radiolib = wget_github("https://github.com/jgromes/RadioLib.git") + + SRCS = [ + os.path.join(radiolib, "src", "Hal.cpp"), + os.path.join(radiolib, "src", "Module.cpp"), + os.path.join(radiolib, "src", "utils", "CRC.cpp"), + os.path.join(radiolib, "src", "utils", "ConvCode.cpp"), + os.path.join(radiolib, "src", "utils", "Utils.cpp"), + os.path.join(radiolib, "src", "protocols", "PhysicalLayer", "PhysicalLayer.cpp"), + os.path.join(radiolib, "src", "modules", "SX126x", "SX126x.cpp"), + os.path.join(radiolib, "src", "modules", "SX126x", "SX126x_commands.cpp"), + os.path.join(radiolib, "src", "modules", "SX126x", "SX126x_config.cpp"), + os.path.join(radiolib, "src", "modules", "SX126x", "SX126x_LR_FHSS.cpp"), + os.path.join(radiolib, "src", "modules", "SX126x", "SX1262.cpp"), + ] + INCLUDE = [os.path.join(radiolib, "src")] + PRIVATE_INCLUDE = [] + REQUIREMENTS = [] + STATIC_LIB = [] + DYNAMIC_LIB = [] + DEFINITIONS = [] + DEFINITIONS_PRIVATE = [] + LDFLAGS = [] + LINK_SEARCH_PATH = [] + + env["COMPONENTS"].append( + { + "target": os.path.basename(env["component_dir"]), + "SRCS": SRCS, + "INCLUDE": INCLUDE, + "PRIVATE_INCLUDE": PRIVATE_INCLUDE, + "REQUIREMENTS": REQUIREMENTS, + "STATIC_LIB": STATIC_LIB, + "DYNAMIC_LIB": DYNAMIC_LIB, + "DEFINITIONS": DEFINITIONS, + "DEFINITIONS_PRIVATE": DEFINITIONS_PRIVATE, + "LDFLAGS": LDFLAGS, + "LINK_SEARCH_PATH": LINK_SEARCH_PATH, + "REGISTER": "static", + } + ) diff --git a/projects/APPLaunch/config_defaults.mk b/projects/APPLaunch/config_defaults.mk index e37c2f65..031d0d53 100644 --- a/projects/APPLaunch/config_defaults.mk +++ b/projects/APPLaunch/config_defaults.mk @@ -93,4 +93,5 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_LINUX_CP0=y diff --git a/projects/APPLaunch/darwin_config_defaults.mk b/projects/APPLaunch/darwin_config_defaults.mk index 677bdef4..cc1caa22 100644 --- a/projects/APPLaunch/darwin_config_defaults.mk +++ b/projects/APPLaunch/darwin_config_defaults.mk @@ -94,4 +94,5 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_DARWIN_SDL=y diff --git a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk index 3eb6b4c6..a7d1be6d 100644 --- a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk @@ -98,4 +98,5 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_LINUX_X86_CROSS_CP0=y diff --git a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk index a789806f..74d9db65 100644 --- a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk @@ -94,4 +94,5 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_LINUX_X86_SDL2=y diff --git a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk index 9de2f861..ff9b76dd 100644 --- a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk @@ -99,4 +99,5 @@ CONFIG_MINIAUDIO_COMPONENT_ENABLED=y CONFIG_SIGSLOT_COMPONENT_ENABLED=y CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_MAC_CROSS_CP0=y diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 67f15cfd..91d5eaa0 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -156,22 +156,8 @@ if 'CONFIG_APPLAUNCH_DARWIN_SDL' in os.environ: # REQUIREMENTS += pkg_config_ldflags("freetype2") -# add RadioLib -RadioLib = wget_github('https://github.com/jgromes/RadioLib.git') -SRCS += [ - os.path.join(RadioLib, 'src', 'Hal.cpp'), - os.path.join(RadioLib, 'src', 'Module.cpp'), - os.path.join(RadioLib, 'src', 'utils', 'CRC.cpp'), - os.path.join(RadioLib, 'src', 'utils', 'ConvCode.cpp'), - os.path.join(RadioLib, 'src', 'utils', 'Utils.cpp'), - os.path.join(RadioLib, 'src', 'protocols', 'PhysicalLayer', 'PhysicalLayer.cpp'), - os.path.join(RadioLib, 'src', 'modules', 'SX126x', 'SX126x.cpp'), - os.path.join(RadioLib, 'src', 'modules', 'SX126x', 'SX126x_commands.cpp'), - os.path.join(RadioLib, 'src', 'modules', 'SX126x', 'SX126x_config.cpp'), - os.path.join(RadioLib, 'src', 'modules', 'SX126x', 'SX126x_LR_FHSS.cpp'), - os.path.join(RadioLib, 'src', 'modules', 'SX126x', 'SX1262.cpp'), -] -INCLUDE += [os.path.join(RadioLib, 'src')] +# RadioLib is provided by ext_components/RadioLib. +REQUIREMENTS += ['RadioLib'] # add static files From 1286a7b20d1e37cfc7124eecff42a9b57691da67 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:51:35 +0800 Subject: [PATCH 50/70] Relocate legacy doc utilities --- README.md | 5 +++-- README_ZH.md | 5 +++-- ...6\211\223\345\214\205\346\214\207\345\215\227.md" | 0 .../01-project-layout-and-module-responsibilities.md | 12 ++++++++---- .../08-build-and-compilation-guide.md | 9 +++++---- ...6\250\241\345\235\227\350\201\214\350\264\243.md" | 12 ++++++++---- ...7\274\226\350\257\221\346\214\207\345\215\227.md" | 9 +++++---- .../APPLaunch/APPLaunch/bin}/store_cache_sync.py | 0 projects/APPLaunch/main/SConstruct | 1 - {doc => scripts}/firmware_manager.py | 1 - 10 files changed, 32 insertions(+), 22 deletions(-) rename "doc/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" => "docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" (100%) rename {doc => projects/APPLaunch/APPLaunch/bin}/store_cache_sync.py (100%) mode change 100644 => 100755 rename {doc => scripts}/firmware_manager.py (99%) mode change 100644 => 100755 diff --git a/README.md b/README.md index 8b92f2a0..7e1ab7bf 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ launcher/ │ ├── Calculator/ # Calculator │ ├── AppStore/ # App store │ └── HelloWorld/ # Hello World example -├── ext_components/ # External components (Miniaudio, etc.) -├── doc/ # Documentation resources +├── ext_components/ # External components (Miniaudio, RadioLib, etc.) +├── docs/ # Project documentation +├── scripts/ # Repository helper tools ├── README.md └── README_ZH.md ``` diff --git a/README_ZH.md b/README_ZH.md index ea92fc2c..3c8f0cb6 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -46,8 +46,9 @@ launcher/ │ ├── Calculator/ # 计算器 │ ├── AppStore/ # 应用商店 │ └── HelloWorld/ # Hello World 示例 -├── ext_components/ # 外部组件(Miniaudio 等) -├── doc/ # 文档资源 +├── ext_components/ # 外部组件(Miniaudio、RadioLib 等) +├── docs/ # 工程文档 +├── scripts/ # 仓库辅助工具 ├── README.md └── README_ZH.md ``` diff --git "a/doc/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" "b/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" similarity index 100% rename from "doc/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" rename to "docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" diff --git a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md index 1a0e191f..8851e1e3 100644 --- a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md +++ b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md @@ -9,7 +9,6 @@ launcher/ ├── SDK/ ├── ext_components/ ├── projects/ -├── doc/ ├── docs/ ├── README.md └── README_ZH.md @@ -48,6 +47,7 @@ env = SConscript( ext_components/ ├── cp0_lvgl/ ├── Miniaudio/ +├── RadioLib/ └── Sigslot/ ``` @@ -56,6 +56,7 @@ ext_components/ | `cp0_lvgl` | CardputerZero platform adaptation; wraps LVGL initialization, file paths, input, processes, PTY, and system capabilities | | `Miniaudio` | Dependency for audio playback and recording | | `Sigslot` | Signal-slot mechanism | +| `RadioLib` | LoRa/SX126x wireless communication library component | ### 1.3 `projects/` @@ -78,10 +79,11 @@ projects/ | `HelloWorld` | Minimal example project for learning the build flow | | `UserDemo` | User demo project | -### 1.4 `doc/` and `docs/` +### 1.4 `docs/`, `scripts/`, and Runtime Helpers -- `doc/`: historical documentation, packaging guides, and helper scripts, such as `APPLaunch-App-打包指南.md` and `store_cache_sync.py`. -- `docs/`: developer-facing documentation. This documentation set is placed here. +- `docs/`: developer-facing documentation and standalone packaging docs, including `APPLaunch-App-打包指南.md`. +- `scripts/`: repository-level helper tools, such as `firmware_manager.py` and `debian_packager.py`. +- `projects/APPLaunch/APPLaunch/bin/`: APPLaunch runtime helper scripts copied into `/usr/share/APPLaunch/bin/`, including `store_cache_sync.py`. ## 2. APPLaunch Top-Level Structure @@ -117,6 +119,8 @@ projects/APPLaunch/ projects/APPLaunch/APPLaunch/ ├── applications/ │ └── vim.desktop.temple +├── bin/ +│ └── store_cache_sync.py ├── lib/ │ └── nihao.so └── share/ diff --git a/docs/launcher-project-guide/08-build-and-compilation-guide.md b/docs/launcher-project-guide/08-build-and-compilation-guide.md index 9d5fd2f7..cf962699 100644 --- a/docs/launcher-project-guide/08-build-and-compilation-guide.md +++ b/docs/launcher-project-guide/08-build-and-compilation-guide.md @@ -25,15 +25,16 @@ Build artifacts usually appear in: ```text projects/APPLaunch/dist/ ├── M5CardputerZero-APPLaunch -├── APPLaunch/ -└── store_cache_sync.py +└── APPLaunch/ + └── bin/ + └── store_cache_sync.py ``` Where: - `M5CardputerZero-APPLaunch` is the main executable. - `APPLaunch/` is the runtime resource tree and is copied to `dist/APPLaunch`. -- `store_cache_sync.py` comes from repository file `doc/store_cache_sync.py` and is copied together with `STATIC_FILES`. +- `store_cache_sync.py` lives in `projects/APPLaunch/APPLaunch/bin/store_cache_sync.py` and is copied as part of the runtime resource tree. ## 2. Prerequisites @@ -638,7 +639,7 @@ This file registers the APPLaunch main-program component: - Optional dependency: `Backward_cpp`. - Adds SDL2, FreeType, libinput, xkbcommon, udev, libcamera, jpeg, and other dependencies according to different configuration files. - Uses `ext_components/RadioLib` as a static component; the RadioLib component owns the `wget_github('https://github.com/jgromes/RadioLib.git')` source cache and SX1262-related source list. -- Adds the `../APPLaunch` resource tree and `doc/store_cache_sync.py` to `STATIC_FILES`. +- Adds the `../APPLaunch` runtime resource tree to `STATIC_FILES`; this tree includes `bin/store_cache_sync.py`. - Registers project target: `M5CardputerZero-APPLaunch`. ## 11. Common SCons Commands diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" index 4b251b90..3e7e48cd 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" @@ -9,7 +9,6 @@ launcher/ ├── SDK/ ├── ext_components/ ├── projects/ -├── doc/ ├── docs/ ├── README.md └── README_ZH.md @@ -48,6 +47,7 @@ env = SConscript( ext_components/ ├── cp0_lvgl/ ├── Miniaudio/ +├── RadioLib/ └── Sigslot/ ``` @@ -56,6 +56,7 @@ ext_components/ | `cp0_lvgl` | CardputerZero 平台适配,封装 LVGL 初始化、文件路径、输入、进程、PTY、系统能力 | | `Miniaudio` | 音频播放/录音相关依赖 | | `Sigslot` | 信号槽机制 | +| `RadioLib` | LoRa/SX126x 无线通信库组件 | ### 1.3 `projects/` @@ -78,10 +79,11 @@ projects/ | `HelloWorld` | 最小示例工程,用于学习构建流程 | | `UserDemo` | 用户演示工程 | -### 1.4 `doc/` 和 `docs/` +### 1.4 `docs/`、`scripts/` 和运行时辅助脚本 -- `doc/`:历史文档、打包指南、辅助脚本,例如 `APPLaunch-App-打包指南.md`、`store_cache_sync.py`。 -- `docs/`:面向开发者的说明文档,本组文档放在这里。 +- `docs/`:面向开发者的说明文档和独立打包文档,例如 `APPLaunch-App-打包指南.md`。 +- `scripts/`:仓库级辅助工具,例如 `firmware_manager.py`、`debian_packager.py`。 +- `projects/APPLaunch/APPLaunch/bin/`:APPLaunch 运行时辅助脚本,会复制到 `/usr/share/APPLaunch/bin/`,例如 `store_cache_sync.py`。 ## 2. APPLaunch 顶层结构 @@ -117,6 +119,8 @@ projects/APPLaunch/ projects/APPLaunch/APPLaunch/ ├── applications/ │ └── vim.desktop.temple +├── bin/ +│ └── store_cache_sync.py ├── lib/ │ └── nihao.so └── share/ diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" index 22943358..1b5c7378 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" @@ -25,15 +25,16 @@ APPLaunch 可以编译成多种形态。核心差异由 `CONFIG_DEFAULT_FILE` ```text projects/APPLaunch/dist/ ├── M5CardputerZero-APPLaunch -├── APPLaunch/ -└── store_cache_sync.py +└── APPLaunch/ + └── bin/ + └── store_cache_sync.py ``` 其中: - `M5CardputerZero-APPLaunch` 是主可执行文件。 - `APPLaunch/` 是运行时资源树,会被复制到 `dist/APPLaunch`。 -- `store_cache_sync.py` 来自仓库 `doc/store_cache_sync.py`,由 `STATIC_FILES` 一起复制。 +- `store_cache_sync.py` 位于 `projects/APPLaunch/APPLaunch/bin/store_cache_sync.py`,随运行时资源树一起复制。 ## 2. 前置条件 @@ -638,7 +639,7 @@ SDK 构建系统做以下事情: - 可选依赖:`Backward_cpp`。 - 按不同配置文件追加 SDL2、FreeType、libinput、xkbcommon、udev、libcamera、jpeg 等依赖。 - 使用 `ext_components/RadioLib` 静态组件;RadioLib 组件负责 `wget_github('https://github.com/jgromes/RadioLib.git')` 源码缓存和 SX1262 相关源码列表。 -- 把 `../APPLaunch` 资源树和 `doc/store_cache_sync.py` 加入 `STATIC_FILES`。 +- 把 `../APPLaunch` 运行时资源树加入 `STATIC_FILES`;该资源树包含 `bin/store_cache_sync.py`。 - 注册项目 target:`M5CardputerZero-APPLaunch`。 ## 11. 常用 SCons 命令 diff --git a/doc/store_cache_sync.py b/projects/APPLaunch/APPLaunch/bin/store_cache_sync.py old mode 100644 new mode 100755 similarity index 100% rename from doc/store_cache_sync.py rename to projects/APPLaunch/APPLaunch/bin/store_cache_sync.py diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 91d5eaa0..1fbe8fd5 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -162,7 +162,6 @@ REQUIREMENTS += ['RadioLib'] # add static files STATIC_FILES += [ADir('../APPLaunch')] -STATIC_FILES += [os.path.join(env['component_dir'], '..', '..', '..', 'doc', 'store_cache_sync.py')] # register the component to the build system env['COMPONENTS'].append({'target':'M5CardputerZero-APPLaunch', 'SRCS':SRCS, diff --git a/doc/firmware_manager.py b/scripts/firmware_manager.py old mode 100644 new mode 100755 similarity index 99% rename from doc/firmware_manager.py rename to scripts/firmware_manager.py index bd0242e5..18233875 --- a/doc/firmware_manager.py +++ b/scripts/firmware_manager.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ FirmwareManagementV3 命令行客户端 -基于 doc/FirmwareManagementV3.md 使用方法: python3 firmware_manager.py [options] From c47d7c3927c2f085f2f4d99f4f5dcdba55f9ea91 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Fri, 12 Jun 2026 19:52:53 +0800 Subject: [PATCH 51/70] Remove firmware manager script --- scripts/firmware_manager.py | 350 ------------------------------------ 1 file changed, 350 deletions(-) delete mode 100755 scripts/firmware_manager.py diff --git a/scripts/firmware_manager.py b/scripts/firmware_manager.py deleted file mode 100755 index 18233875..00000000 --- a/scripts/firmware_manager.py +++ /dev/null @@ -1,350 +0,0 @@ -#!/usr/bin/env python3 -""" -FirmwareManagementV3 命令行客户端 - -使用方法: - python3 firmware_manager.py [options] - -示例: - # 登录(保存 token 到本地) - python3 firmware_manager.py login --email user@example.com --password mypass - - # 查看公开固件列表(免登录) - python3 firmware_manager.py public-list - - # 查看管理固件列表(需登录) - python3 firmware_manager.py list - python3 firmware_manager.py list --sku M5Stack-CardputerZero - - # 上传固件(需登录) - python3 firmware_manager.py upload --file firmware.bin --avatar logo.png \\ - --name "MyFirmware" --sku M5Stack-Zero --version v1.0.0 \\ - --description "Initial release" --class firmware - - # 查看固件详情(需登录) - python3 firmware_manager.py detail --id - - # 删除固件(需登录) - python3 firmware_manager.py delete --id -""" - -import argparse -import json -import os -import sys - -try: - import requests -except ImportError: - print("缺少依赖库 requests,请先执行: pip3 install requests") - sys.exit(1) - -# ──────────────────────── 常量 ──────────────────────────────────── -LOGIN_URL = "https://uiflow2.m5stack.com/m5stack/api/v2/login/doLogin" -BASE_URL = "https://ota.m5stack.com/ota/api/v3/firmware-management" -TOKEN_FILE = os.path.join(os.path.dirname(__file__), ".fw_manager_token") -TIMEOUT = 30 - - -# ──────────────────────── 工具函数 ──────────────────────────────── - -def _pretty(obj) -> str: - return json.dumps(obj, ensure_ascii=False, indent=2) - - -def _print_response(resp: requests.Response) -> dict: - """打印 HTTP 响应并返回解析后的 JSON 体。""" - print(f"HTTP {resp.status_code}") - try: - body = resp.json() - print(_pretty(body)) - return body - except ValueError: - print(resp.text) - return {} - - -def _load_token() -> str: - """从本地文件读取 token。""" - if os.path.exists(TOKEN_FILE): - with open(TOKEN_FILE, "r", encoding="utf-8") as f: - return f.read().strip() - return "" - - -def _save_token(token: str) -> None: - """将 token 保存到本地文件。""" - with open(TOKEN_FILE, "w", encoding="utf-8") as f: - f.write(token) - print(f"✓ Token 已保存到 {TOKEN_FILE}") - - -def _auth_headers(token: str = "") -> dict: - """构造鉴权请求头。""" - t = token or _load_token() - if not t: - print("⚠ 未找到 token,请先执行: python3 firmware_manager.py login") - sys.exit(1) - return {"token": t} - - -def _url(path: str) -> str: - return f"{BASE_URL}{path}" - - -# ──────────────────────── 接口封装 ──────────────────────────────── - -def cmd_login(args: argparse.Namespace) -> None: - """2.1 登录,获取并保存 token。""" - print(f"→ 登录: {args.email}") - resp = requests.post( - LOGIN_URL, - json={"email": args.email, "password": args.password}, - timeout=TIMEOUT, - ) - body = _print_response(resp) - if body.get("code") == 200: - token = body["data"]["token"] - _save_token(token) - print(f"✓ 登录成功,用户: {body['data'].get('userName', '')}") - else: - print(f"✗ 登录失败: {body.get('msg', '')}") - sys.exit(1) - - -def cmd_public_list(args: argparse.Namespace) -> None: - """5.3 公开固件列表(按 SKU 分组,免登录)。""" - print("→ 获取公开固件列表") - resp = requests.get(_url("/public/list"), timeout=TIMEOUT) - body = _print_response(resp) - - data = body.get("data") or [] - if isinstance(data, list) and data: - print(f"\n共 {len(data)} 个 SKU:") - for group in data: - sku = group.get("sku", "?") - items = group.get("items") or [] - print(f" [{sku}] {len(items)} 个版本") - for item in items: - ver = item.get("version", "?") - url = item.get("url", "") - remark = item.get("remark", "") - print(f" • {ver} {remark} {url}") - - -def cmd_list(args: argparse.Namespace) -> None: - """5.2 管理固件列表(需登录)。""" - params = {} - if args.sku: - params["sku"] = args.sku - print(f"→ 获取固件列表,SKU 过滤: {args.sku}") - else: - print("→ 获取固件列表(全部)") - - resp = requests.get( - _url("/list"), - params=params, - headers=_auth_headers(), - timeout=TIMEOUT, - ) - body = _print_response(resp) - - data = body.get("data") or [] - if isinstance(data, list) and data: - print(f"\n共 {len(data)} 条记录:") - for item in data: - print( - f" id={item.get('id')} sku={item.get('sku')} " - f"version={item.get('version')} name={item.get('firmwareName')} " - f"createTime={item.get('createTime')}" - ) - - -def cmd_upload(args: argparse.Namespace) -> None: - """5.1 上传固件(需登录)。""" - if not os.path.isfile(args.file): - print(f"✗ 固件文件不存在: {args.file}") - sys.exit(1) - if not os.path.isfile(args.avatar): - print(f"✗ 头像文件不存在: {args.avatar}") - sys.exit(1) - - print(f"→ 上传固件: {args.file}") - print(f" 头像: {args.avatar}") - print(f" 固件名: {args.name}") - print(f" SKU: {args.sku}") - print(f" 版本: {args.version}") - - data = { - "firmwareName": args.name, - "sku": args.sku, - "version": args.version, - "description": args.description, - } - if args.fw_class: - data["class"] = args.fw_class - if args.operator: - data["operator"] = args.operator - - with open(args.file, "rb") as fw, open(args.avatar, "rb") as av: - resp = requests.post( - _url("/upload"), - headers=_auth_headers(), - data=data, - files={ - "file": (os.path.basename(args.file), fw, "application/octet-stream"), - "avatar": (os.path.basename(args.avatar), av, "application/octet-stream"), - }, - timeout=TIMEOUT, - ) - - body = _print_response(resp) - if body.get("code") == 200: - fw_id = None - d = body.get("data") - if isinstance(d, dict): - fw_id = d.get("id") - elif d: - fw_id = d - if fw_id: - print(f"✓ 上传成功,固件 id: {fw_id}") - else: - print("✓ 上传成功") - else: - print(f"✗ 上传失败: {body.get('msg', '')}") - sys.exit(1) - - -def cmd_detail(args: argparse.Namespace) -> None: - """5.4 固件详情(需登录)。""" - print(f"→ 查询固件详情,id: {args.id}") - resp = requests.get( - _url("/detail"), - params={"id": args.id}, - headers=_auth_headers(), - timeout=TIMEOUT, - ) - body = _print_response(resp) - - if body.get("code") == 200: - d = body.get("data", {}) - print("\n── 固件详情 ──") - fields = [ - ("id", "ID"), - ("firmwareName", "固件名"), - ("sku", "SKU"), - ("version", "版本"), - ("class", "类名"), - ("description", "描述"), - ("fileUrl", "下载地址"), - ("avatarUrl", "头像地址"), - ("fileMd5", "MD5"), - ("fileSize", "文件大小(bytes)"), - ("operator", "操作人"), - ("createTime", "创建时间"), - ("updateTime", "更新时间"), - ] - for key, label in fields: - print(f" {label:<16}: {d.get(key, '')}") - else: - print(f"✗ 查询失败: {body.get('msg', '')}") - sys.exit(1) - - -def cmd_delete(args: argparse.Namespace) -> None: - """5.5 删除固件(需登录)。""" - print(f"→ 删除固件,id: {args.id}") - if not args.yes: - confirm = input("确认删除?(y/N): ").strip().lower() - if confirm not in ("y", "yes"): - print("已取消。") - return - - resp = requests.delete( - _url("/delete"), - params={"id": args.id}, - headers=_auth_headers(), - timeout=TIMEOUT, - ) - body = _print_response(resp) - if body.get("code") == 200: - print("✓ 删除成功") - else: - print(f"✗ 删除失败: {body.get('msg', '')}") - sys.exit(1) - - -def cmd_whoami(_args: argparse.Namespace) -> None: - """显示当前已保存的 token(前 30 字符)。""" - token = _load_token() - if token: - print(f"当前 Token: {token[:30]}... (来源: {TOKEN_FILE})") - else: - print("尚未登录,请执行: python3 firmware_manager.py login") - - -# ──────────────────────── CLI 入口 ──────────────────────────────── - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - prog="firmware_manager.py", - description="FirmwareManagementV3 命令行客户端", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__, - ) - sub = parser.add_subparsers(dest="command", metavar="") - sub.required = True - - # ── login ── - p_login = sub.add_parser("login", help="登录并保存 token") - p_login.add_argument("--email", required=True, help="登录邮箱") - p_login.add_argument("--password", required=True, help="登录密码") - p_login.set_defaults(func=cmd_login) - - # ── whoami ── - p_whoami = sub.add_parser("whoami", help="显示当前已登录账号的 token 信息") - p_whoami.set_defaults(func=cmd_whoami) - - # ── public-list ── - p_pub = sub.add_parser("public-list", help="公开固件列表(无需登录)") - p_pub.set_defaults(func=cmd_public_list) - - # ── list ── - p_list = sub.add_parser("list", help="管理固件列表(需登录)") - p_list.add_argument("--sku", default="", help="按 SKU 过滤(可选)") - p_list.set_defaults(func=cmd_list) - - # ── upload ── - p_upload = sub.add_parser("upload", help="上传固件(需登录)") - p_upload.add_argument("--file", required=True, help="固件文件路径") - p_upload.add_argument("--avatar", required=True, help="头像/封面图片路径") - p_upload.add_argument("--name", required=True, dest="name", help="固件名 (firmwareName)") - p_upload.add_argument("--sku", required=True, help="SKU") - p_upload.add_argument("--version", required=True, help="版本号") - p_upload.add_argument("--description", required=True, help="描述") - p_upload.add_argument("--class", default="", dest="fw_class", help="类名(可选)") - p_upload.add_argument("--operator", default="", help="操作人(可选)") - p_upload.set_defaults(func=cmd_upload) - - # ── detail ── - p_detail = sub.add_parser("detail", help="固件详情(需登录)") - p_detail.add_argument("--id", required=True, help="固件 ID") - p_detail.set_defaults(func=cmd_detail) - - # ── delete ── - p_delete = sub.add_parser("delete", help="删除固件(需登录)") - p_delete.add_argument("--id", required=True, help="固件 ID") - p_delete.add_argument("-y", "--yes", action="store_true", help="跳过确认提示") - p_delete.set_defaults(func=cmd_delete) - - return parser - - -def main() -> None: - parser = build_parser() - args = parser.parse_args() - args.func(args) - - -if __name__ == "__main__": - main() From 2c3e9f243b82d28032acd36c4725557e6f14ea91 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 10:27:50 +0800 Subject: [PATCH 52/70] Add Windows APPLaunch build configs --- SDK | 2 +- projects/APPLaunch/SConstruct | 20 +- projects/APPLaunch/config_defaults.mk | 1 + projects/APPLaunch/darwin_config_defaults.mk | 1 + .../linux_x86_cross_cp0_config_defaults.mk | 1 + .../linux_x86_sdl2_config_defaults.mk | 1 + .../mac_cross_cp0_config_defaults.mk | 1 + projects/APPLaunch/main/Kconfig | 12 + projects/APPLaunch/main/SConstruct | 71 +- projects/APPLaunch/main/ui/Launch.cpp | 4 - .../main/ui/page_app/ui_app_music.hpp | 1197 ----------------- .../win_x86_cross_config_defaults.mk | 110 ++ .../APPLaunch/win_x86_sdl2_config_defaults.mk | 104 ++ 13 files changed, 277 insertions(+), 1248 deletions(-) delete mode 100644 projects/APPLaunch/main/ui/page_app/ui_app_music.hpp create mode 100644 projects/APPLaunch/win_x86_cross_config_defaults.mk create mode 100644 projects/APPLaunch/win_x86_sdl2_config_defaults.mk diff --git a/SDK b/SDK index dc9db15b..1b124f48 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit dc9db15b4e65c2bd11542150756d2933ca42b152 +Subproject commit 1b124f486436f141a49aa74b8528be1b47ce0ca8 diff --git a/projects/APPLaunch/SConstruct b/projects/APPLaunch/SConstruct index 4b5fac75..427df316 100644 --- a/projects/APPLaunch/SConstruct +++ b/projects/APPLaunch/SConstruct @@ -16,8 +16,7 @@ cross_package_enabled = False version = "v0.0.4" local_path = Path(os.getcwd()) sdk_path = local_path.parent.parent / "SDK" -static_lib_path = os.path.join(sdk_path, "github_source", f"static_lib_{version}") - +static_lib_path = sdk_path / "github_source" / f"static_lib_{version}" if os.environ.get("CardputerZero", '') == 'y': os.environ["CONFIG_DEFAULT_FILE"] = "linux_x86_cross_cp0_config_defaults.mk" @@ -28,11 +27,14 @@ if os.environ.get("CONFIG_DEFAULT_FILE") == None: if "cross" in os.environ.get("CONFIG_DEFAULT_FILE", ''): cross_package_enabled = True - if not os.path.exists("build/config"): - os.makedirs("build/config") - with open("build/config/config_tmp.mk", "w") as f: - f.write(f'CONFIG_TOOLCHAIN_SYSROOT="{static_lib_path}"\n') - f.write(f'CONFIG_TOOLCHAIN_FLAGS="-I{static_lib_path}/usr/include/aarch64-linux-gnu"\n') + config_tmp_path = Path("build") / "config" / "config_tmp.mk" + sysroot_path_str = static_lib_path.as_posix().replace('"', r'\"') + config_tmp_content = ( + f'CONFIG_TOOLCHAIN_SYSROOT="{sysroot_path_str}"\n' + ) + if not config_tmp_path.exists() or config_tmp_path.read_text() != config_tmp_content: + config_tmp_path.parent.mkdir(parents=True, exist_ok=True) + config_tmp_path.write_text(config_tmp_content) os.environ["SDK_PATH"] = str(sdk_path) os.environ["EXT_COMPONENTS_PATH"] = str(sdk_path.parent / "ext_components") @@ -46,11 +48,11 @@ env = SConscript( # static_lib is only needed for cross-compilation, skip in SDL mode. if cross_package_enabled: update = False - if not os.path.exists(static_lib_path): + if not static_lib_path.exists(): update = True else: try: - with open(str(Path(static_lib_path) / "version"), "r") as f: + with open(str(static_lib_path / "version"), "r") as f: if version != f.read().strip(): update = True except Exception: diff --git a/projects/APPLaunch/config_defaults.mk b/projects/APPLaunch/config_defaults.mk index 031d0d53..155e0e68 100644 --- a/projects/APPLaunch/config_defaults.mk +++ b/projects/APPLaunch/config_defaults.mk @@ -95,3 +95,4 @@ CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_LINUX_CP0=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file diff --git a/projects/APPLaunch/darwin_config_defaults.mk b/projects/APPLaunch/darwin_config_defaults.mk index cc1caa22..6a1d84ba 100644 --- a/projects/APPLaunch/darwin_config_defaults.mk +++ b/projects/APPLaunch/darwin_config_defaults.mk @@ -96,3 +96,4 @@ CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_DARWIN_SDL=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file diff --git a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk index a7d1be6d..ba28b638 100644 --- a/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_cross_cp0_config_defaults.mk @@ -100,3 +100,4 @@ CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_LINUX_X86_CROSS_CP0=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file diff --git a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk index 74d9db65..37b33109 100644 --- a/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk +++ b/projects/APPLaunch/linux_x86_sdl2_config_defaults.mk @@ -96,3 +96,4 @@ CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_LINUX_X86_SDL2=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file diff --git a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk index ff9b76dd..516bd593 100644 --- a/projects/APPLaunch/mac_cross_cp0_config_defaults.mk +++ b/projects/APPLaunch/mac_cross_cp0_config_defaults.mk @@ -101,3 +101,4 @@ CONFIG_CP0_LVGL_COMPONENT_ENABLED=y CONFIG_EVENTPP_ENABLED=y CONFIG_RADIOLIB_COMPONENT_ENABLED=y CONFIG_APPLAUNCH_MAC_CROSS_CP0=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file diff --git a/projects/APPLaunch/main/Kconfig b/projects/APPLaunch/main/Kconfig index 80fe33d8..6e039506 100644 --- a/projects/APPLaunch/main/Kconfig +++ b/projects/APPLaunch/main/Kconfig @@ -4,6 +4,12 @@ menu "APPLaunch" +config APPLAUNCH_CROSS + bool "cross compile" + default y if APPLAUNCH_LINUX_X86_CROSS_CP0 + default y if APPLAUNCH_WIN_X86_CROSS_CP0 + default y if APPLAUNCH_MAC_CROSS_CP0 + choice APPLAUNCH_BUILD_TARGET prompt "APPLaunch build target" default APPLAUNCH_LINUX_X86_SDL2 @@ -14,6 +20,12 @@ config APPLAUNCH_LINUX_X86_SDL2 config APPLAUNCH_LINUX_X86_CROSS_CP0 bool "Linux x86 cross CP0" +config APPLAUNCH_WIN_X86_SDL2 + bool "Windows x86 SDL2" + +config APPLAUNCH_WIN_X86_CROSS_CP0 + bool "Windows x86 cross CP0" + config APPLAUNCH_LINUX_CP0 bool "Linux CP0" diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 1fbe8fd5..7dd533b6 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -8,18 +8,19 @@ import platform import subprocess import shutil import parse as _parse +from pathlib import Path # iomport env and tools from the SDK's scons project.py Import('env') with open(env['PROJECT_TOOL_S']) as f: exec(f.read()) -rootfs_path=os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "") +rootfs_path=Path(os.environ.get("CONFIG_TOOLCHAIN_SYSROOT", "")) # define the project's environment SRCS = [] INCLUDE = [] PRIVATE_INCLUDE = [] -REQUIREMENTS = ['cp0_lvgl', 'eventpp'] +REQUIREMENTS = ['cp0_lvgl', 'eventpp', 'utilities', 'lvgl_component', 'freetype', 'pthread', 'RadioLib'] # REQUIREMENTS = [] STATIC_LIB = [] DYNAMIC_LIB = [] @@ -30,14 +31,16 @@ LINK_SEARCH_PATH = [] STATIC_FILES = [] # run generate_page_app_includes.py to generate the header file for including app pages in the UI. -script_path = os.path.abspath(str(AFile('ui/generate_page_app_includes.py'))) -script_dir = os.path.dirname(script_path) -subprocess.run( - [sys.executable, script_path], - cwd=script_dir, - capture_output=True, - text=True -) +def run_python_script(script_rel_path): + script_path = script_rel_path.get_abspath() + script_dir = os.path.dirname(script_path) + subprocess.run( + [sys.executable, script_path], + cwd=script_dir, + capture_output=True, + text=True + ) +run_python_script(AFile('ui/generate_page_app_includes.py')) # Get git commit short hash for version display try: @@ -60,11 +63,7 @@ SRCS += Glob('src/*.c*') SRCS += append_srcs_dir(ADir('ui')) # add includes INCLUDE += [ADir('.'), ADir('include')] -INCLUDE += [os.path.join(os.environ['EXT_COMPONENTS_PATH'], 'cp0_lvgl', 'include')] -INCLUDE += [os.path.join(os.environ['SDK_PATH'], 'components', 'utilities', 'include')] -# add requirements -REQUIREMENTS += ['lvgl_component', 'pthread'] # Inject git commit hash as a compile-time macro DEFINITIONS += ['-DLAUNCHER_GIT_COMMIT_RAW=%s' % _git_commit] @@ -73,13 +72,12 @@ DEFINITIONS += ['-DLAUNCHER_GIT_COMMIT_RAW=%s' % _git_commit] if os.environ.get('APPLAUNCH_STARTUP_ANIMATION', '0') == '1': DEFINITIONS += ['-DAPPLAUNCH_STARTUP_ANIMATION'] -# add miniaudio -REQUIREMENTS += ['Miniaudio'] -# add +# add debug if 'CONFIG_BACKWARD_CPP_ENABLED' in os.environ: REQUIREMENTS += ['Backward_cpp'] + # custom lvgl component lvgl_component = list(filter(lambda x: x['target'] == 'lvgl_component', env['COMPONENTS']))[0] lvgl_component['SRCS'] = list(filter( @@ -87,33 +85,27 @@ lvgl_component['SRCS'] = list(filter( lvgl_component['SRCS'] )) + +if 'CONFIG_APPLAUNCH_CROSS' in os.environ: + lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] + lvgl_component['INCLUDE'] += [rootfs_path/"usr"/"include"/"freetype2", + rootfs_path/"usr"/"include"/"libpng16", + rootfs_path/"usr"/"include"/"libcamera", + ] + LDFLAGS += [rootfs_path/"usr"/"lib"/"aarch64-linux-gnu"/"libstdc++.so.6"] + + # x86 if 'CONFIG_APPLAUNCH_LINUX_X86_SDL2' in os.environ: lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") lvgl_component['REQUIREMENTS'] += pkg_config_ldflags("freetype2") - - REQUIREMENTS += ['cp0_lvgl'] DEFINITIONS += pkg_config_cflags("sdl2") REQUIREMENTS += pkg_config_ldflags("sdl2") - REQUIREMENTS += ['input', 'xkbcommon', 'udev'] - -# x86 cross -if 'CONFIG_APPLAUNCH_LINUX_X86_CROSS_CP0' in os.environ: - lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] - lvgl_component['INCLUDE'] += [f'{rootfs_path}/usr/include/freetype2', - f'{rootfs_path}/usr/include/libpng16', - f'{rootfs_path}/usr/include/libcamera', - ] - REQUIREMENTS += ['cp0_lvgl'] - REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'freetype'] - REQUIREMENTS += ['camera', 'camera-base', 'jpeg'] - LDFLAGS += [f'{rootfs_path}/usr/lib/aarch64-linux-gnu/libstdc++.so.6'] - DEFINITIONS += ['-Wno-format-truncation'] if 'CONFIG_APPLAUNCH_LINUX_CP0' in os.environ: REQUIREMENTS += ['cp0_lvgl'] - REQUIREMENTS += ['input', 'xkbcommon', 'udev', 'freetype'] + REQUIREMENTS += ['input', 'xkbcommon', 'udev'] if 'CONFIG_APPLAUNCH_MAC_CROSS_CP0' in os.environ: lvgl_component['DEFINITIONS'] += ['-D_REENTRANT'] @@ -155,10 +147,15 @@ if 'CONFIG_APPLAUNCH_DARWIN_SDL' in os.environ: # lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") # REQUIREMENTS += pkg_config_ldflags("freetype2") +if 'CONFIG_APPLAUNCH_WIN_X86_CROSS_CP0' in os.environ: + rootfs_lib_path = rootfs_path/"lib"/"aarch64-linux-gnu" + rootfs_usr_lib_path = rootfs_path/"usr"/"lib"/"aarch64-linux-gnu" -# RadioLib is provided by ext_components/RadioLib. -REQUIREMENTS += ['RadioLib'] - + LDFLAGS += [rootfs_usr_lib_path/'libstdc++.so.6', + f'-Wl,-rpath-link,{rootfs_lib_path}', + f'-Wl,-rpath-link,{rootfs_usr_lib_path}', + f'-B{rootfs_usr_lib_path}'] + DEFINITIONS += ['-Wno-format-truncation'] # add static files STATIC_FILES += [ADir('../APPLaunch')] diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index 40d9ee92..b288f351 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -70,10 +70,6 @@ void Launch::bind_ui() // Dynamic icons filtered by Settings configuration #define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - if (APP_ENABLED("Music")) - app_list.emplace_back("MUSIC", - cp0_file_path("music_100.png"), page_v); - if (APP_ENABLED("Math")) app_list.emplace_back("MATH", cp0_file_path("math_100.png"), diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_music.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_music.hpp deleted file mode 100644 index d2536589..00000000 --- a/projects/APPLaunch/main/ui/page_app/ui_app_music.hpp +++ /dev/null @@ -1,1197 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ - -#pragma once -#include "sample_log.h" - -#include "../ui_app_page.hpp" -#include "compat/input_keys.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "miniaudio.h" - -// ============================================================ -// Music player UIMusicPage -// Screen resolution: 320 x 170 top bar20px, ui_APP_Container 320x150 -// -// Features: -// 1. Select a folder -// 2. Scan mp3 / wav files in the current folder -// 3. Show the playlist -// 4. Play via miniaudio + PulseAudio -// -// View state: -// VIEW_MAIN — main playback screen -// VIEW_FOLDER_SEL — 'i'(Tab) directory browser popup -// VIEW_PLAYLIST — 'p' playlist popup -// -// Key mapping: -// LV_KEY_UP -> play / move up in the list -// LV_KEY_DOWN -> pause / move down in the list -// LV_KEY_LEFT -> previous track / go to parent directory -// LV_KEY_RIGHT -> next track / enter subdirectory -// 'i'(Tab) -> open directory browser -// 'p' -> open playlist -// LV_KEY_ENTER -> directory browser: load audio from current directory / playlist: play selected track -// LV_KEY_ESC -> return to main screen / exit app -// ============================================================ - -class UIMusicPage : public AppPage -{ - enum class PlayState - { - STOPPED, - PLAYING, - PAUSED - }; - - enum class ViewState - { - MAIN, - FOLDER_SEL, - PLAYLIST - }; - -public: - UIMusicPage() : AppPage() - { - set_page_title("MUSIC"); - - creat_UI(); - init_audio(); - event_handler_init(); - update_main_ui(); - } - - ~UIMusicPage() - { - if (audio_timer_) - { - lv_timer_del(audio_timer_); - audio_timer_ = nullptr; - } - - stop_playback(); - uninit_audio(); - } - - // ==================== Public API ==================== - - void prev_track() - { - if (playlist_.empty()) - return; - - bool was_playing = play_state_ == PlayState::PLAYING; - - if (current_track_ > 0) - --current_track_; - else - current_track_ = static_cast(playlist_.size()) - 1; - - if (was_playing) - { - if (start_playback()) - play_state_ = PlayState::PLAYING; - else - play_state_ = PlayState::STOPPED; - } - - update_main_ui(); - } - - void next_track() - { - if (playlist_.empty()) - return; - - bool was_playing = play_state_ == PlayState::PLAYING; - - current_track_ = (current_track_ + 1) % static_cast(playlist_.size()); - - if (was_playing) - { - if (start_playback()) - play_state_ = PlayState::PLAYING; - else - play_state_ = PlayState::STOPPED; - } - - update_main_ui(); - } - - void play() - { - if (playlist_.empty()) - return; - - if (!audio_ready_) - { - SLOGI("[Music] Cannot play: PulseAudio/miniaudio not ready"); - play_state_ = PlayState::STOPPED; - update_main_ui(); - return; - } - - // If currently paused, resume playback - if (play_state_ == PlayState::PAUSED && - sound_loaded_ && - loaded_track_ == current_track_) - { - ma_result r = ma_sound_start(&audio_sound_); - - if (r == MA_SUCCESS) - { - play_state_ = PlayState::PLAYING; - } - else - { - SLOGI("[Music] resume failed, result=%d", static_cast(r)); - stop_playback(); - play_state_ = PlayState::STOPPED; - } - - update_main_ui(); - return; - } - - // Otherwise restart the current track - if (start_playback()) - play_state_ = PlayState::PLAYING; - else - play_state_ = PlayState::STOPPED; - - update_main_ui(); - } - - void pause() - { - if (play_state_ != PlayState::PLAYING) - return; - - if (sound_loaded_) - { - ma_sound_stop(&audio_sound_); - play_state_ = PlayState::PAUSED; - } - - update_main_ui(); - } - -private: - // ==================== Data members ==================== - - std::unordered_map ui_obj_; - - std::string browse_dir_; - std::vector browse_entries_; - - std::string music_dir_; - std::vector playlist_; - - int current_track_ = 0; - - PlayState play_state_ = PlayState::STOPPED; - ViewState view_state_ = ViewState::MAIN; - - // ==================== miniaudio / PulseAudio ==================== - - ma_context audio_ctx_{}; - ma_engine audio_engine_{}; - ma_sound audio_sound_{}; - - bool audio_ready_ = false; - bool sound_loaded_ = false; - - int loaded_track_ = -1; - - std::atomic_bool track_finished_{false}; - - lv_timer_t *audio_timer_ = nullptr; - -private: - // ==================== POSIX path utilities ==================== - - static std::string path_parent(const std::string &path) - { - if (path.empty()) - return "/home/pi"; - - std::vector buf(path.begin(), path.end()); - buf.push_back('\0'); - - char *p = dirname(buf.data()); - return std::string(p ? p : "/"); - } - - static std::string path_basename(const std::string &path) - { - if (path.empty()) - return ""; - - std::vector buf(path.begin(), path.end()); - buf.push_back('\0'); - - char *p = basename(buf.data()); - return std::string(p ? p : ""); - } - - static std::string path_join(const std::string &base, const std::string &name) - { - if (base.empty()) - return name; - - if (name.empty()) - return base; - - if (base == "/") - return "/" + name; - - if (!base.empty() && base.back() == '/') - return base + name; - - return base + "/" + name; - } - - static bool is_supported_audio_file(const std::string &fname) - { - size_t pos = fname.rfind('.'); - if (pos == std::string::npos) - return false; - - std::string ext = fname.substr(pos); - - for (auto &c : ext) - c = static_cast(tolower(static_cast(c))); - - return ext == ".mp3" || ext == ".wav"; - } - -private: - // ==================== UI construction ==================== - - void creat_UI() - { - lv_obj_t *bg = lv_obj_create(ui_APP_Container); - lv_obj_set_size(bg, 320, 150); - lv_obj_set_pos(bg, 0, 0); - lv_obj_set_style_radius(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(bg, lv_color_hex(0x0D1117), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(bg, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["ui_bg"] = bg; - - lv_obj_t *title_bar = lv_obj_create(bg); - lv_obj_set_size(title_bar, 320, 22); - lv_obj_set_pos(title_bar, 0, 0); - lv_obj_set_style_radius(title_bar, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(title_bar, lv_color_hex(0x1F3A5F), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(title_bar, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(title_bar, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(title_bar, 8, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *lbl_title = lv_label_create(title_bar); - lv_label_set_text(lbl_title, "Music Player"); - lv_obj_set_align(lbl_title, LV_ALIGN_LEFT_MID); - lv_obj_set_style_text_color(lbl_title, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lbl_hint = lv_label_create(title_bar); - lv_label_set_text(lbl_hint, "i:Folder p:List ESC:Back"); - lv_obj_set_align(lbl_hint, LV_ALIGN_RIGHT_MID); - lv_obj_set_x(lbl_hint, -4); - lv_obj_set_style_text_color(lbl_hint, lv_color_hex(0x7EA8D8), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *content = lv_obj_create(bg); - lv_obj_set_size(content, 320, 128); - lv_obj_set_pos(content, 0, 22); - lv_obj_set_style_radius(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(content, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(content, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["ui_content"] = content; - - lv_obj_t *cover = lv_obj_create(content); - lv_obj_set_size(cover, 96, 96); - lv_obj_set_pos(cover, 8, 16); - lv_obj_set_style_radius(cover, 8, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(cover, lv_color_hex(0x1A2A4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(cover, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(cover, lv_color_hex(0x3A5A8A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(cover, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(cover, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *lbl_note = lv_label_create(cover); - lv_label_set_text(lbl_note, "MUSIC"); - lv_obj_set_align(lbl_note, LV_ALIGN_CENTER); - lv_obj_set_style_text_color(lbl_note, lv_color_hex(0x4A7ABF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_note, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lbl_track = lv_label_create(content); - lv_label_set_text(lbl_track, "No track"); - lv_obj_set_pos(lbl_track, 114, 8); - lv_obj_set_width(lbl_track, 198); - lv_label_set_long_mode(lbl_track, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_set_style_text_color(lbl_track, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_track, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["ui_lbl_track"] = lbl_track; - - lv_obj_t *lbl_count = lv_label_create(content); - lv_label_set_text(lbl_count, "0 / 0"); - lv_obj_set_pos(lbl_count, 114, 30); - lv_obj_set_style_text_color(lbl_count, lv_color_hex(0x8AABCF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_count, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["ui_lbl_count"] = lbl_count; - - lv_obj_t *lbl_dir = lv_label_create(content); - lv_label_set_text(lbl_dir, "Dir: (none)"); - lv_obj_set_pos(lbl_dir, 114, 48); - lv_obj_set_width(lbl_dir, 198); - lv_label_set_long_mode(lbl_dir, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_set_style_text_color(lbl_dir, lv_color_hex(0x6A8FAF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_dir, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["ui_lbl_dir"] = lbl_dir; - - lv_obj_t *lbl_state = lv_label_create(content); - lv_label_set_text(lbl_state, "[STOPPED]"); - lv_obj_set_pos(lbl_state, 114, 65); - lv_obj_set_style_text_color(lbl_state, lv_color_hex(0xFFD700), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_state, &lv_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["ui_lbl_state"] = lbl_state; - - lv_obj_t *lbl_keys = lv_label_create(content); - lv_label_set_text(lbl_keys, "UP:Play DOWN:Pause LEFT/RIGHT:Prev/Next"); - lv_obj_set_pos(lbl_keys, 4, 112); - lv_obj_set_width(lbl_keys, 312); - lv_label_set_long_mode(lbl_keys, LV_LABEL_LONG_CLIP); - lv_obj_set_style_text_color(lbl_keys, lv_color_hex(0x4A5A6A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_keys, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - } - -private: - // ==================== Event binding ==================== - - void event_handler_init() - { - lv_obj_add_event_cb(root_screen_, UIMusicPage::static_lvgl_handler, LV_EVENT_ALL, this); - } - - static void static_lvgl_handler(lv_event_t *e) - { - UIMusicPage *self = static_cast(lv_event_get_user_data(e)); - if (self) - self->event_handler(e); - } - - static uint32_t fzxc_to_lv_arrow(uint32_t key) - { - switch (key) - { - case KEY_F: - return LV_KEY_UP; - case KEY_X: - return LV_KEY_DOWN; - case KEY_Z: - return LV_KEY_LEFT; - case KEY_C: - return LV_KEY_RIGHT; - default: - return key; - } - } - - void event_handler(lv_event_t *e) - { - lv_event_code_t ec = lv_event_get_code(e); - - if (ec == static_cast(LV_EVENT_KEYBOARD)) - { - struct key_item *elm = static_cast(lv_event_get_param(e)); - - SLOGI("[MUSIC][KEYBOARD] code=%u state=%s sym=%s view=%d", - elm->key_code, - kbd_state_name(elm->key_state), - elm->sym_name, - static_cast(view_state_)); - return; - } - - if (ec != LV_EVENT_KEY) - return; - - uint32_t raw = lv_event_get_key(e); - uint32_t key = fzxc_to_lv_arrow(raw); - - SLOGI("[MUSIC][LV_KEY] raw=%u mapped=%u view=%d", - raw, - key, - static_cast(view_state_)); - - switch (view_state_) - { - case ViewState::MAIN: - handle_main_key(key); - break; - - case ViewState::FOLDER_SEL: - handle_folder_key(key); - break; - - case ViewState::PLAYLIST: - handle_playlist_key(key); - break; - } - } - -private: - // ================================================================ - // Main screen keys - // ================================================================ - - void handle_main_key(uint32_t key) - { - switch (key) - { - case LV_KEY_UP: - play(); - break; - - case LV_KEY_DOWN: - pause(); - break; - - case LV_KEY_LEFT: - prev_track(); - break; - - case LV_KEY_RIGHT: - next_track(); - break; - - case 15: - open_folder_browser(); - break; - - case 'p': - open_playlist(); - break; - - case LV_KEY_ESC: - SLOGI("[MUSIC] ESC -> navigate_home()"); - navigate_home(); - break; - - default: - break; - } - } - -private: - // ================================================================ - // Directory browser keys - // ================================================================ - - void handle_folder_key(uint32_t key) - { - lv_obj_t *roller = ui_obj_.count("ui_folder_roller") ? ui_obj_["ui_folder_roller"] : nullptr; - if (!roller) - return; - - switch (key) - { - case LV_KEY_UP: - { - uint16_t sel = lv_roller_get_selected(roller); - if (sel > 0) - lv_roller_set_selected(roller, sel - 1, LV_ANIM_ON); - break; - } - - case LV_KEY_DOWN: - { - uint16_t sel = lv_roller_get_selected(roller); - uint16_t cnt = static_cast(browse_entries_.size()); - - if (cnt > 0 && sel + 1 < cnt) - lv_roller_set_selected(roller, sel + 1, LV_ANIM_ON); - - break; - } - - case LV_KEY_RIGHT: - { - uint16_t sel = lv_roller_get_selected(roller); - - if (sel < static_cast(browse_entries_.size())) - { - const std::string &entry = browse_entries_[sel]; - - if (entry != "..") - { - std::string target = path_join(browse_dir_, entry); - - struct stat st; - if (stat(target.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) - { - navigate_to(target); - } - } - } - - break; - } - - case LV_KEY_LEFT: - { - navigate_to(path_parent(browse_dir_)); - break; - } - - case LV_KEY_ENTER: - { - // ENTER selects the current directory and scans audio files - load_music_from_folder(browse_dir_); - close_folder_browser(); - break; - } - - case LV_KEY_ESC: - close_folder_browser(); - break; - - default: - break; - } - } - -private: - // ================================================================ - // Playlist keys - // ================================================================ - - void handle_playlist_key(uint32_t key) - { - lv_obj_t *roller = ui_obj_.count("ui_playlist_roller") ? ui_obj_["ui_playlist_roller"] : nullptr; - - switch (key) - { - case LV_KEY_UP: - { - if (roller) - { - uint16_t sel = lv_roller_get_selected(roller); - if (sel > 0) - lv_roller_set_selected(roller, sel - 1, LV_ANIM_ON); - } - break; - } - - case LV_KEY_DOWN: - { - if (roller) - { - uint16_t sel = lv_roller_get_selected(roller); - uint16_t cnt = static_cast(playlist_.size()); - - if (cnt > 0 && sel + 1 < cnt) - lv_roller_set_selected(roller, sel + 1, LV_ANIM_ON); - } - break; - } - - case LV_KEY_ENTER: - { - if (roller && !playlist_.empty()) - { - uint16_t sel = lv_roller_get_selected(roller); - - if (sel < static_cast(playlist_.size())) - { - current_track_ = static_cast(sel); - - if (start_playback()) - play_state_ = PlayState::PLAYING; - else - play_state_ = PlayState::STOPPED; - } - } - - close_playlist(); - update_main_ui(); - break; - } - - case 'p': - case LV_KEY_ESC: - close_playlist(); - break; - - default: - break; - } - } - -private: - // ================================================================ - // Directory browser - // ================================================================ - - void open_folder_browser() - { - view_state_ = ViewState::FOLDER_SEL; - browse_dir_ = "/"; - - lv_obj_t *panel = lv_obj_create(ui_APP_Container); - lv_obj_set_size(panel, 316, 148); - lv_obj_set_pos(panel, 2, 1); - lv_obj_set_style_radius(panel, 4, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(panel, lv_color_hex(0x0D1B2A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(panel, 250, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(panel, lv_color_hex(0x1F6FEB), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["ui_folder_panel"] = panel; - - lv_obj_t *lbl_path = lv_label_create(panel); - lv_label_set_text(lbl_path, browse_dir_.c_str()); - lv_obj_set_pos(lbl_path, 4, 3); - lv_obj_set_width(lbl_path, 308); - lv_label_set_long_mode(lbl_path, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_set_style_text_color(lbl_path, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_path, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_obj_["ui_folder_path_lbl"] = lbl_path; - - lv_obj_t *lbl_h = lv_label_create(panel); - lv_label_set_text(lbl_h, "UP/DN:sel RIGHT:enter dir LEFT:up OK:load"); - lv_obj_set_pos(lbl_h, 2, 132); - lv_obj_set_width(lbl_h, 312); - lv_label_set_long_mode(lbl_h, LV_LABEL_LONG_CLIP); - lv_obj_set_style_text_color(lbl_h, lv_color_hex(0x3A5A7A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_h, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *roller = lv_roller_create(panel); - lv_obj_set_size(roller, 308, 114); - lv_obj_set_pos(roller, 4, 16); - lv_obj_set_style_radius(roller, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(roller, lv_color_hex(0x0D1B2A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(roller, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(roller, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(roller, lv_color_hex(0xCCDDEE), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(roller, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(roller, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(roller, lv_color_hex(0x1F3A5F), LV_PART_SELECTED | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(roller, 220, LV_PART_SELECTED | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(roller, lv_color_hex(0xFFFFFF), LV_PART_SELECTED | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(roller, &lv_font_montserrat_12, LV_PART_SELECTED | LV_STATE_DEFAULT); - - ui_obj_["ui_folder_roller"] = roller; - - refresh_folder_roller(); - } - - void navigate_to(const std::string &dir) - { - browse_dir_ = dir.empty() ? std::string("/") : dir; - - if (ui_obj_.count("ui_folder_path_lbl") && ui_obj_["ui_folder_path_lbl"]) - lv_label_set_text(ui_obj_["ui_folder_path_lbl"], browse_dir_.c_str()); - - refresh_folder_roller(); - } - - void refresh_folder_roller() - { - browse_entries_.clear(); - - if (browse_dir_ != "/") - browse_entries_.push_back(".."); - - DIR *dp = opendir(browse_dir_.c_str()); - - if (dp) - { - struct dirent *ent; - - while ((ent = readdir(dp)) != nullptr) - { - if (ent->d_name[0] == '.') - continue; - - std::string full = path_join(browse_dir_, ent->d_name); - - struct stat st; - if (stat(full.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) - { - browse_entries_.push_back(ent->d_name); - } - } - - closedir(dp); - } - else - { - SLOGI("[Music] opendir failed: %s", browse_dir_.c_str()); - } - - if (!browse_entries_.empty() && browse_entries_[0] == "..") - std::sort(browse_entries_.begin() + 1, browse_entries_.end()); - else - std::sort(browse_entries_.begin(), browse_entries_.end()); - - std::string options; - - if (browse_entries_.empty()) - { - options = "(empty)"; - } - else - { - for (size_t i = 0; i < browse_entries_.size(); ++i) - { - if (i) - options += '\n'; - - if (browse_entries_[i] == "..") - options += "../"; - else - options += browse_entries_[i] + "/"; - } - } - - lv_obj_t *roller = ui_obj_.count("ui_folder_roller") ? ui_obj_["ui_folder_roller"] : nullptr; - - if (roller) - { - lv_roller_set_options(roller, options.c_str(), LV_ROLLER_MODE_NORMAL); - lv_roller_set_visible_row_count(roller, 5); - lv_roller_set_selected(roller, 0, LV_ANIM_OFF); - } - } - - void close_folder_browser() - { - if (ui_obj_.count("ui_folder_panel") && ui_obj_["ui_folder_panel"]) - { - lv_obj_del(ui_obj_["ui_folder_panel"]); - - ui_obj_["ui_folder_panel"] = nullptr; - ui_obj_["ui_folder_roller"] = nullptr; - ui_obj_["ui_folder_path_lbl"] = nullptr; - } - - view_state_ = ViewState::MAIN; - } - -private: - // ================================================================ - // Playlist popup - // ================================================================ - - void open_playlist() - { - if (playlist_.empty()) - return; - - view_state_ = ViewState::PLAYLIST; - - lv_obj_t *panel = lv_obj_create(ui_APP_Container); - lv_obj_set_size(panel, 316, 148); - lv_obj_set_pos(panel, 2, 1); - lv_obj_set_style_radius(panel, 4, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(panel, lv_color_hex(0x0D1B2A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(panel, 250, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(panel, lv_color_hex(0x00AA66), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(panel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - ui_obj_["ui_playlist_panel"] = panel; - - lv_obj_t *lbl_t = lv_label_create(panel); - - char title_buf[64]; - snprintf(title_buf, sizeof(title_buf), "Playlist %d audio", static_cast(playlist_.size())); - - lv_label_set_text(lbl_t, title_buf); - lv_obj_set_pos(lbl_t, 6, 3); - lv_obj_set_style_text_color(lbl_t, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_t, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lbl_h = lv_label_create(panel); - lv_label_set_text(lbl_h, "UP/DOWN: select OK: play p/ESC: cancel"); - lv_obj_set_pos(lbl_h, 2, 132); - lv_obj_set_style_text_color(lbl_h, lv_color_hex(0x2A6A4A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl_h, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - - std::string options; - - for (size_t i = 0; i < playlist_.size(); ++i) - { - if (i) - options += '\n'; - - options += path_basename(playlist_[i]); - } - - lv_obj_t *roller = lv_roller_create(panel); - lv_roller_set_options(roller, options.c_str(), LV_ROLLER_MODE_NORMAL); - lv_roller_set_visible_row_count(roller, 5); - - uint16_t init_sel = 0; - - if (current_track_ >= 0 && current_track_ < static_cast(playlist_.size())) - init_sel = static_cast(current_track_); - - lv_roller_set_selected(roller, init_sel, LV_ANIM_OFF); - - lv_obj_set_size(roller, 308, 114); - lv_obj_set_pos(roller, 4, 16); - lv_obj_set_style_radius(roller, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(roller, lv_color_hex(0x0D1B2A), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(roller, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(roller, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(roller, lv_color_hex(0xCCDDCC), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(roller, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(roller, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(roller, lv_color_hex(0x1A4A2A), LV_PART_SELECTED | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(roller, 220, LV_PART_SELECTED | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(roller, lv_color_hex(0xFFFFFF), LV_PART_SELECTED | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(roller, &lv_font_montserrat_12, LV_PART_SELECTED | LV_STATE_DEFAULT); - - ui_obj_["ui_playlist_roller"] = roller; - } - - void close_playlist() - { - if (ui_obj_.count("ui_playlist_panel") && ui_obj_["ui_playlist_panel"]) - { - lv_obj_del(ui_obj_["ui_playlist_panel"]); - - ui_obj_["ui_playlist_panel"] = nullptr; - ui_obj_["ui_playlist_roller"] = nullptr; - } - - view_state_ = ViewState::MAIN; - } - -private: - // ================================================================ - // Scan mp3 / wav files in the specified directory - // ================================================================ - - void load_music_from_folder(const std::string &dir) - { - stop_playback(); - - playlist_.clear(); - music_dir_ = dir; - current_track_ = 0; - play_state_ = PlayState::STOPPED; - - DIR *dp = opendir(dir.c_str()); - - if (!dp) - { - SLOGI("[Music] Cannot open dir: %s", dir.c_str()); - update_main_ui(); - return; - } - - struct dirent *ent; - - while ((ent = readdir(dp)) != nullptr) - { - std::string fname = ent->d_name; - - if (fname.empty()) - continue; - - if (fname[0] == '.') - continue; - - if (!is_supported_audio_file(fname)) - continue; - - std::string full = path_join(dir, fname); - - struct stat st; - if (stat(full.c_str(), &st) != 0) - continue; - - if (!S_ISREG(st.st_mode)) - continue; - - playlist_.push_back(full); - } - - closedir(dp); - - std::sort(playlist_.begin(), playlist_.end()); - - SLOGI("[Music] Loaded %d audio files from %s", - static_cast(playlist_.size()), - dir.c_str()); - - update_main_ui(); - } - -private: - // ================================================================ - // miniaudio initialization: PulseAudio backend - // ================================================================ - - void init_audio() - { - ma_backend backends[] = { - ma_backend_pulseaudio - }; - - ma_result r = ma_context_init(backends, 1, nullptr, &audio_ctx_); - - if (r != MA_SUCCESS) - { - SLOGI("[Music] ma_context_init PulseAudio failed, result=%d", static_cast(r)); - audio_ready_ = false; - return; - } - - ma_engine_config engine_config = ma_engine_config_init(); - engine_config.pContext = &audio_ctx_; - - r = ma_engine_init(&engine_config, &audio_engine_); - - if (r != MA_SUCCESS) - { - SLOGI("[Music] ma_engine_init failed, result=%d", static_cast(r)); - - ma_context_uninit(&audio_ctx_); - - audio_ready_ = false; - return; - } - - audio_ready_ = true; - - audio_timer_ = lv_timer_create(UIMusicPage::static_audio_timer_cb, 200, this); - - SLOGI("[Music] miniaudio initialized with PulseAudio backend"); - } - - void uninit_audio() - { - if (audio_ready_) - { - ma_engine_uninit(&audio_engine_); - ma_context_uninit(&audio_ctx_); - audio_ready_ = false; - } - } - -private: - // ================================================================ - // Playback completion callback - // Note:This callback runs on the audio thread; do not operate LVGL directly - // ================================================================ - - static void static_sound_end_cb(void *pUserData, ma_sound *pSound) - { - (void)pSound; - - UIMusicPage *self = static_cast(pUserData); - if (!self) - return; - - self->track_finished_.store(true); - } - - static void static_audio_timer_cb(lv_timer_t *timer) - { -#if defined(LVGL_VERSION_MAJOR) && LVGL_VERSION_MAJOR >= 9 - UIMusicPage *self = static_cast(lv_timer_get_user_data(timer)); -#else - UIMusicPage *self = static_cast(timer->user_data); -#endif - - if (!self) - return; - - self->audio_timer_cb(); - } - - void audio_timer_cb() - { - if (!track_finished_.exchange(false)) - return; - - if (play_state_ != PlayState::PLAYING) - return; - - if (playlist_.empty()) - return; - - current_track_ = (current_track_ + 1) % static_cast(playlist_.size()); - - if (start_playback()) - play_state_ = PlayState::PLAYING; - else - play_state_ = PlayState::STOPPED; - - update_main_ui(); - } - -private: - // ================================================================ - // Playback control: miniaudio + PulseAudio - // ================================================================ - - bool start_playback() - { - stop_playback(); - - if (!audio_ready_) - { - SLOGI("[Music] Audio not ready. PulseAudio backend unavailable."); - return false; - } - - if (playlist_.empty()) - return false; - - if (current_track_ < 0 || current_track_ >= static_cast(playlist_.size())) - return false; - - const std::string &file = playlist_[current_track_]; - - SLOGI("[Music] Playing by miniaudio: %s", file.c_str()); - - track_finished_.store(false); - - ma_uint32 flags = MA_SOUND_FLAG_STREAM; - - ma_result r = ma_sound_init_from_file( - &audio_engine_, - file.c_str(), - flags, - nullptr, - nullptr, - &audio_sound_); - - if (r != MA_SUCCESS) - { - SLOGI("[Music] ma_sound_init_from_file failed, result=%d, file=%s", - static_cast(r), - file.c_str()); - - sound_loaded_ = false; - loaded_track_ = -1; - return false; - } - - sound_loaded_ = true; - loaded_track_ = current_track_; - - ma_sound_set_end_callback( - &audio_sound_, - UIMusicPage::static_sound_end_cb, - this); - - r = ma_sound_start(&audio_sound_); - - if (r != MA_SUCCESS) - { - SLOGI("[Music] ma_sound_start failed, result=%d", static_cast(r)); - stop_playback(); - return false; - } - - return true; - } - - void stop_playback() - { - track_finished_.store(false); - - if (sound_loaded_) - { - ma_sound_set_end_callback(&audio_sound_, nullptr, nullptr); - - ma_sound_stop(&audio_sound_); - ma_sound_uninit(&audio_sound_); - - sound_loaded_ = false; - loaded_track_ = -1; - } - } - -private: - // ================================================================ - // Refresh main screen labels - // ================================================================ - - void update_main_ui() - { - if (!playlist_.empty() && - current_track_ >= 0 && - current_track_ < static_cast(playlist_.size())) - { - std::string fname = path_basename(playlist_[current_track_]); - lv_label_set_text(ui_obj_["ui_lbl_track"], fname.c_str()); - } - else - { - lv_label_set_text(ui_obj_["ui_lbl_track"], "No track"); - } - - char buf[32]; - - snprintf(buf, - sizeof(buf), - "%d / %d", - playlist_.empty() ? 0 : current_track_ + 1, - static_cast(playlist_.size())); - - lv_label_set_text(ui_obj_["ui_lbl_count"], buf); - - std::string dir_show = "Dir: " + (music_dir_.empty() ? std::string("(none)") : music_dir_); - lv_label_set_text(ui_obj_["ui_lbl_dir"], dir_show.c_str()); - - const char *state_str = "[STOPPED]"; - - if (!audio_ready_) - { - state_str = "[NO PULSE]"; - } - else if (play_state_ == PlayState::PLAYING) - { - state_str = "[ PLAYING ]"; - } - else if (play_state_ == PlayState::PAUSED) - { - state_str = "[ PAUSED ]"; - } - - lv_label_set_text(ui_obj_["ui_lbl_state"], state_str); - } -}; \ No newline at end of file diff --git a/projects/APPLaunch/win_x86_cross_config_defaults.mk b/projects/APPLaunch/win_x86_cross_config_defaults.mk new file mode 100644 index 00000000..276f0450 --- /dev/null +++ b/projects/APPLaunch/win_x86_cross_config_defaults.mk @@ -0,0 +1,110 @@ +# https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe + + +# Windows host -> CardputerZero/Raspberry64 AArch64 Linux cross build. +CONFIG_TOOLCHAIN_PATH="D:\\app\\SysGCC\\bin" +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_TOOLCHAIN_SYSTEM_WIN=y +CONFIG_GCC_DUMPMACHINE="aarch64-linux-gnu" +CONFIG_REPO_AUTOMATION=y + + + + +CONFIG_LVGL_COMPONENT_ENABLED=y +CONFIG_LVGL_9_5_SRC=y + + + + +CONFIG_V9_5_LV_USE_CLIB_MALLOC=y +CONFIG_V9_5_LV_USE_CLIB_STRING=y +CONFIG_V9_5_LV_USE_CLIB_SPRINTF=y + +# CONFIG_V9_5_LV_USE_LINUX_FBDEV=y + +CONFIG_V9_5_LV_USE_DEMO_MUSIC=y + + +# CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +# CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 + +# CONFIG_SMOOTH_UI_TOOLKIT_ENABLED=y + + +CONFIG_V9_5_LV_FS_DEFAULT_DRIVER_LETTER=65 +CONFIG_V9_5_LV_USE_FS_POSIX=y +CONFIG_V9_5_LV_FS_POSIX_LETTER=65 +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +CONFIG_V9_5_LV_FS_POSIX_CACHE_SIZE=0 + + +CONFIG_V9_5_LV_USE_LODEPNG=y + +CONFIG_V9_5_LV_USE_VECTOR_GRAPHIC=y +CONFIG_V9_5_LV_USE_THORVG=y +CONFIG_V9_5_LV_USE_THORVG_INTERNAL=y + + + + + + + + +CONFIG_V9_5_LV_FONT_MONTSERRAT_8=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_10=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_12=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_14=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_16=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_18=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_20=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_22=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_24=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_26=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_28=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_30=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_32=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_34=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_36=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_38=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_40=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_42=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_44=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_46=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_48=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_28_COMPRESSED=y +CONFIG_V9_5_LV_FONT_DEJAVU_16_PERSIAN_HEBREW=y +CONFIG_V9_5_LV_FONT_SOURCE_HAN_SANS_SC_14_CJK=y +CONFIG_V9_5_LV_FONT_SOURCE_HAN_SANS_SC_16_CJK=y +CONFIG_V9_5_LV_FONT_UNSCII_8=y +CONFIG_V9_5_LV_FONT_UNSCII_16=y + +CONFIG_V9_5_LV_FONT_DEFAULT_MONTSERRAT_14=y +CONFIG_V9_5_LV_USE_FREETYPE=y +CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 + + + +CONFIG_V9_5_LV_USE_EXT_DATA=y +CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y + +CONFIG_V9_5_LV_USE_GIF=y + +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_LINUX_FBDEV_RENDER_MODE_FULL=y +# Helium/ARMv7 assembly is not valid for this AArch64 SysGCC build. +# CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +# CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_OS_PTHREAD=y +CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 +CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_SIGSLOT_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y +CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y +CONFIG_APPLAUNCH_WIN_X86_CROSS_CP0=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file diff --git a/projects/APPLaunch/win_x86_sdl2_config_defaults.mk b/projects/APPLaunch/win_x86_sdl2_config_defaults.mk new file mode 100644 index 00000000..b9f73c18 --- /dev/null +++ b/projects/APPLaunch/win_x86_sdl2_config_defaults.mk @@ -0,0 +1,104 @@ + + +# Windows x86 SDL2 build configuration. +# Toolchain settings are intentionally empty for host-native builds. +# CONFIG_TOOLCHAIN_PATH="" +# CONFIG_TOOLCHAIN_PREFIX="" +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_TOOLCHAIN_SYSTEM_WIN=y +CONFIG_REPO_AUTOMATION=y + + + + +CONFIG_LVGL_COMPONENT_ENABLED=y +CONFIG_LVGL_9_5_SRC=y + + + + +CONFIG_V9_5_LV_USE_CLIB_MALLOC=y +CONFIG_V9_5_LV_USE_CLIB_STRING=y +CONFIG_V9_5_LV_USE_CLIB_SPRINTF=y + +# CONFIG_V9_5_LV_USE_LINUX_FBDEV=y + +CONFIG_V9_5_LV_USE_DEMO_MUSIC=y + + +# CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +# CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 + +# CONFIG_SMOOTH_UI_TOOLKIT_ENABLED=y + + +CONFIG_V9_5_LV_FS_DEFAULT_DRIVER_LETTER=65 +CONFIG_V9_5_LV_USE_FS_POSIX=y +CONFIG_V9_5_LV_FS_POSIX_LETTER=65 +CONFIG_V9_5_LV_FS_POSIX_CACHE_SIZE=0 + + +CONFIG_V9_5_LV_USE_LODEPNG=y + +CONFIG_V9_5_LV_USE_VECTOR_GRAPHIC=y +CONFIG_V9_5_LV_USE_THORVG=y +CONFIG_V9_5_LV_USE_THORVG_INTERNAL=y + + + + + + + + +CONFIG_V9_5_LV_FONT_MONTSERRAT_8=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_10=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_12=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_14=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_16=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_18=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_20=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_22=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_24=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_26=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_28=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_30=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_32=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_34=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_36=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_38=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_40=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_42=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_44=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_46=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_48=y +CONFIG_V9_5_LV_FONT_MONTSERRAT_28_COMPRESSED=y +CONFIG_V9_5_LV_FONT_DEJAVU_16_PERSIAN_HEBREW=y +CONFIG_V9_5_LV_FONT_SOURCE_HAN_SANS_SC_14_CJK=y +CONFIG_V9_5_LV_FONT_SOURCE_HAN_SANS_SC_16_CJK=y +CONFIG_V9_5_LV_FONT_UNSCII_8=y +CONFIG_V9_5_LV_FONT_UNSCII_16=y + +CONFIG_V9_5_LV_FONT_DEFAULT_MONTSERRAT_14=y +CONFIG_V9_5_LV_USE_FREETYPE=y +CONFIG_V9_5_LV_FREETYPE_CACHE_FT_GLYPH_CNT=512 + + + +CONFIG_V9_5_LV_USE_EXT_DATA=y +CONFIG_V9_5_LV_USE_DEMO_WIDGETS=y + +CONFIG_V9_5_LV_USE_GIF=y + +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_V9_5_LV_OS_PTHREAD=y +CONFIG_V9_5_LV_DRAW_THREAD_STACK_SIZE=65536 +CONFIG_V9_5_LV_DRAW_THREAD_PRIO=3 +CONFIG_MINIAUDIO_COMPONENT_ENABLED=y +CONFIG_SIGSLOT_COMPONENT_ENABLED=y +CONFIG_CP0_LVGL_COMPONENT_ENABLED=y +CONFIG_EVENTPP_ENABLED=y +CONFIG_RADIOLIB_COMPONENT_ENABLED=y +CONFIG_APPLAUNCH_WIN_X86_SDL2=y +CONFIG_UTILITIES_ENABLED=y \ No newline at end of file From d59e4c5509ef65a5828dfbdb5d0ba819ece1646b Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 16:28:43 +0800 Subject: [PATCH 53/70] refactor: split cp0_lvgl platform services Move cp0_lvgl service implementations into explicit CP0 and SDL backends and retire the older cp0_app/hal shim files. Add filesystem/config/network/process/pty/screenshot/settings/imu/lora/bq27220 service modules for CP0 and SDL, update registration headers, and adjust the cp0_lvgl build graph. Introduce APPLaunch app registry plumbing and update launcher UI/page integration to use the platform service APIs. Add architecture and platform-decoupling documentation for the launcher refactor. --- ext_components/cp0_lvgl/SConstruct | 3 +- .../cp0_lvgl/include/cp0_lvgl_app.h | 85 +- .../cp0_lvgl/include/cp0_lvgl_file.hpp | 4 +- .../cp0_lvgl/include/cp0_lvgl_filesystem.hpp | 5 + .../cp0_lvgl/include/hal/hal_config.h | 16 - ext_components/cp0_lvgl/include/hal/hal_pty.h | 20 - .../cp0_lvgl/include/hal/hal_screenshot.h | 11 - .../cp0_lvgl/include/hal_lvgl_bsp.h | 1 + .../cp0_lvgl/include/signal_register_plan.h | 12 +- ext_components/cp0_lvgl/src/commount.c | 11 - ext_components/cp0_lvgl/src/commount.cpp | 29 + .../cp0_lvgl/src/cp0/cp0_app_config.cpp | 109 -- .../cp0_lvgl/src/cp0/cp0_app_filesystem.cpp | 58 - .../cp0_lvgl/src/cp0/cp0_app_network.cpp | 33 - .../cp0_lvgl/src/cp0/cp0_app_process.cpp | 269 --- .../cp0_lvgl/src/cp0/cp0_app_pty.cpp | 127 -- .../cp0_lvgl/src/cp0/cp0_app_screenshot.cpp | 127 -- .../cp0_lvgl/src/cp0/cp0_app_settings.cpp | 449 ----- ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c | 11 + ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h | 11 + .../cp0_lvgl/src/cp0/cp0_lvgl_app.cpp | 54 - .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_bq27220.cpp | 350 ++++ .../cp0_lvgl/src/cp0/cp0_lvgl_config.cpp | 221 +++ .../cp0_lvgl/src/cp0/cp0_lvgl_file.cpp | 88 - .../cp0_lvgl/src/cp0/cp0_lvgl_filesystem.cpp | 405 +++++ .../cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp | 243 +++ .../cp0_lvgl/src/cp0/cp0_lvgl_lara.cpp | 1425 +++++++++++++++ .../cp0_lvgl/src/cp0/cp0_lvgl_network.cpp | 411 +++++ .../cp0_lvgl/src/cp0/cp0_lvgl_osinfo.cpp | 269 +++ .../cp0_lvgl/src/cp0/cp0_lvgl_process.cpp | 756 ++++++++ .../cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp | 336 ++++ .../cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp | 188 ++ .../cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp | 403 +++++ .../cp0_lvgl/src/cp0_app_internal_utils.h | 18 + .../cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp | 321 ---- .../cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c | 86 - .../cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp | 11 - .../src/sdl/cp0_hal_filesystem_sdl.cpp | 90 - .../cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp | 46 - .../cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c | 43 - .../cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp | 201 --- .../cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp | 102 -- .../src/sdl/cp0_hal_screenshot_sdl.cpp | 9 - .../cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp | 86 - .../cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp | 1 - .../cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp | 10 - ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c | 18 +- ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h | 15 + .../cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp | 376 ++++ ..._lvgl_battery.cpp => sdl_lvgl_battery.cpp} | 0 .../cp0_lvgl/src/sdl/sdl_lvgl_bq27220.cpp | 159 ++ .../cp0_lvgl/src/sdl/sdl_lvgl_camera.cpp | 259 +++ .../cp0_lvgl/src/sdl/sdl_lvgl_config.cpp | 253 +++ .../cp0_lvgl/src/sdl/sdl_lvgl_display.c | 10 + .../cp0_lvgl/src/sdl/sdl_lvgl_file.cpp | 268 --- .../cp0_lvgl/src/sdl/sdl_lvgl_filesystem.cpp | 383 ++++ .../cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp | 111 ++ .../cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c | 222 ++- .../cp0_lvgl/src/sdl/sdl_lvgl_lara.cpp | 132 ++ .../cp0_lvgl/src/sdl/sdl_lvgl_network.cpp | 190 ++ .../cp0_lvgl/src/sdl/sdl_lvgl_osinfo.cpp | 341 ++++ .../cp0_lvgl/src/sdl/sdl_lvgl_process.cpp | 778 +++++++++ .../cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp | 336 ++++ .../cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp | 132 ++ .../cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp | 510 ++++++ .../cp0_lvgl/src/web/cp0_hal_stubs_web.c | 23 - .../cp0_lvgl/src/win32/cp0_hal_stubs_win32.c | 23 - .../APPLaunch/docs/architecture_review.md | 397 +++++ .../docs/cp0_lvgl_platform_decoupling_plan.md | 242 +++ projects/APPLaunch/main/SConstruct | 23 +- .../APPLaunch/main/include/APPLaunch_api.h | 15 + projects/APPLaunch/main/src/APPLaunch_api.cpp | 89 + projects/APPLaunch/main/ui/AppRegistry.cpp | 62 + projects/APPLaunch/main/ui/AppRegistry.h | 26 + projects/APPLaunch/main/ui/Launch.cpp | 233 ++- projects/APPLaunch/main/ui/Launch.h | 10 +- projects/APPLaunch/main/ui/UILaunchPage.cpp | 9 +- projects/APPLaunch/main/ui/UILaunchPage.h | 4 +- .../main/ui/generate_page_app_includes.py | 37 +- .../main/ui/page_app/ui_app_compass.hpp | 255 +-- .../main/ui/page_app/ui_app_console.hpp | 110 +- .../main/ui/page_app/ui_app_lora.hpp | 1541 ++--------------- .../main/ui/page_app/ui_app_setup.hpp | 289 ++-- projects/APPLaunch/main/ui/ui_app_page.hpp | 108 +- projects/APPLaunch/main/ui/ui_global_hint.cpp | 7 +- projects/APPLaunch/main/ui/zero_lvgl_os.cpp | 16 +- projects/APPLaunch/main/ui/zero_lvgl_os.h | 7 +- 88 files changed, 10786 insertions(+), 4798 deletions(-) create mode 100644 ext_components/cp0_lvgl/include/cp0_lvgl_filesystem.hpp delete mode 100644 ext_components/cp0_lvgl/include/hal/hal_config.h delete mode 100644 ext_components/cp0_lvgl/include/hal/hal_pty.h delete mode 100644 ext_components/cp0_lvgl/include/hal/hal_screenshot.h delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_bq27220.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp delete mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_filesystem.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_lara.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_osinfo.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp create mode 100644 ext_components/cp0_lvgl/src/cp0_app_internal_utils.h delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp rename ext_components/cp0_lvgl/src/sdl/{cp0_lvgl_battery.cpp => sdl_lvgl_battery.cpp} (100%) create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_bq27220.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_camera.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp delete mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_filesystem.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_lara.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_osinfo.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp create mode 100644 ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp create mode 100644 projects/APPLaunch/docs/architecture_review.md create mode 100644 projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md create mode 100644 projects/APPLaunch/main/include/APPLaunch_api.h create mode 100644 projects/APPLaunch/main/src/APPLaunch_api.cpp create mode 100644 projects/APPLaunch/main/ui/AppRegistry.cpp create mode 100644 projects/APPLaunch/main/ui/AppRegistry.h diff --git a/ext_components/cp0_lvgl/SConstruct b/ext_components/cp0_lvgl/SConstruct index 6c90d2d5..ca71c8dd 100644 --- a/ext_components/cp0_lvgl/SConstruct +++ b/ext_components/cp0_lvgl/SConstruct @@ -10,7 +10,7 @@ if "CONFIG_CP0_LVGL_COMPONENT_ENABLED" in os.environ or "CONFIG_SIGSLOT_COMPONEN SRCS = [] INCLUDE = [ADir('include')] PRIVATE_INCLUDE = [] - REQUIREMENTS = ['lvgl_component', 'eventpp', 'Miniaudio'] + REQUIREMENTS = ['lvgl_component', 'eventpp', 'Miniaudio', 'RadioLib'] STATIC_LIB = [] DYNAMIC_LIB = [] DEFINITIONS = [] @@ -19,6 +19,7 @@ if "CONFIG_CP0_LVGL_COMPONENT_ENABLED" in os.environ or "CONFIG_SIGSLOT_COMPONEN LINK_SEARCH_PATH = [] SRCS += Glob('src/*.c*') if 'CONFIG_V9_5_LV_USE_SDL' in os.environ: + DEFINITIONS += ['-DHAL_PLATFORM_SDL=1'] SRCS += Glob('src/sdl/*.c*') else: SRCS += Glob('src/cp0/*.c*') diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index fa06f9bf..0582a6eb 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -62,21 +62,56 @@ typedef struct { int is_dir; } cp0_dirent_t; -typedef void *cp0_watcher_t; -typedef void *cp0_pty_t; -typedef int cp0_pid_t; +typedef struct { + char ipv4[48]; + char gateway[48]; + char mac[32]; +} cp0_eth_info_t; -void cp0_signal_audio_api_play_file(const char *path); -void cp0_signal_audio_api_play_asset(const char *name); -void cp0_signal_system_play_asset(const char *name); +typedef struct { + char user[64]; + char hostname[128]; +} cp0_account_info_t; -void cp0_config_init(void); -int cp0_config_get_int(const char *key, int default_val); -void cp0_config_set_int(const char *key, int val); -const char *cp0_config_get_str(const char *key, const char *default_val); -void cp0_config_set_str(const char *key, const char *val); -void cp0_config_save(void); +typedef struct { + char status[64]; + float yaw; + float pitch; + float roll; + float acc_x; + float acc_y; + float acc_z; + float gyr_x; + float gyr_y; + float gyr_z; + float mag_x; + float mag_y; + float mag_z; + int sensor_ready; +} cp0_compass_info_t; +typedef struct { + int initialized; + int hw_ready; + int tx_mode; + int tx_in_progress; + int has_sent_message; + int rx_event; + int tx_event; + char spi_device[64]; + char last_rx[128]; + char last_tx[128]; + char diag[256]; + char probe_summary[256]; + char probe_display[128]; + char pi4io_status[160]; + float rssi; + float snr; +} cp0_lora_info_t; + +typedef void *cp0_watcher_t; +typedef int cp0_pid_t; +typedef void (*cp0_compass_read_cb_t)(int code, const cp0_compass_info_t *info, void *user); const char *cp0_file_path_c(const char *file); @@ -95,24 +130,22 @@ void cp0_process_kill(int pid, int grace_ms); void cp0_system_shutdown(void); void cp0_system_reboot(void); -cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows); -int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size); -int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len); -int cp0_pty_check_child(cp0_pty_t pty, int *exit_status); -void cp0_pty_close(cp0_pty_t pty); - -int cp0_screenshot_save(const char *dir); - +int cp0_process_run_argv(const char *const *argv, int background); +int cp0_process_capture_argv(const char *const *argv, char *out, int out_size); +int cp0_file_read_first_line(const char *path, char *out, int out_size); +int cp0_desktop_exec_is_safe(const char *exec, char *reason, int reason_size); +int cp0_network_default_info_read(cp0_eth_info_t *info); +int cp0_eth_info_read(cp0_eth_info_t *info); +int cp0_account_info_read(cp0_account_info_t *info); +int cp0_system_apt_update_background(void); +int cp0_system_update_launcher_background(void); +int cp0_time_set(const char *timestamp); +int cp0_bq27220_calibrate(int command_index); +int cp0_compass_read(cp0_compass_read_cb_t callback, void *user); cp0_battery_info_t cp0_battery_read(void); int cp0_backlight_read(void); int cp0_backlight_max(void); int cp0_backlight_write(int val); -int cp0_volume_read(void); -int cp0_volume_write(int val); -cp0_wifi_status_t cp0_wifi_get_status(void); -int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps); -int cp0_wifi_connect(const char *ssid, const char *password); -int cp0_wifi_disconnect(void); cp0_bt_status_t cp0_bt_get_status(void); int cp0_bt_set_power(int on); int cp0_bt_scan(cp0_bt_device_t *out, int max_devices); diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp b/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp index 221bb53a..41734b43 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp @@ -1,5 +1,3 @@ #pragma once -#include - -std::string cp0_file_path(std::string file); +#include "cp0_lvgl_filesystem.hpp" diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_filesystem.hpp b/ext_components/cp0_lvgl/include/cp0_lvgl_filesystem.hpp new file mode 100644 index 00000000..221bb53a --- /dev/null +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_filesystem.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string cp0_file_path(std::string file); diff --git a/ext_components/cp0_lvgl/include/hal/hal_config.h b/ext_components/cp0_lvgl/include/hal/hal_config.h deleted file mode 100644 index a3a014a4..00000000 --- a/ext_components/cp0_lvgl/include/hal/hal_config.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -void hal_config_init(void); -int hal_config_get_int(const char *key, int default_val); -void hal_config_set_int(const char *key, int val); -const char *hal_config_get_str(const char *key, const char *default_val); -void hal_config_set_str(const char *key, const char *val); -void hal_config_save(void); - -#ifdef __cplusplus -} -#endif diff --git a/ext_components/cp0_lvgl/include/hal/hal_pty.h b/ext_components/cp0_lvgl/include/hal/hal_pty.h deleted file mode 100644 index c6d6abff..00000000 --- a/ext_components/cp0_lvgl/include/hal/hal_pty.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct hal_pty *hal_pty_t; - -hal_pty_t hal_pty_open(const char *cmd, const char *const *args, - int cols, int rows); -int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size); -int hal_pty_write(hal_pty_t pty, const char *buf, size_t len); -int hal_pty_check_child(hal_pty_t pty, int *exit_status); -void hal_pty_close(hal_pty_t pty); - -#ifdef __cplusplus -} -#endif diff --git a/ext_components/cp0_lvgl/include/hal/hal_screenshot.h b/ext_components/cp0_lvgl/include/hal/hal_screenshot.h deleted file mode 100644 index 007d1c45..00000000 --- a/ext_components/cp0_lvgl/include/hal/hal_screenshot.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -int hal_screenshot_save(const char *dir); - -#ifdef __cplusplus -} -#endif diff --git a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h index a549729a..850f26a4 100644 --- a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h +++ b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h @@ -33,6 +33,7 @@ const char *hal_path_audio_dir(void); // #include #include "eventpp/callbacklist.h" #include "eventpp/eventqueue.h" +#include #include #include diff --git a/ext_components/cp0_lvgl/include/signal_register_plan.h b/ext_components/cp0_lvgl/include/signal_register_plan.h index 1915a5cf..4c655ac7 100644 --- a/ext_components/cp0_lvgl/include/signal_register_plan.h +++ b/ext_components/cp0_lvgl/include/signal_register_plan.h @@ -2,9 +2,19 @@ def_hal_fun(void(std::string), cp0_signal_audio_play) def_hal_fun(void(bool), cp0_signal_audio_cap) def_hal_fun(void(std::list, std::function), cp0_signal_audio_setup) def_hal_fun(void(std::list, std::function), cp0_signal_audio_api) +def_hal_fun(void(std::list, std::function), cp0_signal_pty_api) +def_hal_fun(void(std::list, std::function), cp0_signal_config_api) +def_hal_fun(void(std::list, std::function), cp0_signal_filesystem_api) +def_hal_fun(void(std::list, std::function), cp0_signal_lora_api) +def_hal_fun(void(std::list, std::function), cp0_signal_wifi_api) +def_hal_fun(void(std::list, std::function), cp0_signal_settings_api) +def_hal_fun(void(std::list, std::function), cp0_signal_process_api) +def_hal_fun(void(std::list, std::function), cp0_signal_osinfo_api) +def_hal_fun(void(std::list, std::function), cp0_signal_bq27220_api) +def_hal_fun(void(std::list, std::function), cp0_signal_imu_api) def_hal_fun(void(), cp0_signal_network) def_hal_fun(void(), cp0_signal_forkexec) -def_hal_fun(void(), cp0_signal_screenshot) +def_hal_fun(void(std::list, std::function), cp0_signal_screenshot_api) def_hal_fun(void(std::function), cp0_signal_battery_pub) def_hal_fun(void(std::list, std::function), cp0_signal_camera_api) def_hal_fun(void(std::string), cp0_signal_system_play) diff --git a/ext_components/cp0_lvgl/src/commount.c b/ext_components/cp0_lvgl/src/commount.c index c85b731d..ed05b953 100644 --- a/ext_components/cp0_lvgl/src/commount.c +++ b/ext_components/cp0_lvgl/src/commount.c @@ -64,17 +64,6 @@ void init_lvgl_env() #endif } -void init_lvgl_saved_settings() -{ - int saved_bright = cp0_config_get_int("brightness", -1); - if (saved_bright > 0) - cp0_backlight_write(saved_bright); - - int saved_vol = cp0_config_get_int("volume", -1); - if (saved_vol >= 0) - cp0_volume_write(saved_vol); -} - void init_lvgl_event() { for (int i = 0; i < CP0_C_EVENT_END; i++) diff --git a/ext_components/cp0_lvgl/src/commount.cpp b/ext_components/cp0_lvgl/src/commount.cpp index 9693be08..054bd194 100644 --- a/ext_components/cp0_lvgl/src/commount.cpp +++ b/ext_components/cp0_lvgl/src/commount.cpp @@ -1,7 +1,10 @@ #include "hal_lvgl_bsp.h" #include "commount.h" +#include "cp0_lvgl_app.h" #include "lvgl/lvgl.h" +#include #include +#include #define def_hal_fun(arg, name) eventpp::CallbackList name; #include "signal_register_plan.h" @@ -19,3 +22,29 @@ extern "C" void init_lvgl_event_cpp() cp0_task_queue.process(); }); t.detach(); } + +static int config_get_int(const char *key, int default_val) +{ + int val = default_val; + cp0_signal_config_api({"GetInt", key ? std::string(key) : std::string(), std::to_string(default_val)}, + [&](int code, std::string data) { + if (code == 0) val = std::atoi(data.c_str()); + }); + return val; +} + +static void saved_volume_write(int val) +{ + cp0_signal_audio_api({"VolumeWrite", std::to_string(val)}, nullptr); +} + +extern "C" void init_lvgl_saved_settings() +{ + int saved_bright = config_get_int("brightness", -1); + if (saved_bright > 0) + cp0_backlight_write(saved_bright); + + int saved_vol = config_get_int("volume", -1); + if (saved_vol >= 0) + saved_volume_write(saved_vol); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp deleted file mode 100644 index 471fa10d..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include - -#define CONFIG_DIR "/var/lib/applaunch" -#define CONFIG_FILE CONFIG_DIR "/settings" -#define MAX_ENTRIES 32 -#define KEY_MAX 64 -#define VAL_MAX 256 - -struct config_entry { - char key[KEY_MAX]; - char val[VAL_MAX]; -}; - -static struct config_entry s_entries[MAX_ENTRIES]; -static int s_count = 0; -static int s_loaded = 0; - -static void ensure_loaded(void) -{ - if (s_loaded) return; - s_loaded = 1; - cp0_config_init(); -} - -void cp0_config_init(void) -{ - s_count = 0; - FILE *fp = fopen(CONFIG_FILE, "r"); - if (!fp) return; - - char line[KEY_MAX + VAL_MAX + 4]; - while (fgets(line, sizeof(line), fp) && s_count < MAX_ENTRIES) { - line[strcspn(line, "\r\n")] = 0; - char *eq = strchr(line, '='); - if (!eq) continue; - *eq = 0; - char *key = line; - char *val = eq + 1; - strncpy(s_entries[s_count].key, key, KEY_MAX - 1); - strncpy(s_entries[s_count].val, val, VAL_MAX - 1); - s_count++; - } - fclose(fp); -} - -static int find_entry(const char *key) -{ - for (int i = 0; i < s_count; i++) { - if (strcmp(s_entries[i].key, key) == 0) - return i; - } - return -1; -} - -int cp0_config_get_int(const char *key, int default_val) -{ - ensure_loaded(); - int idx = find_entry(key); - if (idx < 0) return default_val; - return atoi(s_entries[idx].val); -} - -void cp0_config_set_int(const char *key, int val) -{ - ensure_loaded(); - int idx = find_entry(key); - if (idx < 0) { - if (s_count >= MAX_ENTRIES) return; - idx = s_count++; - strncpy(s_entries[idx].key, key, KEY_MAX - 1); - } - snprintf(s_entries[idx].val, VAL_MAX, "%d", val); -} - -const char *cp0_config_get_str(const char *key, const char *default_val) -{ - ensure_loaded(); - int idx = find_entry(key); - if (idx < 0) return default_val; - return s_entries[idx].val; -} - -void cp0_config_set_str(const char *key, const char *val) -{ - ensure_loaded(); - int idx = find_entry(key); - if (idx < 0) { - if (s_count >= MAX_ENTRIES) return; - idx = s_count++; - strncpy(s_entries[idx].key, key, KEY_MAX - 1); - } - strncpy(s_entries[idx].val, val, VAL_MAX - 1); -} - -void cp0_config_save(void) -{ - mkdir(CONFIG_DIR, 0755); - FILE *fp = fopen(CONFIG_FILE, "w"); - if (!fp) return; - for (int i = 0; i < s_count; i++) - fprintf(fp, "%s=%s\n", s_entries[i].key, s_entries[i].val); - fclose(fp); - sync(); -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp deleted file mode 100644 index 6c5ebbd7..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_filesystem.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include - -int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) -{ - *out_count = 0; - DIR *dir = opendir(path); - if (!dir) return -1; - struct dirent *ent; - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') continue; - if (*out_count >= max_entries) break; - strncpy(entries[*out_count].name, ent->d_name, 255); - entries[*out_count].name[255] = '\0'; - entries[*out_count].is_dir = (ent->d_type == DT_DIR) ? 1 : 0; - (*out_count)++; - } - closedir(dir); - return 0; -} - -struct cp0_dir_watcher { - int inotify_fd; - int watch_fd; -}; - -cp0_watcher_t cp0_dir_watch_start(const char *path) -{ - int fd = inotify_init1(IN_NONBLOCK); - if (fd < 0) return NULL; - int wd = inotify_add_watch(fd, path, IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | IN_MOVED_TO); - if (wd < 0) { close(fd); return NULL; } - struct cp0_dir_watcher *w = (struct cp0_dir_watcher *)malloc(sizeof(struct cp0_dir_watcher)); - w->inotify_fd = fd; - w->watch_fd = wd; - return w; -} - -int cp0_dir_watch_poll(cp0_watcher_t watcher) -{ - if (!watcher) return -1; - char buf[1024] __attribute__((aligned(8))); - struct cp0_dir_watcher *w = (struct cp0_dir_watcher *)watcher; - ssize_t n = read(w->inotify_fd, buf, sizeof(buf)); - return (n > 0) ? 1 : 0; -} - -void cp0_dir_watch_stop(cp0_watcher_t watcher) -{ - if (!watcher) return; - struct cp0_dir_watcher *w = (struct cp0_dir_watcher *)watcher; - close(w->inotify_fd); - free(w); -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp deleted file mode 100644 index 00a11b25..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include - -int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) -{ - *out_count = 0; - struct ifaddrs *ifap = NULL; - if (getifaddrs(&ifap) != 0) return -1; - - for (struct ifaddrs *ifa = ifap; ifa; ifa = ifa->ifa_next) { - if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue; - if (strcmp(ifa->ifa_name, "lo") == 0) continue; - if (*out_count >= max_entries) break; - - cp0_netif_info_t *e = &entries[*out_count]; - strncpy(e->iface, ifa->ifa_name, 31); e->iface[31] = '\0'; - struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; - inet_ntop(AF_INET, &sa->sin_addr, e->ipv4, sizeof(e->ipv4)); - if (ifa->ifa_netmask) { - struct sockaddr_in *nm = (struct sockaddr_in *)ifa->ifa_netmask; - inet_ntop(AF_INET, &nm->sin_addr, e->netmask, sizeof(e->netmask)); - } else { - strcpy(e->netmask, "N/A"); - } - e->is_up = (ifa->ifa_flags & IFF_UP) ? 1 : 0; - (*out_count)++; - } - freeifaddrs(ifap); - return 0; -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp deleted file mode 100644 index f9f065fa..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp +++ /dev/null @@ -1,269 +0,0 @@ -#include "cp0_lvgl_app.h" -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" { - extern void keyboard_pause(void); - extern void keyboard_resume(void); -} - -extern "C" void __attribute__((weak)) keyboard_pause(void) {} -extern "C" void __attribute__((weak)) keyboard_resume(void) {} - -static const char *get_kbd_device() -{ - const char *env = getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); - return env ? env : "/dev/input/by-path/platform-3f804000.i2c-event"; -} - -static const int ESC_HOLD_SEC = 3; - -static bool is_nologin_shell(const char *shell) -{ - if (!shell || !shell[0]) return true; - return strstr(shell, "nologin") != NULL || - strstr(shell, "/bin/false") != NULL; -} - -static const char *get_run_user() -{ - const char *cfg = cp0_config_get_str("run_as_user", NULL); - if (cfg && cfg[0]) return cfg; - - struct passwd *pwd; - setpwent(); - while ((pwd = getpwent()) != NULL) { - if (pwd->pw_uid >= 1000 && pwd->pw_uid < 65534 && - !is_nologin_shell(pwd->pw_shell)) { - endpwent(); - return pwd->pw_name; - } - } - endpwent(); - return "pi"; -} - -static void exec_as_user(const char *exec_path) -{ - const char *user = get_run_user(); - if (getuid() == 0 && strcmp(user, "root") != 0) { - struct passwd *pw = getpwnam(user); - if (pw) { - initgroups(pw->pw_name, pw->pw_gid); - setgid(pw->pw_gid); - setuid(pw->pw_uid); - setenv("HOME", pw->pw_dir, 1); - setenv("USER", pw->pw_name, 1); - setenv("LOGNAME", pw->pw_name, 1); - setenv("SHELL", pw->pw_shell[0] ? pw->pw_shell : "/bin/bash", 1); - chdir(pw->pw_dir); - } - } - execlp("/bin/sh", "sh", "-c", exec_path, (char *)NULL); -} - -/* ------------------------------------------------------------------ - * Experiment: - * - Do NOT EVIOCGRAB the tca8418 evdev while the child is running. - * - Do NOT create a uinput mirror. - * - Child reads /dev/input/event* directly (same physical device); - * multiple readers each receive every input_event on an ungrabbed - * evdev, so both this loop (for ESC-hold detection) and the child - * can see the keys. - * - keyboard_pause() still suspends libinput so APPLauncher's LVGL - * keyboard thread doesn't react while the app is in the foreground. - * ------------------------------------------------------------------ */ -int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, - int keep_root) -{ - (void)home_key_flag; - - keyboard_pause(); - - int evfd = open(get_kbd_device(), O_RDONLY | O_NONBLOCK); - if (evfd < 0) { - perror("[cp0] open evdev"); - keyboard_resume(); - return -1; - } - printf("[cp0] Opened evdev %s (no EVIOCGRAB; shared with child)\n", get_kbd_device()); - fflush(stdout); - - pid_t pid = fork(); - if (pid < 0) { - close(evfd); - keyboard_resume(); - return -1; - } - if (pid == 0) { - close(evfd); - setpgid(0, 0); - if (keep_root) - execlp("/bin/sh", "sh", "-c", exec_path, (char *)NULL); - else - exec_as_user(exec_path); - _exit(127); - } - /* Also set it in the parent in case setpgid races the child. */ - setpgid(pid, pid); - - auto esc_down_since = std::chrono::steady_clock::time_point{}; - bool esc_down = false; - int status = 0; - - while (true) { - int r = waitpid(pid, &status, WNOHANG); - if (r > 0) break; - if (r < 0) { status = -1; break; } - - struct input_event ev; - while (read(evfd, &ev, sizeof(ev)) == (ssize_t)sizeof(ev)) { - if (ev.type == EV_KEY) { - const char *st = (ev.value == 1) ? "DOWN" : - (ev.value == 0) ? "UP" : - (ev.value == 2) ? "REPEAT" : "???"; - printf("[CP0-APP] evdev code=%u value=%d(%s) (shared, child reads too)\n", - ev.code, ev.value, st); - fflush(stdout); - } - if (ev.type == EV_KEY && ev.code == KEY_ESC) { - if (ev.value == 1) { - esc_down = true; - esc_down_since = std::chrono::steady_clock::now(); - printf("[CP0-APP] ESC DOWN\n"); - fflush(stdout); - } else if (ev.value == 0) { - esc_down = false; - printf("[CP0-APP] ESC UP\n"); - fflush(stdout); - } - } - } - - if (esc_down) { - auto held_ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - esc_down_since).count(); - if (held_ms >= ESC_HOLD_SEC * 1000) { - printf("[cp0] ESC held %ldms, SIGTERM pgid %d\n", - (long)held_ms, pid); - fflush(stdout); - /* Kill the whole process group, not just pid, because - * sh -c may have fork'd an inner shell that exec'd the - * real binary as a grandchild. killpg reaches them all - * via the pgid we set with setpgid() above. */ - killpg(pid, SIGTERM); - auto t0 = std::chrono::steady_clock::now(); - while (waitpid(pid, &status, WNOHANG) == 0) { - if (std::chrono::duration_cast( - std::chrono::steady_clock::now() - t0).count() >= 3) { - printf("[cp0] SIGKILL pgid %d\n", pid); - fflush(stdout); - killpg(pid, SIGKILL); - waitpid(pid, &status, 0); - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - break; - } - } - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - close(evfd); - - keyboard_resume(); - - printf("[cp0] Returned to launcher\n"); - fflush(stdout); - if (WIFEXITED(status)) return WEXITSTATUS(status); - return -1; -} - -int cp0_process_check_lock(const char *lock_path, int *holder_pid) -{ - *holder_pid = 0; - int fd = open(lock_path, O_CREAT | O_RDWR, 0666); - if (fd < 0) return -1; - struct flock fl; - memset(&fl, 0, sizeof(fl)); - fl.l_type = F_WRLCK; - fl.l_whence = SEEK_SET; - if (fcntl(fd, F_GETLK, &fl) == -1) { close(fd); return -1; } - close(fd); - if (fl.l_type != F_UNLCK) { - *holder_pid = fl.l_pid; - return fl.l_pid; - } - return 0; -} - -void cp0_process_kill(int pid, int grace_ms) -{ - if (pid <= 0) return; - /* killpg: cp0_process_spawn puts the child in its own pgid, so - * SIGINT/SIGKILL here reaches grandchildren too (sh + exec'd - * binary are typically both inside). */ - killpg(pid, SIGINT); - auto start = std::chrono::steady_clock::now(); - while (true) { - int status; - if (waitpid(pid, &status, WNOHANG) != 0) return; - auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - start).count() >= grace_ms) { - killpg(pid, SIGKILL); - waitpid(pid, &status, 0); - return; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } -} - -cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) -{ - pid_t pid = fork(); - if (pid < 0) return -1; - if (pid == 0) { - setpgid(0, 0); - if (keep_root) - execlp("/bin/sh", "sh", "-c", exec_path, (char *)NULL); - else - exec_as_user(exec_path); - _exit(127); - } - setpgid(pid, pid); - return (cp0_pid_t)pid; -} - -void cp0_process_stop(cp0_pid_t pid) -{ - if (pid <= 0) return; - killpg((pid_t)pid, SIGTERM); - int status; - waitpid((pid_t)pid, &status, WNOHANG); -} - -void cp0_system_shutdown(void) -{ - printf("[CP0] shutdown\n"); - system("sudo shutdown -h now"); -} - -void cp0_system_reboot(void) -{ - printf("[CP0] reboot\n"); - system("sudo reboot"); -} -// rebuild trigger diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp deleted file mode 100644 index 89fa2f89..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp +++ /dev/null @@ -1,127 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct cp0_pty_handle { - int master_fd; - pid_t child_pid; -}; - -cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, - int cols, int rows) -{ - int master_fd; - pid_t pid; - struct winsize ws = {}; - ws.ws_col = cols; - ws.ws_row = rows; - - pid = forkpty(&master_fd, NULL, NULL, &ws); - if (pid < 0) return NULL; - - if (pid == 0) { - setenv("TERM", "vt100", 1); - - // Drop to regular user if running as root - if (getuid() == 0) { - const char *cfg_user = cp0_config_get_str("run_as_user", NULL); - const char *username = NULL; - if (cfg_user && cfg_user[0]) { - username = cfg_user; - } else { - struct passwd *p; - setpwent(); - while ((p = getpwent()) != NULL) { - if (p->pw_uid >= 1000 && p->pw_uid < 65534 && - p->pw_shell && p->pw_shell[0] && - !strstr(p->pw_shell, "nologin") && - !strstr(p->pw_shell, "/bin/false")) { - username = p->pw_name; - break; - } - } - endpwent(); - } - if (!username) username = "pi"; - - struct passwd *pw = getpwnam(username); - if (pw && strcmp(username, "root") != 0) { - initgroups(pw->pw_name, pw->pw_gid); - setgid(pw->pw_gid); - setuid(pw->pw_uid); - setenv("HOME", pw->pw_dir, 1); - setenv("USER", pw->pw_name, 1); - setenv("LOGNAME", pw->pw_name, 1); - setenv("SHELL", pw->pw_shell[0] ? pw->pw_shell : "/bin/bash", 1); - chdir(pw->pw_dir); - } - } - - if (args) - execvp(cmd, (char *const *)args); - else - execlp(cmd, cmd, (char *)NULL); - _exit(127); - } - - int flags = fcntl(master_fd, F_GETFL); - fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); - - struct cp0_pty_handle *pty = (struct cp0_pty_handle *)malloc(sizeof(struct cp0_pty_handle)); - pty->master_fd = master_fd; - pty->child_pid = pid; - return pty; -} - -int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size) -{ - if (!pty) return -1; - struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; - ssize_t n = read(h->master_fd, buf, buf_size); - if (n < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; - return -1; - } - return (int)n; -} - -int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len) -{ - if (!pty) return -1; - struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; - return (int)write(h->master_fd, buf, len); -} - -int cp0_pty_check_child(cp0_pty_t pty, int *exit_status) -{ - if (!pty) return -1; - struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; - int status; - pid_t r = waitpid(h->child_pid, &status, WNOHANG); - if (r == 0) return 0; - if (r > 0) { - if (exit_status) *exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1; - return 1; - } - return -1; -} - -void cp0_pty_close(cp0_pty_t pty) -{ - if (!pty) return; - struct cp0_pty_handle *h = (struct cp0_pty_handle *)pty; - kill(h->child_pid, SIGKILL); - waitpid(h->child_pid, NULL, 0); - close(h->master_fd); - free(h); -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp deleted file mode 100644 index 78a006ba..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp +++ /dev/null @@ -1,127 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static void write_le16(FILE *f, uint16_t v) { fwrite(&v, 2, 1, f); } -static void write_le32(FILE *f, uint32_t v) { fwrite(&v, 4, 1, f); } - -int cp0_screenshot_save(const char *dir) -{ - const char *fbdev = getenv("APPLAUNCH_LINUX_FBDEV_DEVICE"); - if (!fbdev) fbdev = "/dev/fb0"; - - int fd = open(fbdev, O_RDONLY); - if (fd < 0) return -1; - - struct fb_var_screeninfo vinfo; - if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) < 0) { - close(fd); - return -2; - } - - int w = vinfo.xres; - int h = vinfo.yres; - int bpp = vinfo.bits_per_pixel; - int fb_line_len = w * (bpp / 8); - - struct fb_fix_screeninfo finfo; - if (ioctl(fd, FBIOGET_FSCREENINFO, &finfo) == 0) - fb_line_len = finfo.line_length; - - size_t fb_size = fb_line_len * h; - void *fbmem = mmap(NULL, fb_size, PROT_READ, MAP_SHARED, fd, 0); - if (fbmem == MAP_FAILED) { - close(fd); - return -3; - } - - { - struct stat st; - if (stat(dir, &st) != 0) { - char tmp[512]; - snprintf(tmp, sizeof(tmp), "%s", dir); - for (char *p = tmp + 1; *p; ++p) { - if (*p == '/') { *p = 0; mkdir(tmp, 0755); *p = '/'; } - } - mkdir(tmp, 0755); - } - } - - time_t now = time(NULL); - struct tm *t = localtime(&now); - char filename[512]; - snprintf(filename, sizeof(filename), "%s/scr_%04d%02d%02d_%02d%02d%02d.bmp", - dir, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, - t->tm_hour, t->tm_min, t->tm_sec); - - FILE *fp = fopen(filename, "wb"); - if (!fp) { - munmap(fbmem, fb_size); - close(fd); - return -4; - } - - int row_size = w * 3; - int pad = (4 - (row_size % 4)) % 4; - int bmp_row = row_size + pad; - uint32_t img_size = bmp_row * h; - uint32_t file_size = 54 + img_size; - - // BMP header - fputc('B', fp); fputc('M', fp); - write_le32(fp, file_size); - write_le16(fp, 0); write_le16(fp, 0); - write_le32(fp, 54); - // DIB header - write_le32(fp, 40); - write_le32(fp, w); - write_le32(fp, h); - write_le16(fp, 1); - write_le16(fp, 24); - write_le32(fp, 0); - write_le32(fp, img_size); - write_le32(fp, 2835); write_le32(fp, 2835); - write_le32(fp, 0); write_le32(fp, 0); - - uint8_t padding[3] = {0}; - for (int y = h - 1; y >= 0; --y) { - uint8_t *row = (uint8_t *)fbmem + y * fb_line_len; - for (int x = 0; x < w; ++x) { - uint8_t r, g, b; - if (bpp == 16) { - uint16_t px = ((uint16_t *)row)[x]; - // RGB565 - r = ((px >> 11) & 0x1F) << 3; - g = ((px >> 5) & 0x3F) << 2; - b = (px & 0x1F) << 3; - } else if (bpp == 32) { - uint32_t px = ((uint32_t *)row)[x]; - r = (px >> vinfo.red.offset) & 0xFF; - g = (px >> vinfo.green.offset) & 0xFF; - b = (px >> vinfo.blue.offset) & 0xFF; - } else { - r = g = b = 0; - } - // BMP stores BGR - uint8_t bgr[3] = {b, g, r}; - fwrite(bgr, 1, 3, fp); - } - if (pad > 0) fwrite(padding, 1, pad, fp); - } - - fclose(fp); - munmap(fbmem, fb_size); - close(fd); - - printf("[SCREENSHOT] Saved: %s (%dx%d %dbpp)\n", filename, w, h, bpp); - return 0; -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp deleted file mode 100644 index 7b429b23..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp +++ /dev/null @@ -1,449 +0,0 @@ -#include "cp0_lvgl_app.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static int bq27220_read_word(int fd, unsigned char reg) -{ - unsigned char buf[2] = {0}; - struct i2c_msg msgs[2]; - struct i2c_rdwr_ioctl_data data; - - msgs[0].addr = 0x55; - msgs[0].flags = 0; - msgs[0].len = 1; - msgs[0].buf = ® - msgs[1].addr = 0x55; - msgs[1].flags = I2C_M_RD; - msgs[1].len = 2; - msgs[1].buf = buf; - data.msgs = msgs; - data.nmsgs = 2; - - if (ioctl(fd, I2C_RDWR, &data) < 0) return -1; - return buf[0] | (buf[1] << 8); -} - -static int bqmon_read_long(const char *path, long *value) -{ - if (!path || !value) return 0; - FILE *fp = fopen(path, "r"); - if (!fp) return 0; - long v = 0; - int ret = fscanf(fp, "%ld", &v); - fclose(fp); - if (ret != 1) return 0; - *value = v; - return 1; -} - -static int bqmon_read_string(const char *path, char *buf, size_t len) -{ - if (!path || !buf || len == 0) return 0; - FILE *fp = fopen(path, "r"); - if (!fp) return 0; - if (!fgets(buf, len, fp)) { - fclose(fp); - return 0; - } - fclose(fp); - size_t n = strlen(buf); - while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) - buf[--n] = 0; - return 1; -} - -static double bqmon_current_ma(long current_now) -{ - return -(current_now / 1000.0); -} - -static double bqmon_temp_c(long temp) -{ - double c = temp / 10.0; - if (c > 100.0 || c < -40.0) - c = temp / 100.0; - return c; -} - -static int bqmon_has_file(const char *dir, const char *name) -{ - char path[320]; - snprintf(path, sizeof(path), "%s/%s", dir, name); - return access(path, R_OK) == 0; -} - -static int bqmon_find_power_supply(char *out, size_t out_len) -{ - const char *base = "/sys/class/power_supply"; - DIR *dp = opendir(base); - if (!dp) return 0; - - char fallback[320] = {0}; - struct dirent *ent = NULL; - while ((ent = readdir(dp)) != NULL) { - if (ent->d_name[0] == '.') continue; - - char dir[320]; - snprintf(dir, sizeof(dir), "%s/%s", base, ent->d_name); - if (!bqmon_has_file(dir, "capacity") || - !bqmon_has_file(dir, "voltage_now") || - !bqmon_has_file(dir, "current_now") || - !bqmon_has_file(dir, "temp") || - !bqmon_has_file(dir, "status")) - continue; - - if (strstr(ent->d_name, "bq27220") || strstr(ent->d_name, "bq27")) { - snprintf(out, out_len, "%s", dir); - closedir(dp); - return 1; - } - if (fallback[0] == 0) - snprintf(fallback, sizeof(fallback), "%s", dir); - } - - closedir(dp); - if (fallback[0]) { - snprintf(out, out_len, "%s", fallback); - return 1; - } - return 0; -} - -cp0_battery_info_t cp0_battery_read(void) -{ - cp0_battery_info_t info; - memset(&info, 0, sizeof(info)); - - char bq_path[256] = {0}; - long capacity = 0, voltage_uv = 0, current_raw = 0, temp_raw = 0; - char status[64] = "Unknown"; - - if (bqmon_find_power_supply(bq_path, sizeof(bq_path))) { - char path[320]; - snprintf(path, sizeof(path), "%s/capacity", bq_path); - int ok = bqmon_read_long(path, &capacity); - snprintf(path, sizeof(path), "%s/voltage_now", bq_path); - ok = ok && bqmon_read_long(path, &voltage_uv); - snprintf(path, sizeof(path), "%s/current_now", bq_path); - ok = ok && bqmon_read_long(path, ¤t_raw); - snprintf(path, sizeof(path), "%s/temp", bq_path); - ok = ok && bqmon_read_long(path, &temp_raw); - snprintf(path, sizeof(path), "%s/status", bq_path); - bqmon_read_string(path, status, sizeof(status)); - - if (ok) { - double current_ma = bqmon_current_ma(current_raw); - double temp_c = bqmon_temp_c(temp_raw); - - info.soc = (int)capacity; - info.voltage_mv = (int)(voltage_uv / 1000); - info.current_ma = (int)(current_ma >= 0 ? current_ma + 0.5 : current_ma - 0.5); - info.avg_current_ma = info.current_ma; - info.temperature_c10 = (int)(temp_c >= 0 ? temp_c * 10.0 + 0.5 : temp_c * 10.0 - 0.5); - info.flags = (strcmp(status, "Charging") == 0) ? 1 : 0; - info.valid = 1; - return info; - } - } - - int fd = open("/dev/i2c-1", O_RDWR); - if (fd < 0) return info; - - int v; - v = bq27220_read_word(fd, 0x08); if (v >= 0) info.voltage_mv = v; - v = bq27220_read_word(fd, 0x0C); if (v >= 0) info.current_ma = (v > 32767) ? v - 65536 : v; - v = bq27220_read_word(fd, 0x06); if (v >= 0) info.temperature_c10 = v - 2731; - v = bq27220_read_word(fd, 0x2C); if (v >= 0) info.soc = v; - v = bq27220_read_word(fd, 0x10); if (v >= 0) info.remain_mah = v; - v = bq27220_read_word(fd, 0x12); if (v >= 0) info.full_mah = v; - v = bq27220_read_word(fd, 0x0E); if (v >= 0) info.flags = v; - v = bq27220_read_word(fd, 0x14); if (v >= 0) info.avg_current_ma = (v > 32767) ? v - 65536 : v; - - info.valid = 1; - close(fd); - return info; -} - -int cp0_backlight_read(void) -{ - FILE *f = fopen("/sys/class/backlight/backlight/brightness", "r"); - if (!f) return -1; - int val = 0; - if (fscanf(f, "%d", &val) != 1) val = -1; - fclose(f); - return val; -} - -int cp0_backlight_max(void) -{ - FILE *f = fopen("/sys/class/backlight/backlight/max_brightness", "r"); - if (!f) return 100; - int val = 100; - if (fscanf(f, "%d", &val) != 1) val = 100; - fclose(f); - return val; -} - -int cp0_backlight_write(int val) -{ - if (val < 0) val = 0; - int mx = cp0_backlight_max(); - if (val > mx) val = mx; - FILE *f = fopen("/sys/class/backlight/backlight/brightness", "w"); - if (!f) return -1; - fprintf(f, "%d", val); - fclose(f); - return val; -} - -// ── Async WiFi status: background thread polls nmcli, main thread reads cache ── -#include - -static cp0_wifi_status_t s_wifi_cache; -static pthread_mutex_t s_wifi_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_t s_wifi_thread; -static int s_wifi_thread_running = 0; - -static void wifi_poll_once(cp0_wifi_status_t *out) -{ - cp0_wifi_status_t st; - memset(&st, 0, sizeof(st)); - char line[256]; - - FILE *p = popen("nmcli -t -f TYPE,CONNECTION dev status 2>/dev/null", "r"); - if (!p) { *out = st; return; } - while (fgets(line, sizeof(line), p)) { - line[strcspn(line, "\n")] = 0; - if (strncmp(line, "wifi:", 5) == 0) { - char *name = line + 5; - if (name[0] && strcmp(name, "--") != 0) { - st.connected = 1; - strncpy(st.ssid, name, CP0_WIFI_SSID_MAX - 1); - } - break; - } - } - pclose(p); - - if (st.connected) { - p = popen("nmcli -t -f IN-USE,SIGNAL dev wifi list --rescan no 2>/dev/null", "r"); - if (p) { - while (fgets(line, sizeof(line), p)) { - line[strcspn(line, "\n")] = 0; - if (line[0] == '*' && line[1] == ':') { - st.signal = atoi(line + 2); - break; - } - } - pclose(p); - } - - p = popen("ip -4 -o addr show wlan0 2>/dev/null", "r"); - if (p) { - if (fgets(line, sizeof(line), p)) { - char *inet = strstr(line, "inet "); - if (inet) { - inet += 5; - char *sl = strchr(inet, '/'); - if (sl) *sl = 0; - char *sp = strchr(inet, ' '); - if (sp) *sp = 0; - strncpy(st.ip, inet, sizeof(st.ip) - 1); - } - } - pclose(p); - } - } - *out = st; -} - -static void *wifi_poll_thread(void *arg) -{ - (void)arg; - while (1) { - cp0_wifi_status_t st; - wifi_poll_once(&st); - pthread_mutex_lock(&s_wifi_mutex); - s_wifi_cache = st; - pthread_mutex_unlock(&s_wifi_mutex); - usleep(3000000); // poll every 3s - } - return NULL; -} - -static void ensure_wifi_thread(void) -{ - if (!s_wifi_thread_running) { - s_wifi_thread_running = 1; - pthread_create(&s_wifi_thread, NULL, wifi_poll_thread, NULL); - pthread_detach(s_wifi_thread); - } -} - -cp0_wifi_status_t cp0_wifi_get_status(void) -{ - ensure_wifi_thread(); - cp0_wifi_status_t st; - pthread_mutex_lock(&s_wifi_mutex); - st = s_wifi_cache; - pthread_mutex_unlock(&s_wifi_mutex); - return st; -} - -int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps) -{ - system("nmcli dev wifi rescan 2>/dev/null"); - usleep(500000); - FILE *p = popen("nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list 2>/dev/null", "r"); - if (!p) return 0; - char line[512]; - int count = 0; - while (fgets(line, sizeof(line), p) && count < max_aps) { - line[strcspn(line, "\n")] = 0; - if (line[0] == 0) continue; - cp0_wifi_ap_t tmp; - memset(&tmp, 0, sizeof(tmp)); - char *ptr = line; - char *last_colon = strrchr(ptr, ':'); - if (!last_colon) continue; - tmp.in_use = (*(last_colon + 1) == '*') ? 1 : 0; - *last_colon = 0; - char *sec_colon = strrchr(ptr, ':'); - if (!sec_colon) continue; - strncpy(tmp.security, sec_colon + 1, sizeof(tmp.security) - 1); - *sec_colon = 0; - char *sig_colon = strrchr(ptr, ':'); - if (!sig_colon) continue; - tmp.signal = atoi(sig_colon + 1); - *sig_colon = 0; - if (ptr[0] == 0) continue; - strncpy(tmp.ssid, ptr, CP0_WIFI_SSID_MAX - 1); - - /* Dedup: if same SSID already exists, keep the stronger signal, - * but always preserve in_use flag (the connected AP might not be - * the strongest among same-SSID roaming APs). */ - int dup_idx = -1; - for (int i = 0; i < count; i++) { - if (strcmp(out[i].ssid, tmp.ssid) == 0) { - dup_idx = i; - break; - } - } - if (dup_idx >= 0) { - if (tmp.in_use) out[dup_idx].in_use = 1; - if (tmp.signal > out[dup_idx].signal) { - int saved_in_use = out[dup_idx].in_use; - out[dup_idx] = tmp; - out[dup_idx].in_use = saved_in_use; - } - } else { - out[count] = tmp; - count++; - } - } - pclose(p); - return count; -} - -int cp0_wifi_connect(const char *ssid, const char *password) -{ - char cmd[512]; - if (password && password[0]) - snprintf(cmd, sizeof(cmd), "nmcli dev wifi connect '%s' password '%s' 2>&1", ssid, password); - else - snprintf(cmd, sizeof(cmd), "nmcli con up id '%s' 2>&1", ssid); - FILE *p = popen(cmd, "r"); - if (!p) return -1; - char buf[256]; int ok = 0; - while (fgets(buf, sizeof(buf), p)) { if (strstr(buf, "successfully")) ok = 1; } - pclose(p); - return ok ? 0 : -1; -} - -int cp0_wifi_disconnect(void) -{ - // Use "nmcli con down" (deactivate connection) rather than "nmcli dev - // disconnect" (which marks the device unmanaged and prevents autoconnect - // until reboot). With "con down", NM may re-autoconnect another profile - // shortly after — that's usually what the user wants. - FILE *p = popen("nmcli con down id \"$(nmcli -t -f NAME con show --active | grep -v lo | head -1)\" 2>&1", "r"); - if (!p) return -1; - char buf[256]; int ok = 0; - while (fgets(buf, sizeof(buf), p)) { if (strstr(buf, "successfully")) ok = 1; } - pclose(p); - return ok ? 0 : -1; -} - -cp0_bt_status_t cp0_bt_get_status(void) -{ - cp0_bt_status_t st; - memset(&st, 0, sizeof(st)); - FILE *p = popen("bluetoothctl show 2>/dev/null", "r"); - if (!p) return st; - char line[256]; - while (fgets(line, sizeof(line), p)) { - if (strstr(line, "Powered:")) st.powered = strstr(line, "yes") ? 1 : 0; - char *addr = strstr(line, "Controller "); - if (addr) { addr += 11; char *sp = strchr(addr, ' '); if (sp) *sp = 0; addr[strcspn(addr, "\n")] = 0; strncpy(st.address, addr, sizeof(st.address) - 1); } - } - pclose(p); - return st; -} - -int cp0_bt_set_power(int on) -{ - FILE *p = popen(on ? "bluetoothctl power on 2>/dev/null" : "bluetoothctl power off 2>/dev/null", "r"); - if (!p) return -1; - char buf[128]; int ok = 0; - while (fgets(buf, sizeof(buf), p)) { if (strstr(buf, "succeeded") || strstr(buf, "Changing")) ok = 1; } - pclose(p); - return ok ? 0 : -1; -} - -int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) -{ - system("bluetoothctl scan on 2>/dev/null &"); - usleep(4000000); - system("bluetoothctl scan off 2>/dev/null"); - - FILE *p = popen("bluetoothctl devices 2>/dev/null", "r"); - if (!p) return 0; - - char line[256]; - int count = 0; - while (fgets(line, sizeof(line), p) && count < max_devices) { - line[strcspn(line, "\n")] = 0; - // Format: "Device XX:XX:XX:XX:XX:XX Name" - if (strncmp(line, "Device ", 7) != 0) continue; - char *addr = line + 7; - char *sp = strchr(addr, ' '); - if (!sp) continue; - *sp = 0; - char *name = sp + 1; - - cp0_bt_device_t *dev = &out[count]; - memset(dev, 0, sizeof(*dev)); - strncpy(dev->address, addr, sizeof(dev->address) - 1); - strncpy(dev->name, name[0] ? name : addr, CP0_BT_NAME_MAX - 1); - dev->rssi = 0; - dev->connected = 0; - count++; - } - pclose(p); - return count; -} - -void cp0_time_str(char *buf, int buf_size) -{ - time_t now = time(NULL); - struct tm *t = localtime(&now); - snprintf(buf, buf_size, "%02d:%02d", t->tm_hour, t->tm_min); -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c index 3d7ab8e0..4ebc08c6 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c @@ -6,9 +6,20 @@ void cp0_lvgl_init(void) { init_lvgl_env(); init_lvgl_event(); + init_filesystem(); + init_config(); + init_pty(); init_freambuffer_disp(); init_input(); init_audio(); + init_process(); + init_osinfo(); + init_screenshot(); + init_lora(); + init_wifi(); + init_settings(); + init_bq27220(); + init_imu(); init_lvgl_saved_settings(); init_battery(); init_camera(); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h index 6fe9d025..55b53426 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h @@ -6,7 +6,18 @@ extern "C" { void init_freambuffer_disp(); void init_input(); +void init_filesystem(void); void init_audio(); +void init_pty(void); +void init_config(void); +void init_process(void); +void init_screenshot(void); +void init_lora(void); +void init_wifi(); +void init_settings(void); +void init_osinfo(void); +void init_bq27220(void); +void init_imu(void); void init_battery(); void init_camera(void); #ifdef __cplusplus diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp deleted file mode 100644 index 5642fb6c..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_app.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "cp0_lvgl_app.h" -#include "hal_lvgl_bsp.h" - -#include -#include -#include -#include - -extern "C" { - -void cp0_signal_audio_api_play_file(const char *path) -{ - if (path && path[0]) { - cp0_signal_audio_api({"PlayFile", std::string(path)}, nullptr); - } -} - -void cp0_signal_audio_api_play_asset(const char *name) -{ - if (name && name[0]) { - cp0_signal_audio_api({"Play", std::string(name)}, nullptr); - } -} - -void cp0_signal_system_play_asset(const char *name) -{ - if (name && name[0]) { - cp0_signal_system_play(std::string(name)); - } -} - -int cp0_volume_read(void) -{ - int volume = -1; - cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) { - if (code == 0) { - volume = std::atoi(data.c_str()); - } - }); - return volume; -} - -int cp0_volume_write(int val) -{ - int volume = -1; - cp0_signal_audio_api({"VolumeWrite", std::to_string(val)}, [&](int code, std::string data) { - if (code == 0) { - volume = std::atoi(data.c_str()); - } - }); - return volume; -} - -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index 4a8bef45..d250867b 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -750,3 +750,4 @@ extern "C" void init_audio(void) { audio->system_play(name); }); } + diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_bq27220.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_bq27220.cpp new file mode 100644 index 00000000..6beadcd7 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_bq27220.cpp @@ -0,0 +1,350 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() +#include +#endif + +#if __has_include() && __has_include() +#include +#include +#define CP0_HAS_LINUX_I2C_RDWR 1 +#else +#define CP0_HAS_LINUX_I2C_RDWR 0 +#endif + +namespace { + +class Bq27220System +{ +public: + using arg_t = std::list; + using callback_t = std::function; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "Read") { + cp0_battery_info_t info = read(); + report(callback, info.valid ? 0 : -1, encode(info)); + } else if (cmd == "Calibrate") { + const int index = arg.size() >= 2 ? std::atoi(nth_arg(arg, 1).c_str()) : -1; + report(callback, calibrate(index), ""); + } else { + report(callback, -1, "unknown bq27220 api command"); + } + } + + cp0_battery_info_t read() + { + cp0_battery_info_t info{}; + + char bq_path[256] = {0}; + long capacity = 0, voltage_uv = 0, current_raw = 0, temp_raw = 0; + char status[64] = "Unknown"; + + if (find_power_supply(bq_path, sizeof(bq_path))) { + char path[320]; + std::snprintf(path, sizeof(path), "%s/capacity", bq_path); + int ok = read_long(path, &capacity); + std::snprintf(path, sizeof(path), "%s/voltage_now", bq_path); + ok = ok && read_long(path, &voltage_uv); + std::snprintf(path, sizeof(path), "%s/current_now", bq_path); + ok = ok && read_long(path, ¤t_raw); + std::snprintf(path, sizeof(path), "%s/temp", bq_path); + ok = ok && read_long(path, &temp_raw); + std::snprintf(path, sizeof(path), "%s/status", bq_path); + read_string(path, status, sizeof(status)); + + if (ok) { + const double current_ma = -(current_raw / 1000.0); + double temp_c = temp_raw / 10.0; + if (temp_c > 100.0 || temp_c < -40.0) { + temp_c = temp_raw / 100.0; + } + + info.soc = static_cast(capacity); + info.voltage_mv = static_cast(voltage_uv / 1000); + info.current_ma = round_to_int(current_ma); + info.avg_current_ma = info.current_ma; + info.temperature_c10 = round_to_int(temp_c * 10.0); + info.flags = (std::strcmp(status, "Charging") == 0) ? 1 : 0; + info.valid = 1; + return info; + } + } + +#if CP0_HAS_LINUX_I2C_RDWR + int fd = open(kI2cDev, O_RDWR); + if (fd < 0) { + return info; + } + + int v; + v = read_word(fd, 0x08); if (v >= 0) info.voltage_mv = v; + v = read_word(fd, 0x0C); if (v >= 0) info.current_ma = (v > 32767) ? v - 65536 : v; + v = read_word(fd, 0x06); if (v >= 0) info.temperature_c10 = v - 2731; + v = read_word(fd, 0x2C); if (v >= 0) info.soc = v; + v = read_word(fd, 0x10); if (v >= 0) info.remain_mah = v; + v = read_word(fd, 0x12); if (v >= 0) info.full_mah = v; + v = read_word(fd, 0x0E); if (v >= 0) info.flags = v; + v = read_word(fd, 0x14); if (v >= 0) info.avg_current_ma = (v > 32767) ? v - 65536 : v; + + info.valid = 1; + close(fd); +#endif + return info; + } + + int calibrate(int command_index) + { +#if CP0_HAS_LINUX_I2C_RDWR + static const int cmds[] = {0x0081, 0x000A, 0x0009, 0x0080}; + if (command_index < 0 || command_index >= static_cast(sizeof(cmds) / sizeof(cmds[0]))) { + return -1; + } + + int fd = open(kI2cDev, O_RDWR); + if (fd < 0) { + return -errno; + } + + struct i2c_msg msg; + struct i2c_rdwr_ioctl_data data; + unsigned char buf[3] = {0x00, + static_cast(cmds[command_index] & 0xFF), + static_cast((cmds[command_index] >> 8) & 0xFF)}; + msg.addr = kI2cAddr; + msg.flags = 0; + msg.len = 3; + msg.buf = buf; + data.msgs = &msg; + data.nmsgs = 1; + + int ret = ioctl(fd, I2C_RDWR, &data); + int saved_errno = errno; + close(fd); + return ret == 0 ? 0 : -saved_errno; +#else + (void)command_index; + return -1; +#endif + } + +private: + static constexpr const char *kI2cDev = "/dev/i2c-1"; + static constexpr int kI2cAddr = 0x55; + + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) { + callback(code, data); + } + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + std::advance(it, static_cast(std::min(index, arg.size()))); + return it == arg.end() ? std::string() : *it; + } + + static int round_to_int(double value) + { + return static_cast(value >= 0 ? value + 0.5 : value - 0.5); + } + + static std::string encode(const cp0_battery_info_t &info) + { + std::ostringstream os; + os << info.voltage_mv << ',' + << info.current_ma << ',' + << info.temperature_c10 << ',' + << info.soc << ',' + << info.remain_mah << ',' + << info.full_mah << ',' + << info.flags << ',' + << info.avg_current_ma << ',' + << info.valid; + return os.str(); + } + + static bool decode(const std::string &data, cp0_battery_info_t *info) + { + if (!info) { + return false; + } + cp0_battery_info_t parsed{}; + char comma; + std::istringstream is(data); + if (is >> parsed.voltage_mv >> comma && + is >> parsed.current_ma >> comma && + is >> parsed.temperature_c10 >> comma && + is >> parsed.soc >> comma && + is >> parsed.remain_mah >> comma && + is >> parsed.full_mah >> comma && + is >> parsed.flags >> comma && + is >> parsed.avg_current_ma >> comma && + is >> parsed.valid) { + *info = parsed; + return true; + } + return false; + } + + static int read_long(const char *path, long *value) + { + if (!path || !value) return 0; + FILE *fp = std::fopen(path, "r"); + if (!fp) return 0; + long v = 0; + int ret = std::fscanf(fp, "%ld", &v); + std::fclose(fp); + if (ret != 1) return 0; + *value = v; + return 1; + } + + static int read_string(const char *path, char *buf, size_t len) + { + if (!path || !buf || len == 0) return 0; + FILE *fp = std::fopen(path, "r"); + if (!fp) return 0; + if (!std::fgets(buf, static_cast(len), fp)) { + std::fclose(fp); + return 0; + } + std::fclose(fp); + size_t n = std::strlen(buf); + while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) { + buf[--n] = 0; + } + return 1; + } + + static int has_file(const char *dir, const char *name) + { + char path[320]; + std::snprintf(path, sizeof(path), "%s/%s", dir, name); + return access(path, R_OK) == 0; + } + + static int find_power_supply(char *out, size_t out_len) + { + const char *base = "/sys/class/power_supply"; + DIR *dp = opendir(base); + if (!dp) return 0; + + char fallback[320] = {0}; + struct dirent *ent = nullptr; + while ((ent = readdir(dp)) != nullptr) { + if (ent->d_name[0] == '.') continue; + + char dir[320]; + std::snprintf(dir, sizeof(dir), "%s/%s", base, ent->d_name); + if (!has_file(dir, "capacity") || + !has_file(dir, "voltage_now") || + !has_file(dir, "current_now") || + !has_file(dir, "temp") || + !has_file(dir, "status")) { + continue; + } + + if (std::strstr(ent->d_name, "bq27220") || std::strstr(ent->d_name, "bq27")) { + std::snprintf(out, out_len, "%s", dir); + closedir(dp); + return 1; + } + if (fallback[0] == 0) { + std::snprintf(fallback, sizeof(fallback), "%s", dir); + } + } + + closedir(dp); + if (fallback[0]) { + std::snprintf(out, out_len, "%s", fallback); + return 1; + } + return 0; + } + +#if CP0_HAS_LINUX_I2C_RDWR + static int read_word(int fd, unsigned char reg) + { + unsigned char buf[2] = {0}; + struct i2c_msg msgs[2]; + struct i2c_rdwr_ioctl_data data; + + msgs[0].addr = kI2cAddr; + msgs[0].flags = 0; + msgs[0].len = 1; + msgs[0].buf = ® + msgs[1].addr = kI2cAddr; + msgs[1].flags = I2C_M_RD; + msgs[1].len = 2; + msgs[1].buf = buf; + data.msgs = msgs; + data.nmsgs = 2; + + if (ioctl(fd, I2C_RDWR, &data) < 0) return -1; + return buf[0] | (buf[1] << 8); + } +#endif + +public: + static bool decode_info(const std::string &data, cp0_battery_info_t *info) + { + return decode(data, info); + } +}; + +} // namespace + +extern "C" { + +cp0_battery_info_t cp0_battery_read(void) +{ + cp0_battery_info_t info{}; + cp0_signal_bq27220_api({"Read"}, [&](int code, std::string data) { + if (code == 0) { + Bq27220System::decode_info(data, &info); + } + }); + return info; +} + +int cp0_bq27220_calibrate(int command_index) +{ + int ret = -1; + cp0_signal_bq27220_api({"Calibrate", std::to_string(command_index)}, [&](int code, std::string data) { + (void)data; + ret = code; + }); + return ret; +} + +void init_bq27220(void) +{ + std::shared_ptr bq27220 = std::make_shared(); + cp0_signal_bq27220_api.append([bq27220](std::list arg, std::function callback) { + bq27220->api_call(std::move(arg), std::move(callback)); + }); +} + +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp new file mode 100644 index 00000000..d7fac308 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp @@ -0,0 +1,221 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONFIG_DIR "/var/lib/applaunch" +#define CONFIG_FILE CONFIG_DIR "/settings" +#define MAX_ENTRIES 32 +#define KEY_MAX 64 +#define VAL_MAX 256 + +class ConfigSystem +{ +public: + typedef std::function callback_t; + typedef std::list arg_t; + + ConfigSystem() + { + init(); + } + void init() + { + std::lock_guard lock(mutex_); + load_locked(); + } + + int get_int(const char *key, int default_val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = find_entry_locked(key); + if (idx < 0) return default_val; + return std::atoi(entries_[idx].val); + } + + void set_int(const char *key, int val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = get_or_create_entry_locked(key); + if (idx < 0) return; + std::snprintf(entries_[idx].val, VAL_MAX, "%d", val); + } + + const char *get_str(const char *key, const char *default_val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = find_entry_locked(key); + if (idx < 0) return default_val; + return entries_[idx].val; + } + + void set_str(const char *key, const char *val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = get_or_create_entry_locked(key); + if (idx < 0) return; + copy_cstr(entries_[idx].val, val ? val : "", VAL_MAX); + } + + void save() + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + mkdir(CONFIG_DIR, 0755); + FILE *fp = std::fopen(CONFIG_FILE, "w"); + if (!fp) return; + for (int i = 0; i < count_; i++) { + std::fprintf(fp, "%s=%s\n", entries_[i].key, entries_[i].val); + } + std::fclose(fp); + sync(); + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty config api\n"); + return; + } + + const std::string cmd = arg.front(); + if (cmd == "Init") { + init(); + report(callback, 0, "ok"); + } else if (cmd == "GetInt") { + const std::string key = nth_arg(arg, 1); + int default_val = parse_int(nth_arg(arg, 2), 0); + int val = get_int(key.c_str(), default_val); + report(callback, 0, std::to_string(val)); + } else if (cmd == "SetInt") { + const std::string key = nth_arg(arg, 1); + int val = parse_int(nth_arg(arg, 2), 0); + set_int(key.c_str(), val); + report(callback, 0, "ok"); + } else if (cmd == "GetStr") { + const std::string key = nth_arg(arg, 1); + const std::string default_val = nth_arg(arg, 2); + report(callback, 0, get_str(key.c_str(), default_val.c_str())); + } else if (cmd == "SetStr") { + const std::string key = nth_arg(arg, 1); + const std::string val = nth_arg(arg, 2); + set_str(key.c_str(), val.c_str()); + report(callback, 0, "ok"); + } else if (cmd == "Save") { + save(); + report(callback, 0, "ok"); + } else { + report(callback, -1, "unknown config api\n"); + } + } + +private: + struct Entry { + char key[KEY_MAX]; + char val[VAL_MAX]; + }; + + Entry entries_[MAX_ENTRIES] = {}; + int count_ = 0; + bool loaded_ = false; + std::mutex mutex_; + + void ensure_loaded_locked() + { + if (!loaded_) load_locked(); + } + + void load_locked() + { + count_ = 0; + loaded_ = true; + + FILE *fp = std::fopen(CONFIG_FILE, "r"); + if (!fp) return; + + char line[KEY_MAX + VAL_MAX + 4]; + while (std::fgets(line, sizeof(line), fp) && count_ < MAX_ENTRIES) { + line[std::strcspn(line, "\r\n")] = 0; + char *eq = std::strchr(line, '='); + if (!eq) continue; + *eq = 0; + if (line[0] == '\0') continue; + copy_cstr(entries_[count_].key, line, KEY_MAX); + copy_cstr(entries_[count_].val, eq + 1, VAL_MAX); + count_++; + } + std::fclose(fp); + } + + int find_entry_locked(const char *key) const + { + if (!key || key[0] == '\0') return -1; + for (int i = 0; i < count_; i++) { + if (std::strcmp(entries_[i].key, key) == 0) return i; + } + return -1; + } + + int get_or_create_entry_locked(const char *key) + { + int idx = find_entry_locked(key); + if (idx >= 0) return idx; + if (!key || key[0] == '\0' || count_ >= MAX_ENTRIES) return -1; + idx = count_++; + copy_cstr(entries_[idx].key, key, KEY_MAX); + entries_[idx].val[0] = '\0'; + return idx; + } + + static void copy_cstr(char *dst, const char *src, size_t dst_size) + { + if (!dst || dst_size == 0) return; + if (!src) src = ""; + std::strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; + } + + static std::string nth_arg(const arg_t& arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); i++) ++it; + return it == arg.end() ? std::string() : *it; + } + + static int parse_int(const std::string& value, int default_val) + { + if (value.empty()) return default_val; + return std::atoi(value.c_str()); + } + + static void report(callback_t callback, int code, const std::string& data) + { + if (callback) callback(code, data); + } +}; + +extern "C" { + +void init_config(void) +{ + std::shared_ptr config = std::make_shared(); + cp0_signal_config_api.append([config](std::list arg, std::function callback) { + config->api_call(std::move(arg), std::move(callback)); + }); +} + +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp deleted file mode 100644 index f046c96b..00000000 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "cp0_lvgl_app.h" -#include "cp0_lvgl_file.hpp" - -#include -#include -#include -#include - -namespace { -constexpr const char *kAppRoot = "/usr/share/APPLaunch"; - -std::string lower_ext(const std::string &file) -{ - const auto dot = file.find_last_of('.'); - if (dot == std::string::npos) { - return ""; - } - - std::string ext = file.substr(dot + 1); - std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - return ext; -} - -bool is_image_ext(const std::string &ext) -{ - return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg" || ext == "svg"; -} - -bool is_audio_ext(const std::string &ext) -{ - return ext == "wav" || ext == "mp3" || ext == "ogg"; -} - -bool is_font_ext(const std::string &ext) -{ - return ext == "ttf" || ext == "otf"; -} - -} // namespace - -std::string cp0_file_path(std::string file) -{ - if (file.empty()) { - return ""; - } - - if (file == "applications") { - return std::string(kAppRoot) + "/applications"; - } - if (file == "lock_file") { - return "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; - } - if (file == "keyboard_device") { - return "/dev/input/by-path/platform-3f804000.i2c-event"; - } - if (file == "keyboard_map") { - return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; - } - if (file == "store_sync_cmd") { - return std::string("python ") + kAppRoot + "/bin/store_cache_sync.py"; - } - - const std::string ext = lower_ext(file); - if (is_image_ext(ext)) { - return "share/images/" + file; - } - if (is_audio_ext(ext)) { - return std::string(kAppRoot) + "/share/audio/" + file; - } - if (is_font_ext(ext)) { - return std::string(kAppRoot) + "/share/font/" + file; - } - - return file; -} - -extern "C" const char *cp0_file_path_c(const char *file) -{ - static thread_local std::unordered_map paths; - std::string key = file ? std::string(file) : std::string(); - auto it = paths.find(key); - if (it == paths.end()) { - it = paths.emplace(key, cp0_file_path(key)).first; - } - return it->second.c_str(); -} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_filesystem.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_filesystem.cpp new file mode 100644 index 00000000..a208ccde --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_filesystem.cpp @@ -0,0 +1,405 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +class Cp0Filesystem { +public: + void api_call(const std::list &arg, std::function callback) + { + auto report = [&](int code, const std::string &data) { + if (callback) callback(code, data); + }; + + if (arg.empty()) { + report(-1, "missing command"); + return; + } + + const std::string &cmd = arg.front(); + auto value_arg = [&]() -> std::string { + return arg.size() >= 2 ? *std::next(arg.begin()) : std::string(); + }; + + if (cmd == "Path") { + report(0, resolve_path(value_arg())); + } else if (cmd == "DirList") { + std::string data; + report(encode_dir_entries(value_arg().c_str(), data), data); + } else if (cmd == "WatchStart") { + cp0_watcher_t watcher = watch_start(value_arg().c_str()); + report(watcher ? 0 : -1, std::to_string(reinterpret_cast(watcher))); + } else if (cmd == "WatchPoll") { + auto *watcher = reinterpret_cast(parse_uintptr(value_arg())); + report(0, std::to_string(watch_poll(watcher))); + } else if (cmd == "WatchStop") { + auto *watcher = reinterpret_cast(parse_uintptr(value_arg())); + watch_stop(watcher); + report(0, ""); + } else { + report(-2, "unknown command: " + cmd); + } + } + + std::string path(std::string file) + { + int code = -1; + std::string data; + invoke({"Path", std::move(file)}, code, data); + return code == 0 ? data : std::string(); + } + + int dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) + { + int code = -1; + std::string data; + invoke({"DirList", path ? path : ""}, code, data); + if (code != 0) return code; + return decode_dir_entries(data, entries, max_entries, out_count); + } + + cp0_watcher_t dir_watch_start(const char *path) + { + int code = -1; + std::string data; + invoke({"WatchStart", path ? path : ""}, code, data); + return code == 0 ? reinterpret_cast(parse_uintptr(data)) : nullptr; + } + + int dir_watch_poll(cp0_watcher_t watcher) + { + int code = -1; + std::string data; + invoke({"WatchPoll", std::to_string(reinterpret_cast(watcher))}, code, data); + return code == 0 ? std::atoi(data.c_str()) : code; + } + + void dir_watch_stop(cp0_watcher_t watcher) + { + int code = -1; + std::string data; + invoke({"WatchStop", std::to_string(reinterpret_cast(watcher))}, code, data); + } + +private: + struct Watcher { + int inotify_fd; + int watch_fd; + }; + + static constexpr const char *kAppRoot = "/usr/share/APPLaunch"; + static constexpr const char *kLvglFsRoot = "A:/"; + + void invoke(std::list arg, int &code, std::string &data) + { + api_call(arg, [&](int c, std::string d) { + code = c; + data = std::move(d); + }); + } + + static uintptr_t parse_uintptr(const std::string &value) + { + return static_cast(std::strtoull(value.c_str(), nullptr, 0)); + } + + static std::string lower_ext(const std::string &file) + { + const auto dot = file.find_last_of('.'); + if (dot == std::string::npos) return ""; + + std::string ext = file.substr(dot + 1); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return ext; + } + + static bool is_image_ext(const std::string &ext) + { + return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg" || ext == "svg"; + } + + static bool is_audio_ext(const std::string &ext) + { + return ext == "wav" || ext == "mp3" || ext == "ogg"; + } + + static bool is_font_ext(const std::string &ext) + { + return ext == "ttf" || ext == "otf"; + } + + static bool has_lvgl_drive(const std::string &file) + { + return file.size() >= 2 && std::isalpha(static_cast(file[0])) && file[1] == ':'; + } + + static bool starts_with(const std::string &value, const char *prefix) + { + const std::string prefix_str(prefix); + return value.compare(0, prefix_str.size(), prefix_str) == 0; + } + + static std::string strip_app_root_prefix(const std::string &file) + { + const std::string app_root_prefix = std::string(kAppRoot) + "/"; + if (starts_with(file, app_root_prefix.c_str())) return file.substr(app_root_prefix.size()); + if (starts_with(file, "APPLaunch/")) return file.substr(std::strlen("APPLaunch/")); + return file; + } + +#ifdef HAL_PLATFORM_SDL + static bool path_exists(const std::string &path) + { + return access(path.c_str(), F_OK) == 0; + } + + static const std::string &sdl_app_root() + { + static const std::string root = []() { + const char *candidates[] = { + "APPLaunch", + "dist/APPLaunch", + "../APPLaunch", + "../dist/APPLaunch", + "projects/APPLaunch/APPLaunch", + "projects/APPLaunch/dist/APPLaunch", + }; + for (const char *candidate : candidates) { + if (path_exists(std::string(candidate) + "/share/images")) return std::string(candidate); + } + return std::string("APPLaunch"); + }(); + return root; + } + + static std::string sdl_resource_path(const char *dir, const std::string &file) + { + std::string rel = strip_app_root_prefix(file); + const std::string dir_prefix = std::string(dir) + "/"; + if (starts_with(rel, dir_prefix.c_str())) return sdl_app_root() + "/" + rel; + if (!rel.empty() && rel.front() == '/') return rel; + return sdl_app_root() + "/" + dir_prefix + rel; + } +#endif + + static std::string lvgl_root_path(std::string rel) + { + while (!rel.empty() && rel.front() == '/') { + rel.erase(rel.begin()); + } + return std::string(kLvglFsRoot) + rel; + } + + static std::string resolve_lvgl_image_path(const std::string &file) + { +#ifdef HAL_PLATFORM_SDL + return sdl_resource_path("share/images", file); +#else + if (has_lvgl_drive(file)) return file; + + const std::string rel = strip_app_root_prefix(file); + + if (!rel.empty() && rel.front() == '/') return rel; + if (starts_with(rel, "share/images/")) return lvgl_root_path(rel); + + return lvgl_root_path("share/images/" + rel); +#endif + } + + static std::string resolve_path(const std::string &file) + { + if (file.empty()) return ""; + +#ifdef HAL_PLATFORM_SDL + if (file == "applications") return sdl_app_root() + "/applications"; + if (file == "lock_file") return "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; + if (file == "keyboard_device") return ""; + if (file == "keyboard_map") return ""; + if (file == "store_sync_cmd") return std::string("python ") + sdl_app_root() + "/bin/store_cache_sync.py"; +#else + if (file == "applications") return std::string(kAppRoot) + "/applications"; + if (file == "lock_file") return "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; + if (file == "keyboard_device") return "/dev/input/by-path/platform-3f804000.i2c-event"; + if (file == "keyboard_map") return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; + if (file == "store_sync_cmd") return std::string("python ") + kAppRoot + "/bin/store_cache_sync.py"; +#endif + + const std::string ext = lower_ext(file); + if (is_image_ext(ext)) return resolve_lvgl_image_path(file); +#ifdef HAL_PLATFORM_SDL + if (is_audio_ext(ext)) return sdl_resource_path("share/audio", file); + if (is_font_ext(ext)) return sdl_resource_path("share/font", file); +#else + if (is_audio_ext(ext)) return std::string(kAppRoot) + "/share/audio/" + file; + if (is_font_ext(ext)) return std::string(kAppRoot) + "/share/font/" + file; +#endif + + return file; + } + + static int list_dir(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) + { + if (out_count) *out_count = 0; + if (!path || !out_count) return -1; + if (!entries || max_entries <= 0) return 0; + + DIR *dir = opendir(path); + if (!dir) return -1; + + struct dirent *ent; + while ((ent = readdir(dir)) != nullptr) { + if (ent->d_name[0] == '.') continue; + if (*out_count >= max_entries) break; + std::strncpy(entries[*out_count].name, ent->d_name, sizeof(entries[*out_count].name) - 1); + entries[*out_count].name[sizeof(entries[*out_count].name) - 1] = '\0'; + entries[*out_count].is_dir = (ent->d_type == DT_DIR) ? 1 : 0; + (*out_count)++; + } + + closedir(dir); + return 0; + } + + static int encode_dir_entries(const char *path, std::string &data) + { + cp0_dirent_t entries[512]; + int count = 0; + if (list_dir(path, entries, 512, &count) != 0) return -1; + + std::ostringstream out; + for (int i = 0; i < count; ++i) { + out << (entries[i].is_dir ? 'D' : 'F') << '\t' << entries[i].name << '\n'; + } + data = out.str(); + return 0; + } + + static int decode_dir_entries(const std::string &data, cp0_dirent_t *entries, int max_entries, int *out_count) + { + if (out_count) *out_count = 0; + if (!entries || max_entries <= 0 || !out_count) return 0; + + size_t start = 0; + while (start < data.size() && *out_count < max_entries) { + size_t end = data.find('\n', start); + std::string line = data.substr(start, end == std::string::npos ? std::string::npos : end - start); + if (line.size() >= 3 && line[1] == '\t') { + entries[*out_count].is_dir = (line[0] == 'D') ? 1 : 0; + std::strncpy(entries[*out_count].name, line.c_str() + 2, sizeof(entries[*out_count].name) - 1); + entries[*out_count].name[sizeof(entries[*out_count].name) - 1] = '\0'; + (*out_count)++; + } + if (end == std::string::npos) break; + start = end + 1; + } + return 0; + } + + static cp0_watcher_t watch_start(const char *path) + { + if (!path) return nullptr; + + int fd = inotify_init1(IN_NONBLOCK); + if (fd < 0) return nullptr; + + int wd = inotify_add_watch(fd, path, IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | IN_MOVED_TO); + if (wd < 0) { + close(fd); + return nullptr; + } + + auto *watcher = static_cast(std::malloc(sizeof(Watcher))); + if (!watcher) { + close(fd); + return nullptr; + } + watcher->inotify_fd = fd; + watcher->watch_fd = wd; + return watcher; + } + + static int watch_poll(cp0_watcher_t watcher) + { + if (!watcher) return -1; + + auto *w = static_cast(watcher); + char buf[1024] __attribute__((aligned(8))); + ssize_t n = read(w->inotify_fd, buf, sizeof(buf)); + return (n > 0) ? 1 : 0; + } + + static void watch_stop(cp0_watcher_t watcher) + { + if (!watcher) return; + + auto *w = static_cast(watcher); + if (w->watch_fd >= 0) inotify_rm_watch(w->inotify_fd, w->watch_fd); + close(w->inotify_fd); + std::free(w); + } +}; + +Cp0Filesystem &filesystem() +{ + static Cp0Filesystem instance; + return instance; +} +} // namespace + +std::string cp0_file_path(std::string file) +{ + return filesystem().path(std::move(file)); +} + +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::unordered_map paths; + std::string key = file ? std::string(file) : std::string(); + auto it = paths.find(key); + if (it == paths.end()) it = paths.emplace(key, cp0_file_path(key)).first; + return it->second.c_str(); +} + +extern "C" int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) +{ + return filesystem().dir_list(path, entries, max_entries, out_count); +} + +extern "C" cp0_watcher_t cp0_dir_watch_start(const char *path) +{ + return filesystem().dir_watch_start(path); +} + +extern "C" int cp0_dir_watch_poll(cp0_watcher_t watcher) +{ + return filesystem().dir_watch_poll(watcher); +} + +extern "C" void cp0_dir_watch_stop(cp0_watcher_t watcher) +{ + filesystem().dir_watch_stop(watcher); +} + +extern "C" void init_filesystem(void) +{ + cp0_signal_filesystem_api.append([](std::list arg, std::function callback) { + filesystem().api_call(arg, std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp new file mode 100644 index 00000000..46b88a3f --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp @@ -0,0 +1,243 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" +#include "../cp0_app_internal_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#endif + +namespace { + +struct IioDevicePaths { + std::string accel; + std::string magn; + bool has_gyro = false; + + bool ready() const { return !accel.empty() && !magn.empty(); } +}; + +void clear_info(cp0_compass_info_t *info, const char *status, int ready) +{ + if (!info) + return; + std::memset(info, 0, sizeof(*info)); + cp0_copy_cstr(info->status, sizeof(info->status), status ? status : "IMU"); + info->sensor_ready = ready; +} + +#if !defined(_WIN32) +bool file_exists(const std::string &path) +{ + struct stat st; + return stat(path.c_str(), &st) == 0; +} + +bool read_float_file(const std::string &path, float &out) +{ + std::ifstream ifs(path); + if (!ifs.is_open()) + return false; + ifs >> out; + return !ifs.fail(); +} + +float read_float_file_or(const std::string &path, float fallback) +{ + float value = fallback; + return read_float_file(path, value) ? value : fallback; +} + +bool has_accel_files(const std::string &dir) +{ + return file_exists(dir + "/in_accel_x_raw") && + file_exists(dir + "/in_accel_y_raw") && + file_exists(dir + "/in_accel_z_raw"); +} + +bool has_magn_files(const std::string &dir) +{ + return file_exists(dir + "/in_magn_x_raw") && + file_exists(dir + "/in_magn_y_raw") && + file_exists(dir + "/in_magn_z_raw"); +} + +bool has_gyro_files(const std::string &dir) +{ + return file_exists(dir + "/in_anglvel_x_raw") && + file_exists(dir + "/in_anglvel_y_raw") && + file_exists(dir + "/in_anglvel_z_raw"); +} + +IioDevicePaths enumerate_iio_devices() +{ + static constexpr const char *kIioRoot = "/sys/bus/iio/devices"; + IioDevicePaths paths; + + DIR *dp = opendir(kIioRoot); + if (!dp) + return paths; + + while (dirent *ent = readdir(dp)) { + if (std::strncmp(ent->d_name, "iio:device", 10) != 0) + continue; + + std::string dir = std::string(kIioRoot) + "/" + ent->d_name; + if (paths.accel.empty() && has_accel_files(dir)) { + paths.accel = dir; + paths.has_gyro = has_gyro_files(dir); + } + if (paths.magn.empty() && has_magn_files(dir)) + paths.magn = dir; + } + + closedir(dp); + return paths; +} + +bool read_axis_triplet(const std::string &dir, const char *prefix, + float scale, float &x, float &y, float &z) +{ + float rx = 0.0f; + float ry = 0.0f; + float rz = 0.0f; + if (!read_float_file(dir + "/" + prefix + "_x_raw", rx)) return false; + if (!read_float_file(dir + "/" + prefix + "_y_raw", ry)) return false; + if (!read_float_file(dir + "/" + prefix + "_z_raw", rz)) return false; + + x = rx * scale; + y = ry * scale; + z = rz * scale; + return true; +} +#endif + +class ImuSystem { +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty imu api\n"); + return; + } + + if (arg.front() == "Read") { + read(arg, callback); + return; + } + + report(callback, -1, "unknown imu api\n"); + } + +private: + void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + void read(arg_t arg, callback_t callback) + { + (void)arg; + cp0_compass_info_t info{}; + int ret = read_info(&info); + report(callback, ret, std::string(reinterpret_cast(&info), sizeof(info))); + } + + int read_info(cp0_compass_info_t *info) + { + if (!info) + return -1; + clear_info(info, "IIO sensor missing", 0); + +#if defined(_WIN32) + return -1; +#else + IioDevicePaths paths = enumerate_iio_devices(); + if (!paths.ready()) + return -1; + + const float acc_scale = read_float_file_or(paths.accel + "/in_accel_scale", 1.0f); + const float gyr_scale = read_float_file_or(paths.accel + "/in_anglvel_scale", 1.0f); + const float mag_scale = read_float_file_or(paths.magn + "/in_magn_scale", 1.0f); + + float acc_x = 0.0f, acc_y = 0.0f, acc_z = 0.0f; + float mag_x = 0.0f, mag_y = 0.0f, mag_z = 0.0f; + float gyr_x = 0.0f, gyr_y = 0.0f, gyr_z = 0.0f; + + if (!read_axis_triplet(paths.accel, "in_accel", acc_scale, acc_x, acc_y, acc_z) || + !read_axis_triplet(paths.magn, "in_magn", mag_scale, mag_x, mag_y, mag_z)) { + clear_info(info, "IIO read failed", 0); + return -1; + } + + if (paths.has_gyro) + read_axis_triplet(paths.accel, "in_anglvel", gyr_scale, gyr_x, gyr_y, gyr_z); + + float pitch = std::atan2(-acc_x, std::sqrt(acc_y * acc_y + acc_z * acc_z)); + float roll = std::atan2(acc_y, acc_z); + float sin_p = std::sin(pitch); + float cos_p = std::cos(pitch); + float sin_r = std::sin(roll); + float cos_r = std::cos(roll); + + float mag_x_h = mag_x * cos_p + mag_z * sin_p; + float mag_y_h = mag_x * sin_r * sin_p + mag_y * cos_r - mag_z * sin_r * cos_p; + float yaw = std::atan2(-mag_y_h, mag_x_h) * 180.0f / 3.1415926f; + if (yaw < 0.0f) + yaw += 360.0f; + + clear_info(info, "Sensor OK", 1); + info->yaw = yaw; + info->pitch = pitch * 180.0f / 3.1415926f; + info->roll = roll * 180.0f / 3.1415926f; + info->acc_x = acc_x; + info->acc_y = acc_y; + info->acc_z = acc_z; + info->gyr_x = gyr_x; + info->gyr_y = gyr_y; + info->gyr_z = gyr_z; + info->mag_x = mag_x; + info->mag_y = mag_y; + info->mag_z = mag_z; + return 0; +#endif + } +}; + +} // namespace + +extern "C" int cp0_compass_read(cp0_compass_read_cb_t callback, void *user) +{ + cp0_compass_info_t info{}; + clear_info(&info, "IMU unavailable", 0); + int ret = -1; + cp0_signal_imu_api({"Read"}, [&](int code, std::string data) { + ret = code; + if (data.size() == sizeof(info)) + std::memcpy(&info, data.data(), sizeof(info)); + }); + if (callback) + callback(ret, &info, user); + return ret; +} + +extern "C" void init_imu(void) +{ + std::shared_ptr imu = std::make_shared(); + cp0_signal_imu_api.append([imu](std::list arg, std::function callback) { + imu->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_lara.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_lara.cpp new file mode 100644 index 00000000..6e81c06e --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_lara.cpp @@ -0,0 +1,1425 @@ +#include "cp0_lvgl_app.h" +#include "../cp0_app_internal_utils.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SLOGI +#define SLOGI(fmt, ...) std::printf("[cp0_lora] " fmt "\n", ##__VA_ARGS__) +#endif + +#if __has_include() +#include +#define HAS_LINUX_GPIO_CDEV 1 +#else +#define HAS_LINUX_GPIO_CDEV 0 +#endif + +#if __has_include() && __has_include() +#include +#include +#else +extern "C" int ioctl(int fd, unsigned long request, ...); +struct spi_ioc_transfer { + unsigned long tx_buf; + unsigned long rx_buf; + uint32_t len; + uint32_t speed_hz; + uint16_t delay_usecs; + uint8_t bits_per_word; + uint8_t cs_change; + uint32_t pad; +}; +#ifndef SPI_MODE_0 +#define SPI_MODE_0 0 +#endif +#ifndef SPI_IOC_WR_MODE +#define SPI_IOC_WR_MODE 0 +#endif +#ifndef SPI_IOC_WR_BITS_PER_WORD +#define SPI_IOC_WR_BITS_PER_WORD 0 +#endif +#ifndef SPI_IOC_WR_MAX_SPEED_HZ +#define SPI_IOC_WR_MAX_SPEED_HZ 0 +#endif +#ifndef SPI_IOC_MESSAGE +#define SPI_IOC_MESSAGE(N) 0 +#endif +#endif + +#if __has_include() +#include +#define CP0_LORA_HAS_LINUX_I2CDEV 1 +#else +#define CP0_LORA_HAS_LINUX_I2CDEV 0 +#ifndef I2C_SLAVE +#define I2C_SLAVE 0x0703 +#endif +#endif + +#include "RadioLib.h" +#if __has_include() +#include "hal/RPi/PiHal.h" +#else +class PiHal : public RadioLibHal { + public: + PiHal(uint8_t spiChannel, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) + : RadioLibHal(0, 1, 0, 1, 1, 2), + _gpioDevice(gpioDevice), + _spiDevice(spiDevice), + _spiSpeed(spiSpeed), + _spiChannel(spiChannel) {} + + protected: + uint8_t _gpioDevice; + uint8_t _spiDevice; + uint32_t _spiSpeed; + uint8_t _spiChannel; +}; +#endif + +namespace cp0_lora_backend { +// ============================================================ +// Hardware configuration and state +// ============================================================ +static int g_spi_fd = -1; +static bool g_lora_tx_mode = false; +static bool g_lora_selected_tx_mode = false; +static bool g_lora_tx_in_progress = false; +static bool g_lora_pending_rx_after_tx = false; +static uint64_t g_lora_last_auto_tx_ms = 0; +static char g_spi_device[64] = "/dev/spidev0.1"; +static unsigned int g_spi_speed = 1000000; +static int g_lora_sck_gpio = 11; +static int g_lora_mosi_gpio = 10; +static int g_lora_miso_gpio = 9; +static int g_lora_power_gpio = 5; +static int g_lora_nss_gpio = 7; +static bool g_lora_nss_manual = false; +static int g_lora_rst_gpio = 26; +static int g_lora_irq_gpio = 23; +static int g_lora_busy_gpio = 22; +static int g_lora_rst_fd = -1; +static int g_lora_busy_fd = -1; +static int g_lora_irq_fd = -1; +static int g_lora_nss_fd = -1; +static volatile bool g_lora_initialized = false; +static bool g_lora_hw_ready = false; +static bool g_lora_irq_poll_fallback = true; +static volatile bool g_lora_rx_done = false; +static volatile bool g_lora_tx_done = false; +static uint32_t g_lora_tx_counter = 0; +static uint64_t g_lora_tx_start_ms = 0; +static uint64_t g_lora_sent_popup_until_ms = 0; +static char g_lora_last_rx[128] = {0}; +static char g_lora_last_tx[128] = "Hello from M5 LoRa-1262"; +static char g_lora_tx_input[128] = ""; +static bool g_lora_has_sent_message = false; +static float g_lora_last_rssi = 0.0f; +static float g_lora_last_snr = 0.0f; +static const char *g_lora_cfg_freq = "869.525024MHz"; +static const char *g_lora_cfg_bw = "250kHz"; +static const char *g_lora_cfg_sf = "SF7"; +static const char *g_lora_cfg_cr = "4/5"; +static const char *g_lora_cfg_sync = "0x34"; +static const char *g_lora_cfg_preamble = "20"; +static const char *g_lora_cfg_power = "10dBm"; +static const char *g_lora_cfg_tcxo = "0.0V(disabled)"; +static char g_lora_last_diag[256] = "idle"; +static char g_lora_probe_summary[256] = "probe not started"; +static char g_lora_probe_display[128] = "SPI: probing..."; +static const int g_pi4io_i2c_bus = 1; +static const int g_pi4io_sda_gpio = 2; +static const int g_pi4io_scl_gpio = 3; +static const uint8_t g_pi4io_i2c_addr = 0x43; +static bool g_pi4io_found = false; +static bool g_pi4io_initialized = false; +static char g_pi4io_status[160] = "I2C 0x43 not checked"; +static uint8_t g_pi4io_output_cache = 0x00; +static uint8_t g_pi4io_config_cache = 0xFF; +static uint8_t g_pi4io_polarity_cache = 0x00; +static int g_hat_5vout_fd = -1; +static int g_hat_5vout_offset = 5; +static char g_hat_5vout_chip[64] = ""; +static int g_hat_5vout_last_sysfs_ret = -999; +static int g_hat_5vout_last_value = -1; +static bool g_hat_5vout_last_cdev_ok = false; + +// Forward declarations +static uint64_t get_monotonic_ms(void); +static bool lora_spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len); +static int gpio_set_value(int gpio, int value); +#if HAS_LINUX_GPIO_CDEV +static bool gpio_open_output_line(const char *chip_path, int offset, int value, int *line_fd); +static bool gpio_set_output_line_value(int line_fd, int value); +#endif +static int gpio_init_output_any(const char *chip_env_name, const char *offset_env_name, int gpio, int value, int *line_fd, const char *line_name); +static int gpio_init_input_any(const char *chip_env_name, const char *offset_env_name, int gpio, int *line_fd, const char *line_name); +static int gpio_get_value_any(int gpio, int line_fd); +static int gpio_set_value_any(int gpio, int line_fd, int value); +static size_t collect_spi_candidates(char out[][64], size_t max_count, const char *preferred); +static void resolve_lora_spi_device(void); +static bool probe_lora_spi_device(void); +static bool hat_5vout_enable(void); +static bool hat_5vout_prepare_line(void); +static void lora_update_power_debug(const char *stage, int sysfs_ret, int gpio_value, bool cdev_ok); +static bool pi4io_scan_and_init_before_lora(void); +static bool pi4io_open_bus(int *fd); +static bool pi4io_select_device(int fd); +static bool pi4io_write_reg(int fd, uint8_t reg, uint8_t value); +static bool pi4io_probe_device(int fd); +static bool pi4io_init_device(int fd); +static void lora_apply_mode(bool tx_mode); +static void lora_start_receive_mode(void); +static void lora_send_demo_packet(void); +static void lora_service_irq_once(void); +static void lora_check_tx_fallback(void); +static void lora_set_diag_step(const char *step, int code, const char *detail); +static void lora_refresh_status(const char *prefix); +static const char *lora_radiolib_status_text(int16_t state); +static bool lora_send_text_packet(const char *payload); +static void lora_init_hardware(void); + + +// ============================================================ +// GPIO / SPI / I2C low level (ported from UserDemo) +// ============================================================ + +static int write_text_file(const char *path, const char *value) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return -1; + ssize_t ret = write(fd, value, strlen(value)); + close(fd); + return ret < 0 ? -1 : 0; +} + +static int gpio_export_if_needed(int gpio) +{ + char path[64]; + snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); + if (access(path, F_OK) == 0) return 0; + char gpio_str[16]; + snprintf(gpio_str, sizeof(gpio_str), "%d", gpio); + if (write_text_file("/sys/class/gpio/export", gpio_str) < 0 && errno != EBUSY) { + return -1; + } + usleep(100000); + return 0; +} + +static int gpio_set_direction(int gpio, const char *direction) +{ + char path[64]; + snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", gpio); + return write_text_file(path, direction); +} + +static int gpio_init_input(int gpio) +{ + return gpio_export_if_needed(gpio) < 0 || gpio_set_direction(gpio, "in") < 0 ? -1 : 0; +} + +static int gpio_open_value_fd(int gpio) +{ + char path[64]; + snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); + return open(path, O_RDONLY | O_NONBLOCK); +} + +static int gpio_init_input_irq_sysfs(int gpio, int *line_fd) +{ + if (line_fd == NULL) return -1; + if (gpio_init_input(gpio) < 0) return -1; + char edge_path[64]; + snprintf(edge_path, sizeof(edge_path), "/sys/class/gpio/gpio%d/edge", gpio); + if (write_text_file(edge_path, "rising") < 0) return -1; + int fd = gpio_open_value_fd(gpio); + if (fd < 0) return -1; + char dummy = 0; + lseek(fd, 0, SEEK_SET); + (void)read(fd, &dummy, 1); + *line_fd = fd; + return 0; +} + +static int gpio_init_output(int gpio, int value) +{ + if (gpio_export_if_needed(gpio) < 0) return -1; + if (value) { + if (gpio_set_direction(gpio, "high") == 0) return 0; + } else { + if (gpio_set_direction(gpio, "low") == 0) return 0; + } + if (gpio_set_direction(gpio, "out") < 0) return -1; + return gpio_set_value(gpio, value); +} + +static int gpio_get_value(int gpio) +{ + char path[64]; + char value = '0'; + snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + ssize_t ret = read(fd, &value, 1); + close(fd); + if (ret <= 0) return -1; + return value == '0' ? 0 : 1; +} + +static int gpio_set_value(int gpio, int value) +{ + char path[64]; + snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); + return write_text_file(path, value ? "1" : "0"); +} + +#if HAS_LINUX_GPIO_CDEV +static bool gpio_open_input_line(const char *chip_path, int offset, int *line_fd) +{ + if (chip_path == NULL || line_fd == NULL) return false; + int chip_fd = open(chip_path, O_RDONLY); + if (chip_fd < 0) return false; + struct gpiohandle_request req; + memset(&req, 0, sizeof(req)); + req.lines = 1; + req.lineoffsets[0] = (uint32_t)offset; + req.flags = GPIOHANDLE_REQUEST_INPUT; + snprintf(req.consumer_label, sizeof(req.consumer_label), "applaunch-lora-in"); + if (ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) { + close(chip_fd); + return false; + } + close(chip_fd); + *line_fd = req.fd; + return true; +} + +static bool gpio_get_input_line_value(int line_fd, int *value) +{ + if (line_fd < 0 || value == NULL) return false; + struct gpiohandle_data data; + memset(&data, 0, sizeof(data)); + if (ioctl(line_fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data) < 0) return false; + *value = data.values[0] ? 1 : 0; + return true; +} + +static bool gpio_open_input_event_line(const char *chip_path, int offset, int *line_fd) +{ + if (chip_path == NULL || line_fd == NULL) return false; + int chip_fd = open(chip_path, O_RDONLY); + if (chip_fd < 0) return false; + struct gpioevent_request req; + memset(&req, 0, sizeof(req)); + req.lineoffset = (uint32_t)offset; + req.handleflags = GPIOHANDLE_REQUEST_INPUT; + req.eventflags = GPIOEVENT_REQUEST_RISING_EDGE; + snprintf(req.consumer_label, sizeof(req.consumer_label), "applaunch-lora-irq"); + if (ioctl(chip_fd, GPIO_GET_LINEEVENT_IOCTL, &req) < 0) { + close(chip_fd); + return false; + } + close(chip_fd); + *line_fd = req.fd; + (void)fcntl(*line_fd, F_SETFL, fcntl(*line_fd, F_GETFL, 0) | O_NONBLOCK); + return true; +} + +static bool gpio_line_name_matches(const char *name) +{ + static const char *candidates[] = { + "G5_HAT_5VOUT_EN", "HAT_5VOUT_EN", "PG5", "G5", + }; + if (name == NULL || name[0] == '\0') return false; + for (size_t i = 0; i < sizeof(candidates)/sizeof(candidates[0]); ++i) { + if (strcmp(name, candidates[i]) == 0) return true; + } + return false; +} + +static bool gpio_find_named_line(char *chip_path, size_t chip_path_size, int *offset) +{ + if (chip_path == NULL || chip_path_size == 0 || offset == NULL) return false; + for (int chip_index = 0; chip_index < 8; ++chip_index) { + char path[64]; + snprintf(path, sizeof(path), "/dev/gpiochip%d", chip_index); + int chip_fd = open(path, O_RDONLY); + if (chip_fd < 0) continue; + struct gpiochip_info chip_info; + memset(&chip_info, 0, sizeof(chip_info)); + if (ioctl(chip_fd, GPIO_GET_CHIPINFO_IOCTL, &chip_info) < 0) { + close(chip_fd); continue; + } + for (int line = 0; line < (int)chip_info.lines; ++line) { + struct gpioline_info line_info; + memset(&line_info, 0, sizeof(line_info)); + line_info.line_offset = line; + if (ioctl(chip_fd, GPIO_GET_LINEINFO_IOCTL, &line_info) < 0) continue; + if (gpio_line_name_matches(line_info.name) || gpio_line_name_matches(line_info.consumer)) { + snprintf(chip_path, chip_path_size, "%s", path); + *offset = line; + close(chip_fd); + return true; + } + } + close(chip_fd); + } + return false; +} + +static bool gpio_open_output_line(const char *chip_path, int offset, int value, int *line_fd) +{ + if (chip_path == NULL || line_fd == NULL) return false; + int chip_fd = open(chip_path, O_RDONLY); + if (chip_fd < 0) return false; + struct gpiohandle_request req; + memset(&req, 0, sizeof(req)); + req.lines = 1; + req.lineoffsets[0] = (uint32_t)offset; + req.flags = GPIOHANDLE_REQUEST_OUTPUT; + req.default_values[0] = (uint8_t)(value ? 1 : 0); + snprintf(req.consumer_label, sizeof(req.consumer_label), "applaunch-lora-5v"); + if (ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) { + close(chip_fd); + return false; + } + close(chip_fd); + *line_fd = req.fd; + return true; +} + +static bool gpio_set_output_line_value(int line_fd, int value) +{ + if (line_fd < 0) return false; + struct gpiohandle_data data; + memset(&data, 0, sizeof(data)); + data.values[0] = (uint8_t)(value ? 1 : 0); + return ioctl(line_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data) == 0; +} +#endif + +static int gpio_init_output_any(const char *chip_env_name, const char *offset_env_name, int gpio, int value, int *line_fd, const char *line_name) +{ + if (line_fd && *line_fd >= 0) return 0; +#if HAS_LINUX_GPIO_CDEV + const char *chip_env = getenv(chip_env_name); + const char *offset_env = getenv(offset_env_name); + char chip_path[64] = "/dev/gpiochip0"; + int offset = gpio; + if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); + if (offset_env && offset_env[0]) offset = atoi(offset_env); + if (line_fd && gpio_open_output_line(chip_path, offset, value, line_fd)) { + SLOGI("LoRa GPIO %s via cdev: %s[%d]=%d", line_name ? line_name : "out", chip_path, offset, value); + return 0; + } +#endif + if (gpio_init_output(gpio, value) == 0) return 0; + SLOGI("LoRa GPIO %s init failed: gpio=%d errno=%d", line_name ? line_name : "out", gpio, errno); + return -1; +} + +static int gpio_init_input_any(const char *chip_env_name, const char *offset_env_name, int gpio, int *line_fd, const char *line_name) +{ + if (line_fd && *line_fd >= 0) return 0; +#if HAS_LINUX_GPIO_CDEV + const char *chip_env = getenv(chip_env_name); + const char *offset_env = getenv(offset_env_name); + char chip_path[64] = "/dev/gpiochip0"; + int offset = gpio; + if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); + if (offset_env && offset_env[0]) offset = atoi(offset_env); + if (line_fd && gpio_open_input_line(chip_path, offset, line_fd)) { + SLOGI("LoRa GPIO %s via cdev: %s[%d]", line_name ? line_name : "in", chip_path, offset); + return 0; + } +#endif + if (gpio_init_input(gpio) == 0) return 0; + SLOGI("LoRa GPIO %s input init failed: gpio=%d errno=%d", line_name ? line_name : "in", gpio, errno); + return -1; +} + +static int gpio_init_input_irq_any(const char *chip_env_name, const char *offset_env_name, int gpio, int *line_fd, const char *line_name) +{ + if (line_fd && *line_fd >= 0) return 0; +#if HAS_LINUX_GPIO_CDEV + const char *chip_env = getenv(chip_env_name); + const char *offset_env = getenv(offset_env_name); + char chip_path[64] = "/dev/gpiochip0"; + int offset = gpio; + if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); + if (offset_env && offset_env[0]) offset = atoi(offset_env); + if (line_fd && gpio_open_input_event_line(chip_path, offset, line_fd)) { + SLOGI("LoRa GPIO %s irq-event via cdev: %s[%d]", line_name ? line_name : "irq", chip_path, offset); + return 0; + } +#endif + if (line_fd && gpio_init_input_irq_sysfs(gpio, line_fd) == 0) { + SLOGI("LoRa GPIO %s irq-event via sysfs: gpio%d rising", line_name ? line_name : "irq", gpio); + return 0; + } + return -1; +} + +static int gpio_get_value_any(int gpio, int line_fd) +{ +#if HAS_LINUX_GPIO_CDEV + int value = 0; + if (line_fd >= 0 && gpio_get_input_line_value(line_fd, &value)) return value; +#endif + return gpio_get_value(gpio); +} + +static int gpio_set_value_any(int gpio, int line_fd, int value) +{ +#if HAS_LINUX_GPIO_CDEV + if (line_fd >= 0) return gpio_set_output_line_value(line_fd, value) ? 0 : -1; +#endif + return gpio_set_value(gpio, value); +} + +static size_t collect_spi_candidates(char out[][64], size_t max_count, const char *preferred) +{ + if (out == NULL || max_count == 0) return 0; + size_t count = 0; + auto append_candidate = [&](const char *path) { + if (path == NULL || path[0] == '\0') return; + for (size_t i = 0; i < count; ++i) if (strcmp(out[i], path) == 0) return; + if (count < max_count) { snprintf(out[count], 64, "%s", path); ++count; } + }; + append_candidate(preferred); + append_candidate("/dev/spidev0.1"); + append_candidate("/dev/spidev0.0"); + DIR *dir = opendir("/dev"); + if (dir != NULL) { + struct dirent *entry = NULL; + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, "spidev", 6) != 0) continue; + if (strlen(entry->d_name) > sizeof("/dev/") + 57) continue; + char full_path[64]; + snprintf(full_path, sizeof(full_path), "/dev/%s", entry->d_name); + append_candidate(full_path); + } + closedir(dir); + } + const char *fallbacks[] = { + "/dev/spidev0.1", "/dev/spidev0.0", "/dev/spidev1.0", "/dev/spidev1.1", + "/dev/spidev2.0", "/dev/spidev2.1", "/dev/spidev3.0", "/dev/spidev3.1", + "/dev/spidev4.0", "/dev/spidev4.1", + }; + for (size_t i = 0; i < sizeof(fallbacks)/sizeof(fallbacks[0]); ++i) append_candidate(fallbacks[i]); + return count; +} + +static void lora_update_power_debug(const char *stage, int sysfs_ret, int gpio_value, bool cdev_ok) +{ + char text[256]; + const char *chip_text = g_hat_5vout_chip[0] ? g_hat_5vout_chip : "sysfs"; + const char *value_text = gpio_value < 0 ? "read_fail" : (gpio_value ? "HIGH" : "LOW"); + snprintf(text, sizeof(text), "5VDBG %s cdev=%s chip=%s[%d] sysfs_ret=%d gpio5=%s", + stage ? stage : "?", cdev_ok ? "ok" : "fail", chip_text, g_hat_5vout_offset, sysfs_ret, value_text); + SLOGI("%s", text); +} + +static bool hat_5vout_prepare_line(void) +{ +#if HAS_LINUX_GPIO_CDEV + const char *chip_env = getenv("HAT_5VOUT_CHIP"); + const char *offset_env = getenv("HAT_5VOUT_OFFSET"); + if (chip_env && chip_env[0]) { + snprintf(g_hat_5vout_chip, sizeof(g_hat_5vout_chip), "%s", chip_env); + g_hat_5vout_offset = offset_env && offset_env[0] ? atoi(offset_env) : 5; + } else if (!gpio_find_named_line(g_hat_5vout_chip, sizeof(g_hat_5vout_chip), &g_hat_5vout_offset)) { + snprintf(g_hat_5vout_chip, sizeof(g_hat_5vout_chip), "/dev/gpiochip0"); + g_hat_5vout_offset = 5; + } + if (g_hat_5vout_fd >= 0) { g_hat_5vout_last_cdev_ok = true; return true; } + if (gpio_open_output_line(g_hat_5vout_chip, g_hat_5vout_offset, 1, &g_hat_5vout_fd)) { + g_hat_5vout_last_cdev_ok = true; return true; + } + g_hat_5vout_last_cdev_ok = false; +#endif + return false; +} + +static bool lora_spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len) +{ + if (g_spi_fd < 0) return false; + struct spi_ioc_transfer tr; + memset(&tr, 0, sizeof(tr)); + tr.tx_buf = (unsigned long)tx; + tr.rx_buf = (unsigned long)rx; + tr.len = (uint32_t)len; + tr.speed_hz = g_spi_speed; + tr.bits_per_word = 8; + int ret = ioctl(g_spi_fd, SPI_IOC_MESSAGE(1), &tr); + return ret >= 0; +} + +static bool lora_open_runtime_spi(void) +{ + if (g_spi_fd >= 0) return true; + g_spi_fd = open(g_spi_device, O_RDWR); + if (g_spi_fd < 0) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "runtime SPI open failed on %s", g_spi_device); + return false; + } + uint8_t mode = (uint8_t)SPI_MODE_0; + uint8_t bits = 8; + if (ioctl(g_spi_fd, SPI_IOC_WR_MODE, &mode) < 0 || + ioctl(g_spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 || + ioctl(g_spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &g_spi_speed) < 0) { + close(g_spi_fd); g_spi_fd = -1; + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "runtime SPI config failed on %s", g_spi_device); + return false; + } + return true; +} + +static bool sx1262_wait_while_busy(unsigned int timeout_ms) +{ + const unsigned int sleep_us = 1000; + unsigned int waited_ms = 0; + while (waited_ms < timeout_ms) { + int busy = gpio_get_value_any(g_lora_busy_gpio, g_lora_busy_fd); + if (busy < 0) return false; + if (busy == 0) return true; + usleep(sleep_us); + waited_ms += 1; + } + return false; +} + +static bool sx1262_reset(void) +{ + if (gpio_set_value_any(g_lora_rst_gpio, g_lora_rst_fd, 0) < 0) return false; + usleep(20000); + if (gpio_set_value_any(g_lora_rst_gpio, g_lora_rst_fd, 1) < 0) return false; + usleep(10000); + return sx1262_wait_while_busy(200); +} + +static bool sx1262_get_status_raw(uint8_t *status) +{ + uint8_t tx[2] = {0xC0, 0x00}; + uint8_t rx[2] = {0}; + if (!status) return false; + if (!lora_spi_transfer(tx, rx, sizeof(tx))) return false; + *status = rx[1]; + return true; +} + +static bool hat_5vout_enable(void) +{ + bool cdev_ok = false; +#if HAS_LINUX_GPIO_CDEV + if (hat_5vout_prepare_line()) { + if (gpio_set_output_line_value(g_hat_5vout_fd, 0)) { + cdev_ok = true; + g_hat_5vout_last_sysfs_ret = 0; + g_hat_5vout_last_value = gpio_get_value(g_lora_power_gpio); + lora_update_power_debug("cdev_set", g_hat_5vout_last_sysfs_ret, g_hat_5vout_last_value, cdev_ok); + usleep(50000); + return true; + } + } +#endif + g_hat_5vout_last_sysfs_ret = gpio_init_output(g_lora_power_gpio, 0); + g_hat_5vout_last_value = gpio_get_value(g_lora_power_gpio); + lora_update_power_debug("sysfs_set", g_hat_5vout_last_sysfs_ret, g_hat_5vout_last_value, cdev_ok); + if (g_hat_5vout_last_sysfs_ret == 0) { usleep(50000); return true; } + lora_update_power_debug("enable_fail", g_hat_5vout_last_sysfs_ret, g_hat_5vout_last_value, cdev_ok); + return false; +} + +static bool pi4io_open_bus(int *fd) +{ +#if !CP0_LORA_HAS_LINUX_I2CDEV + if (fd) *fd = -1; + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C dev header missing, cannot access 0x%02X", g_pi4io_i2c_addr); + return false; +#else + if (fd == NULL) { snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C fd pointer invalid"); return false; } + char dev_path[64]; + snprintf(dev_path, sizeof(dev_path), "/dev/i2c-%d", g_pi4io_i2c_bus); + *fd = open(dev_path, O_RDWR); + if (*fd < 0) { + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "open %s failed, SDA:%d SCL:%d errno=%d", + dev_path, g_pi4io_sda_gpio, g_pi4io_scl_gpio, errno); + return false; + } + return true; +#endif +} + +static bool pi4io_select_device(int fd) +{ + if (fd < 0) { snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C fd invalid for 0x%02X", g_pi4io_i2c_addr); return false; } + if (ioctl(fd, I2C_SLAVE, g_pi4io_i2c_addr) < 0) { + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "select 0x%02X failed on /dev/i2c-%d errno=%d", + g_pi4io_i2c_addr, g_pi4io_i2c_bus, errno); + return false; + } + return true; +} + +static bool pi4io_write_reg(int fd, uint8_t reg, uint8_t value) +{ + uint8_t buf[2] = {reg, value}; + return write(fd, buf, sizeof(buf)) == (ssize_t)sizeof(buf); +} + +static bool pi4io_probe_device(int fd) +{ + uint8_t reg = 0x00; + if (write(fd, ®, 1) != 1) { + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C 0x%02X not found on /dev/i2c-%d (SDA:%d SCL:%d)", + g_pi4io_i2c_addr, g_pi4io_i2c_bus, g_pi4io_sda_gpio, g_pi4io_scl_gpio); + return false; + } + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C 0x%02X found on /dev/i2c-%d (SDA:%d SCL:%d)", + g_pi4io_i2c_addr, g_pi4io_i2c_bus, g_pi4io_sda_gpio, g_pi4io_scl_gpio); + return true; +} + +static bool pi4io_init_device(int fd) +{ + if (fd < 0) { snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO init invalid fd for 0x%02X", g_pi4io_i2c_addr); return false; } + g_pi4io_polarity_cache = 0x00; + g_pi4io_output_cache = 0x01; + g_pi4io_config_cache = 0xFE; + errno = 0; + if (!pi4io_write_reg(fd, 0x02, g_pi4io_polarity_cache)) { + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO write POL failed at 0x%02X errno=%d", g_pi4io_i2c_addr, errno); + return false; + } + errno = 0; + if (!pi4io_write_reg(fd, 0x01, g_pi4io_output_cache)) { + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO write OUT failed at 0x%02X errno=%d", g_pi4io_i2c_addr, errno); + return false; + } + errno = 0; + if (!pi4io_write_reg(fd, 0x03, g_pi4io_config_cache)) { + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO write CFG failed at 0x%02X errno=%d", g_pi4io_i2c_addr, errno); + return false; + } + snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO init ok OUT=0x%02X POL=0x%02X CFG=0x%02X P0=HIGH", + g_pi4io_output_cache, g_pi4io_polarity_cache, g_pi4io_config_cache); + return true; +} + +static bool pi4io_scan_and_init_before_lora(void) +{ + int fd = -1; + bool ok = false; + g_pi4io_found = false; + g_pi4io_initialized = false; + if (!pi4io_open_bus(&fd)) return false; + do { + if (!pi4io_select_device(fd)) break; + if (!pi4io_probe_device(fd)) break; + g_pi4io_found = true; + if (!pi4io_init_device(fd)) break; + g_pi4io_initialized = true; + ok = true; + } while (0); + if (fd >= 0) close(fd); + return ok; +} + +static bool probe_lora_spi_device(void) +{ + const char *spi_env = getenv("LORA_SPI_DEV"); + char candidates[16][64] = {{0}}; + const size_t candidate_count = collect_spi_candidates(candidates, 16, spi_env); + char summary[256] = {0}; + + if (access("/dev", F_OK) != 0) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "Linux /dev not available; LoRa SPI HAL requires Raspberry Pi Linux runtime"); + snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "no /dev directory visible"); + snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "SPI: /dev unavailable"); + return false; + } + if (candidate_count == 0) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "no /dev/spidev* found; enable SPI on Raspberry Pi OS"); + snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "probe aborted: no spidev nodes"); + snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "SPI: no spidev found"); + return false; + } + SLOGI("LoRa SPI probe policy: prefer SPI0 only, CE1 then CE0"); + summary[0] = '\0'; + for (size_t i = 0; i < candidate_count; ++i) { + const char *dev = candidates[i]; + if (spi_env && spi_env[0] && strcmp(spi_env, dev) == 0) continue; + if (summary[0]) strncat(summary, ", ", sizeof(summary) - strlen(summary) - 1); + strncat(summary, dev, sizeof(summary) - strlen(summary) - 1); + } + if (spi_env && spi_env[0]) { + snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "probe order: %.96s%s%.128s", + spi_env, summary[0] ? ", " : "", summary); + snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "Try: %.96s -> 0.1 -> 0.0", spi_env); + } else { + snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "probe order: %.224s", summary); + snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "Try: /dev/spidev0.1 -> /dev/spidev0.0"); + } + + auto try_probe = [](const char *dev) -> bool { + if (dev == NULL || dev[0] == '\0' || access(dev, F_OK) != 0) return false; + snprintf(g_spi_device, sizeof(g_spi_device), "%s", dev); + g_lora_nss_manual = false; + const char *cs_name = strstr(g_spi_device, "spidev0.1") ? "SPI0-CE1" : (strstr(g_spi_device, "spidev0.0") ? "SPI0-CE0" : "non-SPI0"); + SLOGI("LoRa probe: trying %s [%s] (cs=hw-auto)", g_spi_device, cs_name); + g_lora_initialized = false; + if (g_spi_fd >= 0) { close(g_spi_fd); g_spi_fd = -1; } + if (gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", g_lora_rst_gpio, 1, &g_lora_rst_fd, "RST") < 0) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RST gpio init failed on %s", g_spi_device); + return false; + } + if (!sx1262_reset()) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RST/BUSY handshake failed on %s", g_spi_device); + return false; + } + uint8_t status = 0; + g_spi_fd = open(g_spi_device, O_RDWR); + if (g_spi_fd < 0) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "SPI open failed on %s", g_spi_device); + return false; + } + uint8_t mode = (uint8_t)SPI_MODE_0; + uint8_t bits = 8; + if (ioctl(g_spi_fd, SPI_IOC_WR_MODE, &mode) < 0 || + ioctl(g_spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 || + ioctl(g_spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &g_spi_speed) < 0) { + close(g_spi_fd); g_spi_fd = -1; + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "SPI config failed on %s", g_spi_device); + return false; + } + bool ok = sx1262_get_status_raw(&status); + close(g_spi_fd); g_spi_fd = -1; + if (!ok) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "status read failed on %s", g_spi_device); + return false; + } + SLOGI("LoRa probe: %s [%s] (cs=hw-auto) status=0x%02X", g_spi_device, cs_name, status); + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "probe ok on %s[%s] cs=hw-auto status=0x%02X", g_spi_device, cs_name, status); + snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "FOUND: %s (%s)", g_spi_device, cs_name); + return true; + }; + + if (spi_env && spi_env[0] && try_probe(spi_env)) return true; + for (size_t i = 0; i < candidate_count; ++i) { + if (try_probe(candidates[i])) return true; + } + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "all SPI buses probed, no SX1262 response (%.192s)", g_lora_probe_summary); + snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "NOT FOUND: tried 0.1 and 0.0"); + return false; +} + +static void resolve_lora_spi_device(void) +{ + const char *spi_env = getenv("LORA_SPI_DEV"); + char candidates[16][64] = {{0}}; + const size_t candidate_count = collect_spi_candidates(candidates, 16, spi_env); + if (spi_env != NULL && spi_env[0] != '\0' && access(spi_env, F_OK) == 0) { + snprintf(g_spi_device, sizeof(g_spi_device), "%s", spi_env); return; + } + for (size_t i = 0; i < candidate_count; ++i) { + if (access(candidates[i], F_OK) == 0) { + snprintf(g_spi_device, sizeof(g_spi_device), "%s", candidates[i]); return; + } + } + snprintf(g_spi_device, sizeof(g_spi_device), "%s", spi_env && spi_env[0] ? spi_env : "/dev/spidev0.1"); +} + + +// ============================================================ +// RadioLib HAL / Module / TX/RX logic +// ============================================================ + +class LinuxRadioLibHal : public PiHal { + public: + LinuxRadioLibHal() : PiHal(0, 2000000, 0, 0) {} + + void pinMode(uint32_t pin, uint32_t mode) override { + if (pin == RADIOLIB_NC) return; + if (mode == GpioModeOutput) { + if (pin == (uint32_t)g_lora_rst_gpio) { + (void)gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", (int)pin, 1, &g_lora_rst_fd, "RST"); + } + } else { + if (pin == (uint32_t)g_lora_busy_gpio) { + (void)gpio_init_input_any("LORA_BUSY_CHIP", "LORA_BUSY_OFFSET", (int)pin, &g_lora_busy_fd, "BUSY"); + } + } + } + + void digitalWrite(uint32_t pin, uint32_t value) override { + if (pin == RADIOLIB_NC) return; + int line_fd = -1; + if (pin == (uint32_t)g_lora_rst_gpio) line_fd = g_lora_rst_fd; + (void)gpio_set_value_any((int)pin, line_fd, value ? 1 : 0); + } + + uint32_t digitalRead(uint32_t pin) override { + if (pin == RADIOLIB_NC) return 0; + int line_fd = -1; + if (pin == (uint32_t)g_lora_busy_gpio) line_fd = g_lora_busy_fd; + int value = gpio_get_value_any((int)pin, line_fd); + return value > 0 ? 1U : 0U; + } + + void attachInterrupt(uint32_t, void (*)(void), uint32_t) override {} + void detachInterrupt(uint32_t) override {} + void delay(RadioLibTime_t ms) override { usleep((useconds_t)(ms * 1000)); } + void delayMicroseconds(RadioLibTime_t us) override { usleep((useconds_t)us); } + RadioLibTime_t millis() override { return (RadioLibTime_t)get_monotonic_ms(); } + RadioLibTime_t micros() override { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (RadioLibTime_t)ts.tv_sec * 1000000ULL + (RadioLibTime_t)ts.tv_nsec / 1000ULL; + } + long pulseIn(uint32_t pin, uint32_t state, RadioLibTime_t timeout) override { + RadioLibTime_t start = micros(); + while (micros() - start < timeout) { + if (digitalRead(pin) == state) { + RadioLibTime_t pulse_start = micros(); + while (micros() - start < timeout && digitalRead(pin) == state) {} + return (long)(micros() - pulse_start); + } + } + return 0; + } + void spiBegin() override {} + void spiBeginTransaction() override {} + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override { + uint8_t dummy[512] = {0}; + uint8_t *tx = out ? out : dummy; + uint8_t *rx = in ? in : dummy; + if (len > sizeof(dummy)) len = sizeof(dummy); + (void)lora_spi_transfer(tx, rx, len); + } + void spiEndTransaction() override {} + void spiEnd() override {} +}; + +static LinuxRadioLibHal g_lora_radio_hal; +static Module *g_lora_radio_module = NULL; +static SX1262 *g_lora_radio = NULL; + +static uint64_t get_monotonic_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; +} + +static void lora_set_diag_step(const char *step, int code, const char *detail) +{ + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "%s%s%s | rc=%d", + step ? step : "diag", (detail && detail[0]) ? " | " : "", (detail && detail[0]) ? detail : "", code); + SLOGI("LoRa diag: %s", g_lora_last_diag); +} + +static const char *lora_radiolib_status_text(int16_t state) +{ + switch (state) { + case RADIOLIB_ERR_NONE: return "ok"; + case RADIOLIB_ERR_CHIP_NOT_FOUND: return "chip_not_found"; + case RADIOLIB_ERR_TX_TIMEOUT: return "tx_timeout"; + case RADIOLIB_ERR_RX_TIMEOUT: return "rx_timeout"; + case RADIOLIB_ERR_CRC_MISMATCH: return "crc_mismatch"; + case RADIOLIB_ERR_SPI_WRITE_FAILED: return "spi_write_failed"; + case RADIOLIB_ERR_SPI_CMD_TIMEOUT: return "spi_cmd_timeout"; + case RADIOLIB_ERR_SPI_CMD_INVALID: return "spi_cmd_invalid"; + case RADIOLIB_ERR_SPI_CMD_FAILED: return "spi_cmd_failed"; + default: return "radiolib_err"; + } +} + +static void lora_capture_device_errors(const char *stage, uint16_t irq_status) +{ + if (!g_lora_initialized || g_lora_radio == NULL) return; + SLOGI("LoRa error: %s irq=0x%04X", stage ? stage : "radio_err", irq_status); + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "%s irq=0x%04X", stage ? stage : "radio_err", irq_status); +} + +static bool lora_send_text_packet(const char *payload) +{ + if (!g_lora_initialized || g_lora_radio == NULL) { + SLOGI("LoRa TX: not initialized"); + return false; + } + if (payload == NULL || payload[0] == '\0') return false; + if (g_lora_tx_in_progress) return false; + snprintf(g_lora_last_tx, sizeof(g_lora_last_tx), "%s", payload); + g_lora_has_sent_message = true; + g_lora_tx_done = false; + g_lora_rx_done = false; + g_lora_pending_rx_after_tx = true; + g_lora_tx_mode = false; + g_lora_selected_tx_mode = false; + (void)g_lora_radio->standby(); + int16_t state = g_lora_radio->startTransmit((uint8_t *)g_lora_last_tx, strlen(g_lora_last_tx)); + if (state != RADIOLIB_ERR_NONE) { + g_lora_tx_in_progress = false; + g_lora_pending_rx_after_tx = false; + SLOGI("LoRa TX: startTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + return false; + } + g_lora_tx_in_progress = true; + g_lora_tx_start_ms = g_lora_last_auto_tx_ms = get_monotonic_ms(); + SLOGI("LoRa TX: sending '%s'", g_lora_last_tx); + return true; +} + +static void lora_send_demo_packet(void) +{ + if (!g_lora_initialized || g_lora_radio == NULL) return; + if (!g_lora_tx_mode) return; + snprintf(g_lora_last_tx, sizeof(g_lora_last_tx), "Hello from M5 LoRa-1262 #%lu", (unsigned long)g_lora_tx_counter); + g_lora_has_sent_message = true; + g_lora_pending_rx_after_tx = false; + g_lora_tx_done = false; + g_lora_rx_done = false; + int16_t state = g_lora_radio->startTransmit((uint8_t *)g_lora_last_tx, strlen(g_lora_last_tx)); + if (state != RADIOLIB_ERR_NONE) { + g_lora_tx_in_progress = false; + SLOGI("LoRa TX: demo startTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + return; + } + g_lora_tx_in_progress = true; + g_lora_tx_start_ms = g_lora_last_auto_tx_ms = get_monotonic_ms(); + SLOGI("LoRa TX: demo sending '%s'", g_lora_last_tx); + ++g_lora_tx_counter; +} + +static void lora_start_receive_mode(void) +{ + if (!g_lora_initialized || g_lora_radio == NULL) { + SLOGI("LoRa RX: startReceive skipped, not initialized"); + return; + } + if (g_lora_tx_in_progress) { + SLOGI("LoRa RX: startReceive skipped, TX in progress"); + g_lora_pending_rx_after_tx = true; + return; + } + g_lora_tx_mode = false; + g_lora_selected_tx_mode = false; + g_lora_pending_rx_after_tx = false; + SLOGI("LoRa RX: startReceive()"); + int16_t state = g_lora_radio->startReceive(); + SLOGI("LoRa RX: startReceive rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + if (state != RADIOLIB_ERR_NONE) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "startReceive rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + } +} + +static void lora_apply_mode(bool tx_mode) +{ + g_lora_selected_tx_mode = tx_mode; + if (!g_lora_initialized || g_lora_radio == NULL) { + SLOGI("LoRa mode: not initialized"); + return; + } + if (tx_mode) { + g_lora_pending_rx_after_tx = false; + g_lora_tx_mode = true; + g_lora_last_auto_tx_ms = get_monotonic_ms(); + if (g_lora_tx_in_progress) { + SLOGI("LoRa mode: TX already in progress"); + return; + } + int16_t state = g_lora_radio->standby(); + if (state == RADIOLIB_ERR_NONE) { + SLOGI("LoRa mode: TX ready"); + } else { + SLOGI("LoRa mode: set TX failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + } + } else { + if (g_lora_tx_in_progress) { + g_lora_pending_rx_after_tx = true; + SLOGI("LoRa mode: TX in progress, will RX after done"); + return; + } + g_lora_pending_rx_after_tx = false; + g_lora_tx_mode = false; + g_lora_last_auto_tx_ms = get_monotonic_ms(); + lora_start_receive_mode(); + } +} + +static void lora_service_irq_once(void) +{ + if (!g_lora_initialized || g_lora_radio == NULL) return; + + bool irq_event = false; + if (!g_lora_irq_poll_fallback && g_lora_irq_fd >= 0) { + struct pollfd pfd; + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = g_lora_irq_fd; + pfd.events = POLLIN | POLLPRI; + if (poll(&pfd, 1, 0) > 0 && (pfd.revents & (POLLIN | POLLPRI))) { + irq_event = true; +#if HAS_LINUX_GPIO_CDEV + struct gpioevent_data event_data; + while (read(g_lora_irq_fd, &event_data, sizeof(event_data)) == (ssize_t)sizeof(event_data)) {} +#else + char value_buf[8]; + lseek(g_lora_irq_fd, 0, SEEK_SET); + while (read(g_lora_irq_fd, value_buf, sizeof(value_buf)) > 0) { lseek(g_lora_irq_fd, 0, SEEK_SET); break; } +#endif + } + } + + uint32_t irq_flags = g_lora_radio->getIrqFlags(); + if (irq_flags != RADIOLIB_SX126X_IRQ_NONE || irq_event) { + SLOGI("LoRa IRQ: event=%d flags=0x%08lX tx_in_progress=%d tx_mode=%d", + irq_event ? 1 : 0, (unsigned long)irq_flags, g_lora_tx_in_progress ? 1 : 0, g_lora_tx_mode ? 1 : 0); + } + if (!irq_event && irq_flags == RADIOLIB_SX126X_IRQ_NONE) return; + + if (g_lora_tx_in_progress) { + if (irq_flags & RADIOLIB_SX126X_IRQ_TX_DONE) { + int16_t state = g_lora_radio->finishTransmit(); + if (state == RADIOLIB_ERR_NONE) { + g_lora_tx_done = true; + } else { + g_lora_tx_in_progress = false; + SLOGI("LoRa TX: finishTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + } + } else if (irq_flags & RADIOLIB_SX126X_IRQ_TIMEOUT) { + g_lora_tx_in_progress = false; + g_lora_tx_start_ms = 0; + lora_capture_device_errors("TX irq timeout", 0); + if (g_lora_pending_rx_after_tx || !g_lora_tx_mode) lora_start_receive_mode(); + } + return; + } + + if (irq_flags & RADIOLIB_SX126X_IRQ_RX_DONE) { + uint8_t rx_buf[sizeof(g_lora_last_rx)] = {0}; + int16_t state = g_lora_radio->readData(rx_buf, sizeof(g_lora_last_rx) - 1); + SLOGI("LoRa RX: readData rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + if (state == RADIOLIB_ERR_NONE) { + memcpy(g_lora_last_rx, rx_buf, sizeof(g_lora_last_rx)); + g_lora_last_rx[sizeof(g_lora_last_rx) - 1] = '\0'; + g_lora_last_rssi = g_lora_radio->getRSSI(); + g_lora_last_snr = g_lora_radio->getSNR(); + g_lora_rx_done = true; + SLOGI("LoRa RX OK: '%s' RSSI=%.1f SNR=%.1f", g_lora_last_rx, g_lora_last_rssi, g_lora_last_snr); + } else if (state != RADIOLIB_ERR_CRC_MISMATCH) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "readData rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + } + if (!g_lora_tx_mode) lora_start_receive_mode(); + } else if (irq_flags & (RADIOLIB_SX126X_IRQ_CRC_ERR | RADIOLIB_SX126X_IRQ_HEADER_ERR)) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RX crc/header error irq=0x%04lX", (unsigned long)irq_flags); + SLOGI("LoRa RX error: %s", g_lora_last_diag); + if (!g_lora_tx_mode) lora_start_receive_mode(); + } else if (irq_flags & RADIOLIB_SX126X_IRQ_TIMEOUT) { + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RX timeout irq=0x%04lX", (unsigned long)irq_flags); + SLOGI("LoRa RX timeout: %s", g_lora_last_diag); + } +} + +static void lora_check_tx_fallback(void) +{ + if (!g_lora_initialized || !g_lora_tx_in_progress || g_lora_radio == NULL) return; + uint64_t now_ms = get_monotonic_ms(); + if (g_lora_tx_start_ms != 0 && now_ms - g_lora_tx_start_ms >= 4000ULL) { + g_lora_tx_in_progress = false; + g_lora_tx_start_ms = 0; + g_lora_last_auto_tx_ms = now_ms; + lora_capture_device_errors("TX timeout", 0); + (void)g_lora_radio->standby(); + if (g_lora_pending_rx_after_tx || !g_lora_tx_mode) lora_start_receive_mode(); + } +} + +static bool g_lora_rx_event = false; +static bool g_lora_tx_event = false; + +static void lora_poll_hardware(void) +{ + if (!g_lora_initialized) return; + lora_service_irq_once(); + lora_check_tx_fallback(); + + if (g_lora_tx_done) { + g_lora_tx_done = false; + g_lora_tx_event = true; + g_lora_tx_in_progress = false; + g_lora_tx_start_ms = 0; + if (g_lora_pending_rx_after_tx || !g_lora_tx_mode) { + lora_start_receive_mode(); + } + } + + if (g_lora_rx_done) { + g_lora_rx_done = false; + g_lora_rx_event = true; + } + + if (g_lora_initialized && g_lora_tx_mode && !g_lora_tx_in_progress) { + uint64_t now_ms = get_monotonic_ms(); + if (now_ms - g_lora_last_auto_tx_ms >= 2000ULL) { + lora_send_demo_packet(); + } + } +} + + +// ============================================================ +// Hardware initialization +// ============================================================ + +static void lora_init_hardware(void) +{ + delete g_lora_radio; g_lora_radio = NULL; + delete g_lora_radio_module; g_lora_radio_module = NULL; + + lora_set_diag_step("i2c_scan", 0, "scan 0x43 before LoRa init"); + if (pi4io_scan_and_init_before_lora()) { + lora_set_diag_step("i2c_scan", 0, g_pi4io_status); + } else { + lora_set_diag_step("i2c_scan", 1, g_pi4io_status); + } + + lora_set_diag_step("power_enable", 0, "start"); + if (!hat_5vout_enable()) { + SLOGI("Status: GPIO5 low set failed"); + lora_set_diag_step("power_enable", 1, "GPIO5 low set failed"); + } + usleep(100000); + + lora_set_diag_step("reset_gpio_init", 0, "prepare rst pin"); + if (gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", g_lora_rst_gpio, 1, &g_lora_rst_fd, "RST") < 0) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("reset_gpio_init", 1, "rst gpio init failed"); + return; + } + + if (gpio_init_input_any("LORA_BUSY_CHIP", "LORA_BUSY_OFFSET", g_lora_busy_gpio, &g_lora_busy_fd, "BUSY") < 0) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("busy_gpio_init", 1, "busy gpio init failed"); + return; + } + + lora_set_diag_step("hard_reset", 0, "toggle rst before probe"); + if (!sx1262_reset()) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("hard_reset", 1, "rst/busy handshake failed"); + return; + } + + lora_set_diag_step("resolve_spi", 0, "detect device"); + resolve_lora_spi_device(); + + if (!probe_lora_spi_device()) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("probe_spi", 1, g_lora_last_diag); + return; + } + + lora_set_diag_step("pre_begin_prepare", 0, "reset again before RadioLib begin"); + if (!sx1262_reset()) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("pre_begin_prepare", 1, "rst/busy handshake failed before RadioLib begin"); + return; + } + + lora_set_diag_step("prepare_irq", 0, "init irq pin"); + if (gpio_init_input_irq_any("LORA_IRQ_CHIP", "LORA_IRQ_OFFSET", g_lora_irq_gpio, &g_lora_irq_fd, "IRQ") < 0) { + g_lora_irq_poll_fallback = true; + lora_set_diag_step("prepare_irq", 1, "irq gpio init failed, fallback=poll"); + } else { + g_lora_irq_poll_fallback = false; + lora_set_diag_step("prepare_irq", 0, "irq gpio ok"); + } + + lora_set_diag_step("runtime_spi", 0, "open SPI for RadioLib runtime"); + if (!lora_open_runtime_spi()) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("runtime_spi", 1, g_lora_last_diag); + return; + } + + lora_set_diag_step("radiolib_setup", 0, "create module"); + g_lora_nss_manual = false; + g_lora_radio_module = new Module(&g_lora_radio_hal, RADIOLIB_NC, + (uint32_t)g_lora_irq_gpio, (uint32_t)g_lora_rst_gpio, (uint32_t)g_lora_busy_gpio); + g_lora_radio = new SX1262(g_lora_radio_module); + + if (g_lora_radio_module == NULL || g_lora_radio == NULL) { + g_lora_initialized = false; g_lora_hw_ready = false; + lora_set_diag_step("radiolib_setup", 1, "allocation failed"); + return; + } + + lora_set_diag_step("radiolib_begin", 0, "configure sx1262 via RadioLib"); + int16_t state = g_lora_radio->begin( + 868.0f, // frequency MHz + 125.0f, // bandwidth kHz + 12, // spreading factor + 5, // coding rate 4/5 + 0x34, // sync word + 22, // output power dBm + 20, // preamble length + 3.0f, // TCXO voltage + false + ); + + if (state != RADIOLIB_ERR_NONE) { + g_lora_initialized = false; g_lora_hw_ready = false; + snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RadioLib begin rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); + SLOGI("LoRa init failed: rc=%d (%s)", (int)state, lora_radiolib_status_text(state)); + lora_set_diag_step("radiolib_begin", state, g_lora_last_diag); + return; + } + + (void)g_lora_radio->setCurrentLimit(140); + (void)g_lora_radio->setDio2AsRfSwitch(true); + + g_lora_initialized = true; + g_lora_hw_ready = true; + g_lora_tx_mode = false; + g_lora_selected_tx_mode = false; + g_lora_tx_in_progress = false; + g_lora_pending_rx_after_tx = false; + g_lora_tx_start_ms = 0; + g_lora_last_auto_tx_ms = get_monotonic_ms(); + + lora_set_diag_step("ready", 0, "LoRa init finished"); + SLOGI("LoRa: init done, auto enter RX"); + lora_start_receive_mode(); +} + + + + +static void fill_info(cp0_lora_info_t *info, bool drain_events) +{ + if (!info) return; + memset(info, 0, sizeof(*info)); + info->initialized = g_lora_initialized ? 1 : 0; + info->hw_ready = g_lora_hw_ready ? 1 : 0; + info->tx_mode = g_lora_tx_mode ? 1 : 0; + info->tx_in_progress = g_lora_tx_in_progress ? 1 : 0; + info->has_sent_message = g_lora_has_sent_message ? 1 : 0; + info->rx_event = g_lora_rx_event ? 1 : 0; + info->tx_event = g_lora_tx_event ? 1 : 0; + cp0_copy_cstr(info->spi_device, sizeof(info->spi_device), g_spi_device); + cp0_copy_cstr(info->last_rx, sizeof(info->last_rx), g_lora_last_rx); + cp0_copy_cstr(info->last_tx, sizeof(info->last_tx), g_lora_last_tx); + cp0_copy_cstr(info->diag, sizeof(info->diag), g_lora_last_diag); + cp0_copy_cstr(info->probe_summary, sizeof(info->probe_summary), g_lora_probe_summary); + cp0_copy_cstr(info->probe_display, sizeof(info->probe_display), g_lora_probe_display); + cp0_copy_cstr(info->pi4io_status, sizeof(info->pi4io_status), g_pi4io_status); + info->rssi = g_lora_last_rssi; + info->snr = g_lora_last_snr; + if (drain_events) { + g_lora_rx_event = false; + g_lora_tx_event = false; + } +} + +} // namespace cp0_lora_backend + +class LoraSystem +{ +public: + void api_call(std::list arg, std::function callback) + { + if (arg.empty()) { + report(callback, -1, "missing lora api command\n"); + return; + } + + const std::string command = arg.front(); + if (command == "Init") { + if (!cp0_lora_backend::g_lora_initialized && !cp0_lora_backend::g_lora_hw_ready) + cp0_lora_backend::lora_init_hardware(); + report(callback, cp0_lora_backend::g_lora_hw_ready ? 0 : -1, ""); + return; + } + if (command == "Poll" || command == "Info") { + cp0_lora_info_t info{}; + if (command == "Poll") cp0_lora_backend::lora_poll_hardware(); + cp0_lora_backend::fill_info(&info, command == "Poll"); + report(callback, 0, std::string(reinterpret_cast(&info), sizeof(info))); + return; + } + if (command == "SendText") { + std::string payload = first_arg_after_command(arg); + report(callback, cp0_lora_backend::lora_send_text_packet(payload.c_str()) ? 0 : -1, ""); + return; + } + if (command == "StartReceive") { + cp0_lora_backend::lora_start_receive_mode(); + report(callback, 0, ""); + return; + } + if (command == "SetTxMode") { + cp0_lora_backend::lora_apply_mode(std::atoi(first_arg_after_command(arg).c_str()) != 0); + report(callback, 0, ""); + return; + } + if (command == "Shutdown") { + shutdown(); + report(callback, 0, ""); + return; + } + + report(callback, -1, "unknown lora api\n"); + } + +private: + static std::string first_arg_after_command(const std::list& arg) + { + if (arg.size() < 2) return ""; + return *std::next(arg.begin()); + } + + static void report(std::function callback, int code, const std::string& data) + { + if (callback) callback(code, data); + } + + static void shutdown() + { + using namespace cp0_lora_backend; + delete g_lora_radio; + g_lora_radio = NULL; + delete g_lora_radio_module; + g_lora_radio_module = NULL; + if (g_spi_fd >= 0) { close(g_spi_fd); g_spi_fd = -1; } + if (g_lora_rst_fd >= 0) { close(g_lora_rst_fd); g_lora_rst_fd = -1; } + if (g_lora_busy_fd >= 0) { close(g_lora_busy_fd); g_lora_busy_fd = -1; } + if (g_lora_irq_fd >= 0) { close(g_lora_irq_fd); g_lora_irq_fd = -1; } + if (g_lora_nss_fd >= 0) { close(g_lora_nss_fd); g_lora_nss_fd = -1; } + if (g_hat_5vout_fd >= 0) { close(g_hat_5vout_fd); g_hat_5vout_fd = -1; } + g_lora_initialized = false; + g_lora_hw_ready = false; + } +}; + +extern "C" void init_lora(void) +{ + std::shared_ptr lora = std::make_shared(); + cp0_signal_lora_api.append([lora](std::list arg, std::function callback) { + lora->api_call(arg, callback); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp new file mode 100644 index 00000000..271cde0f --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp @@ -0,0 +1,411 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" +#include "../cp0_app_internal_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAL_PLATFORM_SDL +#include "hal/hal_network.h" +#else +#include +#include +#include +#endif + +int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) +{ + if (!out_count) + return -1; + *out_count = 0; + +#ifdef HAL_PLATFORM_SDL + if (!entries || max_entries <= 0) + return hal_network_list(nullptr, 0, out_count); + + std::vector hal_entries(static_cast(max_entries)); + int ret = hal_network_list(hal_entries.data(), max_entries, out_count); + int count = *out_count; + if (count > max_entries) + count = max_entries; + + for (int i = 0; i < count; ++i) { + cp0_copy_string(entries[i].iface, sizeof(entries[i].iface), hal_entries[i].iface); + cp0_copy_string(entries[i].ipv4, sizeof(entries[i].ipv4), hal_entries[i].ipv4); + cp0_copy_string(entries[i].netmask, sizeof(entries[i].netmask), hal_entries[i].netmask); + entries[i].is_up = hal_entries[i].is_up; + } + *out_count = count; + return ret; +#else + if (!entries || max_entries <= 0) + return 0; + + struct ifaddrs *ifap = nullptr; + if (getifaddrs(&ifap) != 0) + return -1; + + for (struct ifaddrs *ifa = ifap; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) + continue; + if (std::strcmp(ifa->ifa_name, "lo") == 0) + continue; + if (*out_count >= max_entries) + break; + + cp0_netif_info_t *e = &entries[*out_count]; + cp0_copy_string(e->iface, sizeof(e->iface), ifa->ifa_name); + struct sockaddr_in *sa = reinterpret_cast(ifa->ifa_addr); + inet_ntop(AF_INET, &sa->sin_addr, e->ipv4, sizeof(e->ipv4)); + if (ifa->ifa_netmask) { + struct sockaddr_in *nm = reinterpret_cast(ifa->ifa_netmask); + inet_ntop(AF_INET, &nm->sin_addr, e->netmask, sizeof(e->netmask)); + } else { + cp0_copy_string(e->netmask, sizeof(e->netmask), "N/A"); + } + e->is_up = (ifa->ifa_flags & IFF_UP) ? 1 : 0; + (*out_count)++; + } + freeifaddrs(ifap); + return 0; +#endif +} + +class WifiSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + WifiSystem() + { + update_status_cache(); + worker_ = std::thread([this]() { poll_loop(); }); + worker_.detach(); + } + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "Status") { + cp0_wifi_status_t st = get_status(); + report(callback, 0, encode_status(st)); + } else if (cmd == "Scan") { + int max_count = arg.size() >= 2 ? std::atoi(second_arg(arg).c_str()) : CP0_WIFI_AP_MAX; + std::vector aps(std::max(0, max_count)); + int count = scan(aps.empty() ? nullptr : aps.data(), static_cast(aps.size())); + report(callback, count, encode_scan(aps.data(), count)); + } else if (cmd == "Connect") { + const std::string ssid = nth_arg(arg, 1); + const std::string password = nth_arg(arg, 2); + report(callback, connect(ssid.c_str(), password.empty() ? nullptr : password.c_str()), ""); + } else if (cmd == "Disconnect") { + report(callback, disconnect(), ""); + } else if (cmd == "ProfileForget") { + const std::string ssid = nth_arg(arg, 1); + report(callback, profile_forget(ssid.c_str()), ""); + } else if (cmd == "ProfileExists") { + const std::string ssid = nth_arg(arg, 1); + report(callback, profile_exists(ssid.c_str()), ""); + } else if (cmd == "ProfileDisconnectActive") { + report(callback, profile_disconnect_active(), ""); + } else { + report(callback, -1, "unknown wifi api command"); + } + } + + cp0_wifi_status_t get_status() + { + std::lock_guard lock(mutex_); + return cache_; + } + + int scan(cp0_wifi_ap_t *out, int max_aps) + { + const char *rescan_argv[] = {"nmcli", "dev", "wifi", "rescan", nullptr}; + cp0_process_run_argv(rescan_argv, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + char output[8192] = {}; + const char *scan_argv[] = {"nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "dev", "wifi", "list", nullptr}; + if (cp0_process_capture_argv(scan_argv, output, sizeof(output)) != 0) + return 0; + + std::vector aps; + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line.empty()) + continue; + + cp0_wifi_ap_t ap{}; + if (!parse_scan_line(line, ap)) + continue; + upsert_ap(aps, ap); + } + + const int count = static_cast(aps.size()); + if (out && max_aps > 0) { + const int copy_count = std::min(count, max_aps); + for (int i = 0; i < copy_count; ++i) + out[i] = aps[static_cast(i)]; + return copy_count; + } + return count; + } + + int connect(const char *ssid, const char *password) + { + if (!ssid || !ssid[0]) + return -1; + + char output[4096] = {}; + int ret = -1; + if (password && password[0]) { + const char *argv[] = {"nmcli", "dev", "wifi", "connect", ssid, "password", password, nullptr}; + ret = cp0_process_capture_argv(argv, output, sizeof(output)); + } else { + const char *argv[] = {"nmcli", "con", "up", "id", ssid, nullptr}; + ret = cp0_process_capture_argv(argv, output, sizeof(output)); + } + + if (ret == 0 || std::string(output).find("successfully") != std::string::npos) { + update_status_cache(); + return 0; + } + return -1; + } + + int disconnect() + { + int ret = profile_disconnect_active(); + update_status_cache(); + return ret; + } + + int profile_forget(const char *ssid) + { + if (!ssid || !ssid[0]) + return -1; + const char *argv[] = {"nmcli", "con", "delete", "id", ssid, nullptr}; + return cp0_process_run_argv(argv, 0); + } + + int profile_exists(const char *ssid) + { + if (!ssid || !ssid[0]) + return 0; + char output[4096] = {}; + const char *argv[] = {"nmcli", "-t", "-f", "NAME", "con", "show", nullptr}; + if (cp0_process_capture_argv(argv, output, sizeof(output)) != 0) + return 0; + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line == ssid) + return 1; + } + return 0; + } + + int profile_disconnect_active() + { + const std::string active = active_connection_name(); + if (active.empty()) + return -1; + const char *argv[] = {"nmcli", "con", "down", "id", active.c_str(), nullptr}; + return cp0_process_run_argv(argv, 0); + } + +private: + std::mutex mutex_; + cp0_wifi_status_t cache_{}; + std::thread worker_; + std::atomic running_{true}; + + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static std::string second_arg(const arg_t &arg) + { + return nth_arg(arg, 1); + } + + static std::vector split_colon(const std::string &line) + { + std::vector cols; + std::string current; + for (char ch : line) { + if (ch == ':') { + cols.push_back(current); + current.clear(); + } else { + current.push_back(ch); + } + } + cols.push_back(current); + return cols; + } + + static bool parse_scan_line(const std::string &line, cp0_wifi_ap_t &ap) + { + auto cols = split_colon(line); + if (cols.size() < 4 || cols[0].empty()) + return false; + cp0_copy_string(ap.ssid, sizeof(ap.ssid), cols[0]); + ap.signal = std::atoi(cols[1].c_str()); + cp0_copy_string(ap.security, sizeof(ap.security), cols[2]); + ap.in_use = cols[3].find('*') != std::string::npos ? 1 : 0; + return true; + } + + static void upsert_ap(std::vector &aps, const cp0_wifi_ap_t &ap) + { + auto it = std::find_if(aps.begin(), aps.end(), [&](const cp0_wifi_ap_t &existing) { + return std::strcmp(existing.ssid, ap.ssid) == 0; + }); + if (it == aps.end()) { + aps.push_back(ap); + return; + } + + int in_use = it->in_use || ap.in_use; + if (ap.signal > it->signal) + *it = ap; + it->in_use = in_use; + } + + static std::string encode_status(const cp0_wifi_status_t &st) + { + std::ostringstream oss; + oss << st.connected << ':' << st.ssid << ':' << st.ip << ':' << st.signal; + return oss.str(); + } + + static std::string encode_scan(const cp0_wifi_ap_t *aps, int count) + { + std::ostringstream oss; + for (int i = 0; aps && i < count; ++i) { + oss << aps[i].ssid << ':' << aps[i].signal << ':' << aps[i].security << ':' << aps[i].in_use << '\n'; + } + return oss.str(); + } + + void poll_loop() + { + while (running_.load()) { + update_status_cache(); + std::this_thread::sleep_for(std::chrono::seconds(3)); + } + } + + void update_status_cache() + { + cp0_wifi_status_t st{}; + read_status(st); + std::lock_guard lock(mutex_); + cache_ = st; + } + + static void read_status(cp0_wifi_status_t &st) + { + char output[4096] = {}; + const char *status_argv[] = {"nmcli", "-t", "-f", "TYPE,CONNECTION", "dev", "status", nullptr}; + if (cp0_process_capture_argv(status_argv, output, sizeof(output)) == 0) { + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line.rfind("wifi:", 0) != 0) + continue; + std::string name = line.substr(5); + if (!name.empty() && name != "--") { + st.connected = 1; + cp0_copy_string(st.ssid, sizeof(st.ssid), name); + } + break; + } + } + + if (!st.connected) + return; + + const char *signal_argv[] = {"nmcli", "-t", "-f", "IN-USE,SIGNAL", "dev", "wifi", "list", "--rescan", "no", nullptr}; + if (cp0_process_capture_argv(signal_argv, output, sizeof(output)) == 0) { + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (line.rfind("*:", 0) == 0) { + st.signal = std::atoi(line.c_str() + 2); + break; + } + } + } + + const char *ip_argv[] = {"ip", "-4", "-o", "addr", "show", "wlan0", nullptr}; + if (cp0_process_capture_argv(ip_argv, output, sizeof(output)) == 0) { + std::string line(output); + auto pos = line.find("inet "); + if (pos != std::string::npos) { + std::string ip = line.substr(pos + 5); + auto slash = ip.find('/'); + if (slash != std::string::npos) + ip.resize(slash); + cp0_copy_string(st.ip, sizeof(st.ip), ip); + } + } + } + + static std::string active_connection_name() + { + char output[4096] = {}; + const char *argv[] = {"nmcli", "-t", "-f", "NAME", "con", "show", "--active", nullptr}; + if (cp0_process_capture_argv(argv, output, sizeof(output)) != 0) + return {}; + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (!line.empty() && line != "lo") + return line; + } + return {}; + } +}; + +extern "C" void init_wifi(void) +{ + auto wifi = std::make_shared(); + cp0_signal_wifi_api.append([wifi](std::list arg, std::function callback) { + wifi->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_osinfo.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_osinfo.cpp new file mode 100644 index 00000000..ff6c793c --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_osinfo.cpp @@ -0,0 +1,269 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" +#include "../cp0_app_internal_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class OsInfoSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = nth_arg(arg, 0); + if (cmd == "NetworkDefaultInfoRead" || cmd == "EthInfoRead") { + cp0_eth_info_t info{}; + int ret = network_default_info_read(&info); + report(callback, ret, encode_eth_info(info)); + } else if (cmd == "AccountInfoRead") { + cp0_account_info_t info{}; + int ret = account_info_read(&info); + report(callback, ret, encode_account_info(info)); + } else if (cmd == "TimeSet") { + report(callback, time_set(nth_arg(arg, 1).c_str()), ""); + } else if (cmd == "AptUpdateBackground") { + report(callback, apt_update_background(), ""); + } else if (cmd == "UpdateLauncherBackground") { + report(callback, update_launcher_background(), ""); + } else { + report(callback, -1, "unknown osinfo api command"); + } + } + + static int api_simple(const arg_t &arg, std::string *out = nullptr) + { + int result = -1; + cp0_signal_osinfo_api(arg, [&](int code, std::string data) { + result = code; + if (out) + *out = std::move(data); + }); + return result; + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + std::advance(it, std::min(index, arg.size())); + return it == arg.end() ? std::string() : *it; + } + + static void clear_net_info(cp0_eth_info_t *info) + { + if (!info) + return; + std::memset(info, 0, sizeof(*info)); + cp0_copy_cstr(info->ipv4, sizeof(info->ipv4), "N/A"); + cp0_copy_cstr(info->gateway, sizeof(info->gateway), "N/A"); + cp0_copy_cstr(info->mac, sizeof(info->mac), "N/A"); + } + + static int network_default_info_read(cp0_eth_info_t *info) + { + clear_net_info(info); + if (!info) + return -1; + + char output[2048] = {}; + const char *ip_argv[] = {"ip", "-4", "addr", "show", "eth0", nullptr}; + if (cp0_process_capture_argv(ip_argv, output, sizeof(output)) == 0) { + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + auto pos = line.find("inet "); + if (pos == std::string::npos) + continue; + std::istringstream iss(line.substr(pos + 5)); + std::string ip; + iss >> ip; + cp0_copy_string(info->ipv4, sizeof(info->ipv4), ip.empty() ? "N/A" : ip); + break; + } + } + + const char *route_argv[] = {"ip", "route", nullptr}; + if (cp0_process_capture_argv(route_argv, output, sizeof(output)) == 0) { + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (line.find("default") == std::string::npos || line.find("eth0") == std::string::npos) + continue; + std::istringstream iss(line); + std::string word; + while (iss >> word) { + if (word == "via") { + std::string gw; + if (iss >> gw) + cp0_copy_string(info->gateway, sizeof(info->gateway), gw); + break; + } + } + break; + } + } + + cp0_file_read_first_line("/sys/class/net/eth0/address", info->mac, sizeof(info->mac)); + return 0; + } + + static int account_info_read(cp0_account_info_t *info) + { + if (!info) + return -1; + std::memset(info, 0, sizeof(*info)); + const char *user = getlogin(); + if (!user || !user[0]) { + struct passwd *pw = getpwuid(getuid()); + user = pw ? pw->pw_name : nullptr; + } + cp0_copy_cstr(info->user, sizeof(info->user), user && user[0] ? user : "N/A"); + + char host[sizeof(info->hostname)] = {}; + if (gethostname(host, sizeof(host) - 1) == 0 && host[0]) + cp0_copy_cstr(info->hostname, sizeof(info->hostname), host); + else + cp0_copy_cstr(info->hostname, sizeof(info->hostname), "N/A"); + return 0; + } + + static int time_set(const char *timestamp) + { + if (!timestamp || !timestamp[0]) + return -1; + const char *argv[] = {"sudo", "date", "-s", timestamp, nullptr}; + return cp0_process_run_argv(argv, 0); + } + + static int apt_update_background() + { + const char *argv[] = {"apt", "update", nullptr}; + return cp0_process_run_argv(argv, 1); + } + + static int update_launcher_background() + { + const char *argv[] = { + "sh", "-c", + "cd /usr/share/APPLaunch && " + "wget -q https://github.com/CardputerZero/M5CardputerZero-Launcher/releases/latest/download/applaunch_*.deb -O /tmp/launcher_update.deb 2>/dev/null && " + "dpkg -i /tmp/launcher_update.deb >/dev/null 2>&1 && " + "systemctl restart APPLaunch", + nullptr + }; + return cp0_process_run_argv(argv, 1); + } + + static std::string encode_eth_info(const cp0_eth_info_t &info) + { + return std::string(info.ipv4) + "\n" + info.gateway + "\n" + info.mac; + } + + static void decode_eth_info(const std::string &data, cp0_eth_info_t *info) + { + clear_net_info(info); + if (!info) + return; + std::istringstream lines(data); + std::string line; + if (std::getline(lines, line)) cp0_copy_string(info->ipv4, sizeof(info->ipv4), line); + if (std::getline(lines, line)) cp0_copy_string(info->gateway, sizeof(info->gateway), line); + if (std::getline(lines, line)) cp0_copy_string(info->mac, sizeof(info->mac), line); + } + + static std::string encode_account_info(const cp0_account_info_t &info) + { + return std::string(info.user) + "\n" + info.hostname; + } + + static void decode_account_info(const std::string &data, cp0_account_info_t *info) + { + if (!info) + return; + std::memset(info, 0, sizeof(*info)); + std::istringstream lines(data); + std::string line; + if (std::getline(lines, line)) cp0_copy_string(info->user, sizeof(info->user), line); + if (std::getline(lines, line)) cp0_copy_string(info->hostname, sizeof(info->hostname), line); + } + +public: + static int api_eth_info(const char *command, cp0_eth_info_t *info) + { + if (!info) + return -1; + std::string data; + int ret = api_simple({command}, &data); + if (ret == 0) + decode_eth_info(data, info); + return ret; + } + + static int api_account_info(cp0_account_info_t *info) + { + if (!info) + return -1; + std::string data; + int ret = api_simple({"AccountInfoRead"}, &data); + if (ret == 0) + decode_account_info(data, info); + return ret; + } +}; + +extern "C" void init_osinfo(void) +{ + auto osinfo = std::make_shared(); + cp0_signal_osinfo_api.append([osinfo](std::list arg, std::function callback) { + osinfo->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int cp0_network_default_info_read(cp0_eth_info_t *info) +{ + return OsInfoSystem::api_eth_info("NetworkDefaultInfoRead", info); +} + +extern "C" int cp0_eth_info_read(cp0_eth_info_t *info) +{ + return OsInfoSystem::api_eth_info("EthInfoRead", info); +} + +extern "C" int cp0_account_info_read(cp0_account_info_t *info) +{ + return OsInfoSystem::api_account_info(info); +} + +extern "C" int cp0_time_set(const char *timestamp) +{ + return OsInfoSystem::api_simple({"TimeSet", timestamp ? timestamp : ""}); +} + +extern "C" int cp0_system_apt_update_background(void) +{ + return OsInfoSystem::api_simple({"AptUpdateBackground"}); +} + +extern "C" int cp0_system_update_launcher_background(void) +{ + return OsInfoSystem::api_simple({"UpdateLauncherBackground"}); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp new file mode 100644 index 00000000..133ad345 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp @@ -0,0 +1,756 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" +#include "../cp0_app_internal_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#include +#include +#include +#include +#endif + +#if !defined(HAL_PLATFORM_SDL) && !defined(_WIN32) +#include +#endif + +extern "C" { + extern void keyboard_pause(void); + extern void keyboard_resume(void); +} + +extern "C" void __attribute__((weak)) keyboard_pause(void) {} +extern "C" void __attribute__((weak)) keyboard_resume(void) {} + +class ProcessSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "ExecBlocking") { + const std::string exec_path = nth_arg(arg, 1); + volatile int *home_key_flag = decode_flag_ptr(nth_arg(arg, 2)); + int keep_root = std::atoi(nth_arg(arg, 3).c_str()); + report(callback, exec_blocking(exec_path.c_str(), home_key_flag, keep_root), ""); + } else if (cmd == "Spawn") { + const std::string exec_path = nth_arg(arg, 1); + int keep_root = std::atoi(nth_arg(arg, 2).c_str()); + cp0_pid_t pid = spawn(exec_path.c_str(), keep_root); + report(callback, pid < 0 ? -1 : 0, std::to_string(pid)); + } else if (cmd == "Stop") { + stop(static_cast(std::atoi(nth_arg(arg, 1).c_str()))); + report(callback, 0, ""); + } else if (cmd == "CheckLock") { + int holder_pid = 0; + int ret = check_lock(nth_arg(arg, 1).c_str(), &holder_pid); + report(callback, ret, std::to_string(holder_pid)); + } else if (cmd == "Kill") { + int pid = std::atoi(nth_arg(arg, 1).c_str()); + int grace_ms = std::atoi(nth_arg(arg, 2).c_str()); + kill_process(pid, grace_ms); + report(callback, 0, ""); + } else if (cmd == "RunArgv") { + int background = std::atoi(nth_arg(arg, 1).c_str()); + std::vector argv = args_from(arg, 2); + report(callback, run_argv(argv, background), ""); + } else if (cmd == "CaptureArgv") { + std::vector argv = args_from(arg, 1); + std::string output; + int ret = capture_argv(argv, output); + report(callback, ret, output); + } else if (cmd == "Shutdown") { + system_shutdown(); + report(callback, 0, ""); + } else if (cmd == "Reboot") { + system_reboot(); + report(callback, 0, ""); + } else { + report(callback, -1, "unknown process api command"); + } + } + + int exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) + { +#if defined(_WIN32) + (void)exec_path; + (void)home_key_flag; + (void)keep_root; + return -1; +#elif defined(HAL_PLATFORM_SDL) + return exec_blocking_sdl(exec_path, home_key_flag, keep_root); +#else + return exec_blocking_cp0(exec_path, home_key_flag, keep_root); +#endif + } + + cp0_pid_t spawn(const char *exec_path, int keep_root) + { +#if defined(_WIN32) + (void)exec_path; + (void)keep_root; + return -1; +#else + pid_t pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + setpgid(0, 0); + if (keep_root) + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); + else + exec_as_user(exec_path); + _exit(127); + } + setpgid(pid, pid); + return static_cast(pid); +#endif + } + + void stop(cp0_pid_t pid) + { +#if !defined(_WIN32) + if (pid <= 0) + return; + killpg(static_cast(pid), SIGTERM); + int status = 0; + waitpid(static_cast(pid), &status, WNOHANG); +#else + (void)pid; +#endif + } + + int check_lock(const char *lock_path, int *holder_pid) + { + if (holder_pid) + *holder_pid = 0; +#if defined(_WIN32) + (void)lock_path; + return 0; +#else + if (!lock_path || !holder_pid) + return -1; + int fd = open(lock_path, O_CREAT | O_RDWR, 0666); + if (fd < 0) + return -1; + struct flock fl; + std::memset(&fl, 0, sizeof(fl)); + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + if (fcntl(fd, F_GETLK, &fl) == -1) { + close(fd); + return -1; + } + close(fd); + if (fl.l_type != F_UNLCK) { + *holder_pid = fl.l_pid; + return fl.l_pid; + } + return 0; +#endif + } + + void kill_process(int pid, int grace_ms) + { +#if !defined(_WIN32) + if (pid <= 0) + return; + killpg(pid, SIGINT); + auto start = std::chrono::steady_clock::now(); + while (true) { + int status = 0; + if (waitpid(pid, &status, WNOHANG) != 0) + return; + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - start).count() >= grace_ms) { + killpg(pid, SIGKILL); + waitpid(pid, &status, 0); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +#else + (void)pid; + (void)grace_ms; +#endif + } + + int run_argv(const std::vector &argv, int background) + { +#if defined(_WIN32) + (void)argv; + (void)background; + return -1; +#else + if (argv.empty() || argv[0].empty()) + return -EINVAL; + + pid_t pid = fork(); + if (pid < 0) + return -errno; + + if (pid == 0) { + if (background) + redirect_to_devnull(); + auto raw = make_argv(argv); + execvp(raw[0], raw.data()); + _exit(127); + } + + if (background) + return 0; + + int status = 0; + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + return -errno; + } + if (WIFEXITED(status)) + return WEXITSTATUS(status); + if (WIFSIGNALED(status)) + return 128 + WTERMSIG(status); + return -1; +#endif + } + + int capture_argv(const std::vector &argv, std::string &output) + { + output.clear(); +#if defined(_WIN32) + (void)argv; + return -1; +#else + if (argv.empty() || argv[0].empty()) + return -EINVAL; + + int pipefd[2]; + if (pipe(pipefd) != 0) + return -errno; + + pid_t pid = fork(); + if (pid < 0) { + close(pipefd[0]); + close(pipefd[1]); + return -errno; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + dup2(devnull, STDERR_FILENO); + if (devnull > STDERR_FILENO) + close(devnull); + } + if (pipefd[1] > STDERR_FILENO) + close(pipefd[1]); + auto raw = make_argv(argv); + execvp(raw[0], raw.data()); + _exit(127); + } + + close(pipefd[1]); + char buf[256]; + ssize_t n = 0; + while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) + output.append(buf, static_cast(n)); + close(pipefd[0]); + + int status = 0; + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + return -errno; + } + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +#endif + } + + void system_shutdown() + { +#if defined(HAL_PLATFORM_SDL) || defined(_WIN32) + std::printf("[CP0] shutdown (emulator exit)\n"); + std::exit(0); +#else + std::printf("[CP0] shutdown\n"); + const std::vector argv = {"sudo", "shutdown", "-h", "now"}; + run_argv(argv, 1); +#endif + } + + void system_reboot() + { +#if defined(HAL_PLATFORM_SDL) || defined(_WIN32) + std::printf("[CP0] reboot (emulator exit)\n"); + std::exit(0); +#else + std::printf("[CP0] reboot\n"); + const std::vector argv = {"sudo", "reboot"}; + run_argv(argv, 1); +#endif + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static std::vector args_from(const arg_t &arg, size_t index) + { + std::vector out; + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + for (; it != arg.end(); ++it) + out.push_back(*it); + return out; + } + + static volatile int *decode_flag_ptr(const std::string &text) + { + if (text.empty() || text == "0") + return nullptr; + uintptr_t raw = static_cast(std::strtoull(text.c_str(), nullptr, 10)); + return reinterpret_cast(raw); + } + + static bool is_nologin_shell(const char *shell) + { + if (!shell || !shell[0]) + return true; + return std::strstr(shell, "nologin") != nullptr || std::strstr(shell, "/bin/false") != nullptr; + } + + static std::string config_get_str(const char *key, const char *default_val) + { + std::string value = default_val ? default_val : ""; + cp0_signal_config_api({"GetStr", key ? std::string(key) : std::string(), value}, + [&](int code, std::string data) { + if (code == 0) value = std::move(data); + }); + return value; + } + + static const char *get_run_user() + { + static thread_local std::string cfg; + cfg = config_get_str("run_as_user", nullptr); + if (!cfg.empty()) + return cfg.c_str(); + + struct passwd *pwd; + setpwent(); + while ((pwd = getpwent()) != nullptr) { + if (pwd->pw_uid >= 1000 && pwd->pw_uid < 65534 && !is_nologin_shell(pwd->pw_shell)) { + endpwent(); + return pwd->pw_name; + } + } + endpwent(); + return "pi"; + } + + static void exec_as_user(const char *exec_path) + { +#if defined(_WIN32) + (void)exec_path; +#else + const char *user = get_run_user(); + if (getuid() == 0 && std::strcmp(user, "root") != 0) { + struct passwd *pw = getpwnam(user); + if (pw) { + initgroups(pw->pw_name, pw->pw_gid); + setgid(pw->pw_gid); + setuid(pw->pw_uid); + setenv("HOME", pw->pw_dir, 1); + setenv("USER", pw->pw_name, 1); + setenv("LOGNAME", pw->pw_name, 1); + setenv("SHELL", pw->pw_shell[0] ? pw->pw_shell : "/bin/bash", 1); + chdir(pw->pw_dir); + } + } + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); +#endif + } + + static std::vector make_argv(const std::vector &argv) + { + std::vector raw; + raw.reserve(argv.size() + 1); + for (const auto &arg : argv) + raw.push_back(const_cast(arg.c_str())); + raw.push_back(nullptr); + return raw; + } + + static void redirect_to_devnull() + { +#if !defined(_WIN32) + int fd = open("/dev/null", O_RDWR); + if (fd < 0) + return; + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) + close(fd); +#endif + } + + int exec_blocking_sdl(const char *exec_path, volatile int *home_key_flag, int keep_root) + { +#if defined(_WIN32) + (void)exec_path; + (void)home_key_flag; + (void)keep_root; + return -1; +#else + (void)keep_root; + pid_t pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); + _exit(127); + } + int status = 0; + int home_status = 0; + std::chrono::steady_clock::time_point home_start; + while (true) { + int r = waitpid(pid, &status, WNOHANG); + if (r > 0) + break; + if (r < 0) { + status = 0; + break; + } + + if (home_key_flag) { + if (home_status == 0 && *home_key_flag) { + home_status = 1; + home_start = std::chrono::steady_clock::now(); + } else if (home_status == 1) { + if (*home_key_flag) { + auto elapsed = std::chrono::steady_clock::now() - home_start; + if (std::chrono::duration_cast(elapsed).count() >= 5) { + home_status = 2; + kill(pid, SIGINT); + } + } else { + home_status = 0; + } + } else if (home_status == 2) { + auto elapsed = std::chrono::steady_clock::now() - home_start; + if (std::chrono::duration_cast(elapsed).count() >= 8) { + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + break; + } + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + if (home_key_flag) + *home_key_flag = 0; + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +#endif + } + + int exec_blocking_cp0(const char *exec_path, volatile int *home_key_flag, int keep_root) + { +#if defined(_WIN32) || defined(HAL_PLATFORM_SDL) + return exec_blocking_sdl(exec_path, home_key_flag, keep_root); +#else + (void)home_key_flag; + keyboard_pause(); + + int evfd = open(get_kbd_device(), O_RDONLY | O_NONBLOCK); + if (evfd < 0) { + std::perror("[cp0] open evdev"); + keyboard_resume(); + return -1; + } + std::printf("[cp0] Opened evdev %s (no EVIOCGRAB; shared with child)\n", get_kbd_device()); + std::fflush(stdout); + + pid_t pid = fork(); + if (pid < 0) { + close(evfd); + keyboard_resume(); + return -1; + } + if (pid == 0) { + close(evfd); + setpgid(0, 0); + if (keep_root) + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); + else + exec_as_user(exec_path); + _exit(127); + } + setpgid(pid, pid); + + auto esc_down_since = std::chrono::steady_clock::time_point{}; + bool esc_down = false; + int status = 0; + + while (true) { + int r = waitpid(pid, &status, WNOHANG); + if (r > 0) + break; + if (r < 0) { + status = -1; + break; + } + + struct input_event ev; + while (read(evfd, &ev, sizeof(ev)) == static_cast(sizeof(ev))) { + if (ev.type == EV_KEY && ev.code == KEY_ESC) { + if (ev.value == 1) { + esc_down = true; + esc_down_since = std::chrono::steady_clock::now(); + } else if (ev.value == 0) { + esc_down = false; + } + } + } + + if (esc_down) { + auto held_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - esc_down_since).count(); + if (held_ms >= 3000) { + killpg(pid, SIGTERM); + auto t0 = std::chrono::steady_clock::now(); + while (waitpid(pid, &status, WNOHANG) == 0) { + if (std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count() >= 3) { + killpg(pid, SIGKILL); + waitpid(pid, &status, 0); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + close(evfd); + keyboard_resume(); + std::printf("[cp0] Returned to launcher\n"); + std::fflush(stdout); + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +#endif + } + + static const char *get_kbd_device() + { + const char *env = std::getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); + return env ? env : "/dev/input/by-path/platform-3f804000.i2c-event"; + } + +public: + static int api_simple(const arg_t &arg, std::string *data = nullptr) + { + int result = -1; + cp0_signal_process_api(arg, [&](int code, std::string out) { + result = code; + if (data) + *data = std::move(out); + }); + return result; + } +}; + +static bool contains_shell_meta(const char *s) +{ + if (!s) + return true; + static const char *kMeta = "|&;<>`$\\\n\r"; + return std::strpbrk(s, kMeta) != nullptr; +} + +static std::string first_token(const char *exec) +{ + std::istringstream iss(exec ? exec : ""); + std::string token; + iss >> token; + return token; +} + +static bool file_executable(const std::string &path) +{ +#if defined(_WIN32) + (void)path; + return false; +#else + struct stat st; + return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode) && access(path.c_str(), X_OK) == 0; +#endif +} + +extern "C" void init_process(void) +{ + auto process = std::make_shared(); + cp0_signal_process_api.append([process](std::list arg, std::function callback) { + process->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ + return ProcessSystem::api_simple({"ExecBlocking", exec_path ? exec_path : "", + std::to_string(reinterpret_cast(home_key_flag)), + std::to_string(keep_root)}); +} + +extern "C" cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) +{ + std::string data; + int ret = ProcessSystem::api_simple({"Spawn", exec_path ? exec_path : "", std::to_string(keep_root)}, &data); + return ret == 0 ? static_cast(std::atoi(data.c_str())) : -1; +} + +extern "C" void cp0_process_stop(cp0_pid_t pid) +{ + ProcessSystem::api_simple({"Stop", std::to_string(pid)}); +} + +extern "C" int cp0_process_check_lock(const char *lock_path, int *holder_pid) +{ + std::string data; + int ret = ProcessSystem::api_simple({"CheckLock", lock_path ? lock_path : ""}, &data); + if (holder_pid) + *holder_pid = std::atoi(data.c_str()); + return ret; +} + +extern "C" void cp0_process_kill(int pid, int grace_ms) +{ + ProcessSystem::api_simple({"Kill", std::to_string(pid), std::to_string(grace_ms)}); +} + +extern "C" int cp0_process_run_argv(const char *const *argv, int background) +{ + std::list args = {"RunArgv", std::to_string(background)}; + if (argv) { + for (int i = 0; argv[i]; ++i) + args.push_back(argv[i]); + } + return ProcessSystem::api_simple(args); +} + +extern "C" int cp0_process_capture_argv(const char *const *argv, char *out, int out_size) +{ + if (out && out_size > 0) + out[0] = '\0'; + std::list args = {"CaptureArgv"}; + if (argv) { + for (int i = 0; argv[i]; ++i) + args.push_back(argv[i]); + } + std::string data; + int ret = ProcessSystem::api_simple(args, &data); + if (out && out_size > 0) + cp0_copy_string(out, out_size, data); + return ret; +} + +extern "C" int cp0_file_read_first_line(const char *path, char *out, int out_size) +{ + if (out && out_size > 0) + out[0] = '\0'; + if (!path || !out || out_size <= 0) + return -1; + std::ifstream file(path); + if (!file.is_open()) + return -1; + std::string line; + if (!std::getline(file, line)) + return -1; + if (!line.empty() && line.back() == '\r') + line.pop_back(); + cp0_copy_string(out, out_size, line); + return 0; +} + +extern "C" int cp0_desktop_exec_is_safe(const char *exec, char *reason, int reason_size) +{ + auto fail = [reason, reason_size](const char *msg) { + cp0_copy_cstr(reason, reason_size, msg ? msg : "unsafe Exec"); + return 0; + }; + + if (!exec || !exec[0]) + return fail("empty Exec"); + if (std::strlen(exec) > 512) + return fail("Exec too long"); + if (contains_shell_meta(exec)) + return fail("Exec contains shell metacharacters"); + + const std::string token = first_token(exec); + if (token.empty()) + return fail("missing executable"); + + if (token.find('/') != std::string::npos) { + if (!file_executable(token)) + return fail("executable path is not executable"); + return 1; + } + + static const char *kAllowedNames[] = {"bash", "python3", "vim", "vi", "nano", "sh"}; + if (std::find(std::begin(kAllowedNames), std::end(kAllowedNames), token) != std::end(kAllowedNames)) + return 1; + + return fail("executable name is not allowlisted"); +} + +extern "C" void cp0_system_shutdown(void) +{ + ProcessSystem::api_simple({"Shutdown"}); +} + +extern "C" void cp0_system_reboot(void) +{ + ProcessSystem::api_simple({"Reboot"}); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp new file mode 100644 index 00000000..78aea037 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp @@ -0,0 +1,336 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +namespace { + +typedef void *pty_handle_t; + +struct cp0_pty_handle { + int master_fd; + pid_t child_pid; +}; + +class PtySystem { +public: + typedef std::function callback_t; + typedef std::list arg_t; + + pty_handle_t open(const char *cmd, const char *const *args, int cols, int rows) + { +#if defined(__linux__) + if (!cmd || !cmd[0]) return NULL; + + int master_fd = -1; + struct winsize ws = {}; + ws.ws_col = cols; + ws.ws_row = rows; + std::string run_as_user = config_get_str("run_as_user", ""); + + pid_t pid = forkpty(&master_fd, NULL, NULL, &ws); + if (pid < 0) return NULL; + + if (pid == 0) { + setenv("TERM", "vt100", 1); + drop_root_user(run_as_user); + + if (args) + execvp(cmd, const_cast(args)); + else + execlp(cmd, cmd, static_cast(NULL)); + _exit(127); + } + + int flags = fcntl(master_fd, F_GETFL); + if (flags >= 0) fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); + + cp0_pty_handle *pty = static_cast(std::malloc(sizeof(cp0_pty_handle))); + if (!pty) { + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + ::close(master_fd); + return NULL; + } + pty->master_fd = master_fd; + pty->child_pid = pid; + return pty; +#else + (void)cmd; + (void)args; + (void)cols; + (void)rows; + return NULL; +#endif + } + + int read(pty_handle_t pty, char *buf, size_t buf_size) + { +#if defined(__linux__) + if (!pty || !buf || buf_size == 0) return -1; + cp0_pty_handle *h = static_cast(pty); + ssize_t n = ::read(h->master_fd, buf, buf_size); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; + return -1; + } + return static_cast(n); +#else + (void)pty; + (void)buf; + (void)buf_size; + return -1; +#endif + } + + int write(pty_handle_t pty, const char *buf, size_t len) + { +#if defined(__linux__) + if (!pty || !buf) return -1; + cp0_pty_handle *h = static_cast(pty); + return static_cast(::write(h->master_fd, buf, len)); +#else + (void)pty; + (void)buf; + (void)len; + return -1; +#endif + } + + int check_child(pty_handle_t pty, int *exit_status) + { +#if defined(__linux__) + if (!pty) return -1; + cp0_pty_handle *h = static_cast(pty); + int status = 0; + pid_t r = waitpid(h->child_pid, &status, WNOHANG); + if (r == 0) return 0; + if (r > 0) { + if (exit_status) *exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + return 1; + } + return -1; +#else + (void)pty; + (void)exit_status; + return -1; +#endif + } + + void close(pty_handle_t pty) + { +#if defined(__linux__) + if (!pty) return; + cp0_pty_handle *h = static_cast(pty); + kill(h->child_pid, SIGKILL); + waitpid(h->child_pid, NULL, 0); + ::close(h->master_fd); + std::free(h); +#else + (void)pty; +#endif + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty pty api\n"); + return; + } + + const std::string cmd = arg.front(); + if (cmd == "Open") { + api_open(arg, callback); + } else if (cmd == "Read") { + api_read(arg, callback); + } else if (cmd == "Write") { + api_write(arg, callback); + } else if (cmd == "CheckChild") { + api_check_child(arg, callback); + } else if (cmd == "Close") { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + close(pty); + report(callback, 0, ""); + } else { + report(callback, -1, "unknown pty api: " + cmd + "\n"); + } + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + if (index >= arg.size()) return ""; + auto it = arg.begin(); + std::advance(it, index); + return *it; + } + + static std::string handle_to_string(pty_handle_t pty) + { + std::ostringstream os; + os << reinterpret_cast(pty); + return os.str(); + } + + static pty_handle_t parse_handle(const std::string &value) + { + if (value.empty()) return NULL; + char *end = NULL; + uintptr_t raw = static_cast(std::strtoull(value.c_str(), &end, 0)); + if (!end || *end != '\0') return NULL; + return reinterpret_cast(raw); + } + + static int parse_int(const std::string &value, int fallback) + { + if (value.empty()) return fallback; + char *end = NULL; + long parsed = std::strtol(value.c_str(), &end, 10); + return (end && *end == '\0') ? static_cast(parsed) : fallback; + } + + static std::string config_get_str(const char *key, const char *default_val) + { + std::string value = default_val ? default_val : ""; + cp0_signal_config_api({"GetStr", key ? std::string(key) : std::string(), value}, + [&](int code, std::string data) { + if (code == 0) value = std::move(data); + }); + return value; + } + + static void drop_root_user(const std::string &configured_user) + { +#if defined(__linux__) + if (getuid() != 0) return; + + const char *username = configured_user.empty() ? NULL : configured_user.c_str(); + if (!username) { + struct passwd *p = NULL; + setpwent(); + while ((p = getpwent()) != NULL) { + if (p->pw_uid >= 1000 && p->pw_uid < 65534 && + p->pw_shell && p->pw_shell[0] && + !std::strstr(p->pw_shell, "nologin") && + !std::strstr(p->pw_shell, "/bin/false")) { + username = p->pw_name; + break; + } + } + endpwent(); + } + if (!username) username = "pi"; + + struct passwd *pw = getpwnam(username); + if (pw && std::strcmp(username, "root") != 0) { + initgroups(pw->pw_name, pw->pw_gid); + setgid(pw->pw_gid); + setuid(pw->pw_uid); + setenv("HOME", pw->pw_dir, 1); + setenv("USER", pw->pw_name, 1); + setenv("LOGNAME", pw->pw_name, 1); + setenv("SHELL", pw->pw_shell[0] ? pw->pw_shell : "/bin/bash", 1); + chdir(pw->pw_dir); + } +#endif + } + + void api_open(const arg_t &arg, callback_t callback) + { + std::string exec = nth_arg(arg, 1); + int cols = parse_int(nth_arg(arg, 2), 80); + int rows = parse_int(nth_arg(arg, 3), 24); + if (exec.empty()) { + report(callback, -1, "empty pty command\n"); + return; + } + + std::vector argv_storage; + for (auto it = arg.begin(); it != arg.end(); ++it) { + size_t index = static_cast(std::distance(arg.begin(), it)); + if (index >= 4) argv_storage.push_back(*it); + } + + std::vector argv; + if (!argv_storage.empty()) { + for (const std::string &item : argv_storage) argv.push_back(item.c_str()); + argv.push_back(NULL); + } + + pty_handle_t pty = open(exec.c_str(), argv.empty() ? NULL : argv.data(), cols, rows); + if (!pty) { + report(callback, -1, "open pty failed\n"); + return; + } + report(callback, 0, handle_to_string(pty)); + } + + void api_read(const arg_t &arg, callback_t callback) + { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + int max_len = parse_int(nth_arg(arg, 2), 4096); + if (max_len <= 0) max_len = 4096; + + std::string out(static_cast(max_len), '\0'); + int n = read(pty, &out[0], out.size()); + if (n < 0) { + report(callback, -1, "read pty failed\n"); + return; + } + out.resize(static_cast(n)); + report(callback, n, out); + } + + void api_write(const arg_t &arg, callback_t callback) + { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + std::string data = nth_arg(arg, 2); + int n = write(pty, data.c_str(), data.size()); + report(callback, n, n < 0 ? "write pty failed\n" : ""); + } + + void api_check_child(const arg_t &arg, callback_t callback) + { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + int status = 0; + int ret = check_child(pty, &status); + report(callback, ret, std::to_string(status)); + } +}; + +} // namespace + +extern "C" void init_pty(void) +{ + std::shared_ptr pty = std::make_shared(); + cp0_signal_pty_api.append([pty](std::list arg, std::function callback) { + pty->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp new file mode 100644 index 00000000..2d57493b --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp @@ -0,0 +1,188 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void write_le16(FILE *f, uint16_t v) { fwrite(&v, 2, 1, f); } +static void write_le32(FILE *f, uint32_t v) { fwrite(&v, 4, 1, f); } + +namespace { + +class ScreenshotSystem { +public: + typedef std::function callback_t; + typedef std::list arg_t; + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty screenshot api\n"); + return; + } + + if (arg.front() == "Save") { + Save(std::move(arg), std::move(callback)); + return; + } + + report(callback, -1, "unknown screenshot api\n"); + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) callback(code, data); + } + + static std::string first_arg_after_command(const arg_t &arg) + { + if (arg.size() < 2) return ""; + return *std::next(arg.begin()); + } + + void Save(arg_t arg, callback_t callback) + { + std::string dir = first_arg_after_command(arg); + if (dir.empty()) { + report(callback, -1, "Save need dir\n"); + return; + } + + int ret = save_to_bmp(dir.c_str()); + report(callback, ret, ret == 0 ? "screenshot saved\n" : "screenshot failed\n"); + } + + static int save_to_bmp(const char *dir) + { + const char *fbdev = getenv("APPLAUNCH_LINUX_FBDEV_DEVICE"); + if (!fbdev) fbdev = "/dev/fb0"; + + int fd = open(fbdev, O_RDONLY); + if (fd < 0) return -1; + + struct fb_var_screeninfo vinfo; + if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) < 0) { + close(fd); + return -2; + } + + int w = vinfo.xres; + int h = vinfo.yres; + int bpp = vinfo.bits_per_pixel; + int fb_line_len = w * (bpp / 8); + + struct fb_fix_screeninfo finfo; + if (ioctl(fd, FBIOGET_FSCREENINFO, &finfo) == 0) + fb_line_len = finfo.line_length; + + size_t fb_size = fb_line_len * h; + void *fbmem = mmap(NULL, fb_size, PROT_READ, MAP_SHARED, fd, 0); + if (fbmem == MAP_FAILED) { + close(fd); + return -3; + } + + { + struct stat st; + if (stat(dir, &st) != 0) { + char tmp[512]; + snprintf(tmp, sizeof(tmp), "%s", dir); + for (char *p = tmp + 1; *p; ++p) { + if (*p == '/') { *p = 0; mkdir(tmp, 0755); *p = '/'; } + } + mkdir(tmp, 0755); + } + } + + time_t now = time(NULL); + struct tm *t = localtime(&now); + char filename[512]; + snprintf(filename, sizeof(filename), "%s/scr_%04d%02d%02d_%02d%02d%02d.bmp", + dir, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec); + + FILE *fp = fopen(filename, "wb"); + if (!fp) { + munmap(fbmem, fb_size); + close(fd); + return -4; + } + + int row_size = w * 3; + int pad = (4 - (row_size % 4)) % 4; + int bmp_row = row_size + pad; + uint32_t img_size = bmp_row * h; + uint32_t file_size = 54 + img_size; + + fputc('B', fp); fputc('M', fp); + write_le32(fp, file_size); + write_le16(fp, 0); write_le16(fp, 0); + write_le32(fp, 54); + write_le32(fp, 40); + write_le32(fp, w); + write_le32(fp, h); + write_le16(fp, 1); + write_le16(fp, 24); + write_le32(fp, 0); + write_le32(fp, img_size); + write_le32(fp, 2835); write_le32(fp, 2835); + write_le32(fp, 0); write_le32(fp, 0); + + uint8_t padding[3] = {0}; + for (int y = h - 1; y >= 0; --y) { + uint8_t *row = (uint8_t *)fbmem + y * fb_line_len; + for (int x = 0; x < w; ++x) { + uint8_t r, g, b; + if (bpp == 16) { + uint16_t px = ((uint16_t *)row)[x]; + r = ((px >> 11) & 0x1F) << 3; + g = ((px >> 5) & 0x3F) << 2; + b = (px & 0x1F) << 3; + } else if (bpp == 32) { + uint32_t px = ((uint32_t *)row)[x]; + r = (px >> vinfo.red.offset) & 0xFF; + g = (px >> vinfo.green.offset) & 0xFF; + b = (px >> vinfo.blue.offset) & 0xFF; + } else { + r = g = b = 0; + } + uint8_t bgr[3] = {b, g, r}; + fwrite(bgr, 1, 3, fp); + } + if (pad > 0) fwrite(padding, 1, pad, fp); + } + + fclose(fp); + munmap(fbmem, fb_size); + close(fd); + + printf("[SCREENSHOT] Saved: %s (%dx%d %dbpp)\n", filename, w, h, bpp); + return 0; + } +}; + +} // namespace + +extern "C" void init_screenshot(void) +{ + auto screenshot = std::make_shared(); + cp0_signal_screenshot_api.append([screenshot](std::list arg, std::function callback) { + screenshot->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp new file mode 100644 index 00000000..a003ffe5 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp @@ -0,0 +1,403 @@ +#include "hal_lvgl_bsp.h" +#include "cp0_lvgl_app.h" + +#ifdef HAL_PLATFORM_SDL +#include "hal/hal_settings.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class SettingsSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "BacklightRead") { + int val = backlight_read(); + report(callback, val < 0 ? -1 : 0, std::to_string(val)); + } else if (cmd == "BacklightMax") { + int val = backlight_max(); + report(callback, val < 0 ? -1 : 0, std::to_string(val)); + } else if (cmd == "BacklightWrite") { + int val = backlight_write(std::atoi(nth_arg(arg, 1).c_str())); + report(callback, val < 0 ? -1 : 0, std::to_string(val)); + } else if (cmd == "BtStatus") { + report(callback, 0, encode_bt_status(bt_get_status())); + } else if (cmd == "BtPower") { + report(callback, bt_set_power(std::atoi(nth_arg(arg, 1).c_str())), ""); + } else if (cmd == "BtScan") { + int max_count = arg.size() >= 2 ? std::atoi(nth_arg(arg, 1).c_str()) : CP0_BT_DEVICE_MAX; + std::vector devices(std::max(0, max_count)); + int count = bt_scan(devices.empty() ? nullptr : devices.data(), static_cast(devices.size())); + report(callback, count, encode_bt_scan(devices.data(), count)); + } else if (cmd == "TimeStr") { + char buf[32] = {}; + time_str(buf, sizeof(buf)); + report(callback, 0, buf); + } else { + report(callback, -1, "unknown settings api command"); + } + } + + static int api_int(const arg_t &arg, int default_value = -1) + { + int result = default_value; + cp0_signal_settings_api(arg, [&](int code, std::string data) { + if (code >= 0) + result = std::atoi(data.c_str()); + }); + return result; + } + + static cp0_bt_status_t api_bt_status() + { + cp0_bt_status_t st{}; + cp0_signal_settings_api({"BtStatus"}, [&](int code, std::string data) { + if (code == 0) + decode_bt_status(data, st); + }); + return st; + } + + static int api_bt_power(int on) + { + int result = -1; + cp0_signal_settings_api({"BtPower", std::to_string(on)}, [&](int code, std::string) { + result = code; + }); + return result; + } + + static int api_bt_scan(cp0_bt_device_t *out, int max_devices) + { + int count = 0; + cp0_signal_settings_api({"BtScan", std::to_string(max_devices)}, [&](int code, std::string data) { + count = out && max_devices > 0 ? decode_bt_scan(data, out, max_devices) : code; + }); + return count; + } + + static void api_time_str(char *buf, int buf_size) + { + if (!buf || buf_size <= 0) + return; + buf[0] = '\0'; + cp0_signal_settings_api({"TimeStr"}, [&](int code, std::string data) { + if (code == 0) + copy_string(buf, static_cast(buf_size), data); + }); + if (buf[0] == '\0') + fallback_time_str(buf, buf_size); + } + +private: + void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + std::advance(it, std::min(index, arg.size())); + return it == arg.end() ? std::string() : *it; + } + + static void copy_string(char *dst, size_t dst_size, const std::string &src) + { + if (!dst || dst_size == 0) + return; + std::strncpy(dst, src.c_str(), dst_size - 1); + dst[dst_size - 1] = '\0'; + } + + static std::vector split_char(const std::string &line, char delimiter) + { + std::vector cols; + std::string item; + std::istringstream iss(line); + while (std::getline(iss, item, delimiter)) + cols.push_back(item); + return cols; + } + + static std::string encode_bt_status(const cp0_bt_status_t &st) + { + std::ostringstream oss; + oss << st.powered << '\t' << st.address; + return oss.str(); + } + + static bool decode_bt_status(const std::string &data, cp0_bt_status_t &st) + { + auto cols = split_char(data, '\t'); + if (cols.size() < 2) + return false; + st = {}; + st.powered = std::atoi(cols[0].c_str()); + copy_string(st.address, sizeof(st.address), cols[1]); + return true; + } + + static std::string encode_bt_scan(const cp0_bt_device_t *devices, int count) + { + std::ostringstream oss; + for (int i = 0; devices && i < count; ++i) { + oss << devices[i].address << '\t' << devices[i].rssi << '\t' << devices[i].connected << '\t' << devices[i].name << '\n'; + } + return oss.str(); + } + + static int decode_bt_scan(const std::string &data, cp0_bt_device_t *out, int max_devices) + { + if (!out || max_devices <= 0) + return 0; + int count = 0; + std::istringstream lines(data); + std::string line; + while (count < max_devices && std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + auto cols = split_char(line, '\t'); + if (cols.size() < 4 || cols[0].empty()) + continue; + cp0_bt_device_t dev{}; + copy_string(dev.address, sizeof(dev.address), cols[0]); + dev.rssi = std::atoi(cols[1].c_str()); + dev.connected = std::atoi(cols[2].c_str()); + copy_string(dev.name, sizeof(dev.name), cols[3]); + out[count++] = dev; + } + return count; + } + + static void fallback_time_str(char *buf, int buf_size) + { + if (!buf || buf_size <= 0) + return; + std::time_t now = std::time(nullptr); + std::tm *t = std::localtime(&now); + if (!t) { + buf[0] = '\0'; + return; + } + std::snprintf(buf, static_cast(buf_size), "%02d:%02d", t->tm_hour, t->tm_min); + } + +#ifdef HAL_PLATFORM_SDL + int backlight_read() { return hal_backlight_read(); } + int backlight_max() { return hal_backlight_max(); } + int backlight_write(int val) { return hal_backlight_write(val); } + + cp0_bt_status_t bt_get_status() + { + hal_bt_status_t hal = hal_bt_get_status(); + cp0_bt_status_t st{}; + st.powered = hal.powered; + copy_string(st.address, sizeof(st.address), hal.address); + return st; + } + + int bt_set_power(int on) { return hal_bt_set_power(on); } + + int bt_scan(cp0_bt_device_t *out, int max_devices) + { + if (!out || max_devices <= 0) + return hal_bt_scan(nullptr, 0); + + std::vector hal_devices(static_cast(max_devices)); + int count = hal_bt_scan(hal_devices.data(), max_devices); + count = std::min(count, max_devices); + for (int i = 0; i < count; ++i) { + copy_string(out[i].name, sizeof(out[i].name), hal_devices[static_cast(i)].name); + copy_string(out[i].address, sizeof(out[i].address), hal_devices[static_cast(i)].address); + out[i].rssi = hal_devices[static_cast(i)].rssi; + out[i].connected = hal_devices[static_cast(i)].connected; + } + return count; + } + + void time_str(char *buf, int buf_size) { hal_time_str(buf, buf_size); } +#else + static int read_int_file(const char *path, int default_value) + { + FILE *f = std::fopen(path, "r"); + if (!f) + return default_value; + int val = default_value; + if (std::fscanf(f, "%d", &val) != 1) + val = default_value; + std::fclose(f); + return val; + } + + int backlight_read() + { + return read_int_file("/sys/class/backlight/backlight/brightness", -1); + } + + int backlight_max() + { + return read_int_file("/sys/class/backlight/backlight/max_brightness", 100); + } + + int backlight_write(int val) + { + if (val < 0) + val = 0; + int mx = backlight_max(); + if (val > mx) + val = mx; + FILE *f = std::fopen("/sys/class/backlight/backlight/brightness", "w"); + if (!f) + return -1; + std::fprintf(f, "%d", val); + std::fclose(f); + return val; + } + + cp0_bt_status_t bt_get_status() + { + cp0_bt_status_t st{}; + char output[4096] = {}; + const char *argv[] = {"bluetoothctl", "show", nullptr}; + if (cp0_process_capture_argv(argv, output, sizeof(output)) != 0) + return st; + + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (line.find("Powered:") != std::string::npos) + st.powered = line.find("yes") != std::string::npos ? 1 : 0; + std::string marker = "Controller "; + size_t pos = line.find(marker); + if (pos != std::string::npos) { + std::string addr = line.substr(pos + marker.size()); + size_t sp = addr.find(' '); + if (sp != std::string::npos) + addr.resize(sp); + copy_string(st.address, sizeof(st.address), addr); + } + } + return st; + } + + int bt_set_power(int on) + { + const char *argv_on[] = {"bluetoothctl", "power", "on", nullptr}; + const char *argv_off[] = {"bluetoothctl", "power", "off", nullptr}; + char output[1024] = {}; + int ret = cp0_process_capture_argv(on ? argv_on : argv_off, output, sizeof(output)); + if (ret != 0) + return -1; + std::string data(output); + return (data.find("succeeded") != std::string::npos || data.find("Changing") != std::string::npos) ? 0 : -1; + } + + int bt_scan(cp0_bt_device_t *out, int max_devices) + { + const char *scan_on[] = {"bluetoothctl", "scan", "on", nullptr}; + const char *scan_off[] = {"bluetoothctl", "scan", "off", nullptr}; + cp0_process_run_argv(scan_on, 1); + std::this_thread::sleep_for(std::chrono::seconds(4)); + cp0_process_run_argv(scan_off, 0); + + char output[8192] = {}; + const char *devices[] = {"bluetoothctl", "devices", nullptr}; + if (cp0_process_capture_argv(devices, output, sizeof(output)) != 0) + return 0; + + int count = 0; + std::istringstream lines(output); + std::string line; + while (out && count < max_devices && std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line.rfind("Device ", 0) != 0) + continue; + std::string rest = line.substr(7); + size_t sp = rest.find(' '); + if (sp == std::string::npos) + continue; + + cp0_bt_device_t dev{}; + std::string addr = rest.substr(0, sp); + std::string name = rest.substr(sp + 1); + copy_string(dev.address, sizeof(dev.address), addr); + copy_string(dev.name, sizeof(dev.name), name.empty() ? addr : name); + dev.rssi = 0; + dev.connected = 0; + out[count++] = dev; + } + return count; + } + + void time_str(char *buf, int buf_size) { fallback_time_str(buf, buf_size); } +#endif +}; + +} // namespace + +extern "C" void init_settings(void) +{ + auto settings = std::make_shared(); + cp0_signal_settings_api.append([settings](std::list arg, std::function callback) { + settings->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int cp0_backlight_read(void) +{ + return SettingsSystem::api_int({"BacklightRead"}); +} + +extern "C" int cp0_backlight_max(void) +{ + return SettingsSystem::api_int({"BacklightMax"}, 100); +} + +extern "C" int cp0_backlight_write(int val) +{ + return SettingsSystem::api_int({"BacklightWrite", std::to_string(val)}); +} + +extern "C" cp0_bt_status_t cp0_bt_get_status(void) +{ + return SettingsSystem::api_bt_status(); +} + +extern "C" int cp0_bt_set_power(int on) +{ + return SettingsSystem::api_bt_power(on); +} + +extern "C" int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) +{ + return SettingsSystem::api_bt_scan(out, max_devices); +} + +extern "C" void cp0_time_str(char *buf, int buf_size) +{ + SettingsSystem::api_time_str(buf, buf_size); +} diff --git a/ext_components/cp0_lvgl/src/cp0_app_internal_utils.h b/ext_components/cp0_lvgl/src/cp0_app_internal_utils.h new file mode 100644 index 00000000..968334b9 --- /dev/null +++ b/ext_components/cp0_lvgl/src/cp0_app_internal_utils.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +static inline void cp0_copy_string(char *dst, int dst_size, const std::string &src) +{ + if (!dst || dst_size <= 0) + return; + snprintf(dst, static_cast(dst_size), "%s", src.c_str()); +} + +static inline void cp0_copy_cstr(char *dst, int dst_size, const char *src) +{ + if (!dst || dst_size <= 0) + return; + snprintf(dst, static_cast(dst_size), "%s", src ? src : ""); +} diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp deleted file mode 100644 index 44b8d23a..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp +++ /dev/null @@ -1,321 +0,0 @@ -#include "cp0_lvgl_app.h" - -#include "hal/hal_audio.h" -#include "hal/hal_config.h" -#include "hal/hal_filesystem.h" -#include "hal/hal_network.h" -#include "hal/hal_process.h" -#include "hal/hal_pty.h" -#include "hal/hal_screenshot.h" -#include "hal/hal_settings.h" - -#include - -extern "C" { - -void cp0_signal_audio_api_play_file(const char *path) -{ - if (path && path[0]) { - hal_audio_play(path); - } -} - -void cp0_signal_audio_api_play_asset(const char *name) -{ - const char *path = cp0_file_path_c(name); - cp0_signal_audio_api_play_file(path && path[0] ? path : name); -} - -void cp0_signal_system_play_asset(const char *name) -{ - cp0_signal_audio_api_play_asset(name); -} - -void cp0_config_init(void) -{ - hal_config_init(); -} - -int cp0_config_get_int(const char *key, int default_val) -{ - return hal_config_get_int(key, default_val); -} - -void cp0_config_set_int(const char *key, int val) -{ - hal_config_set_int(key, val); -} - -const char *cp0_config_get_str(const char *key, const char *default_val) -{ - return hal_config_get_str(key, default_val); -} - -void cp0_config_set_str(const char *key, const char *val) -{ - hal_config_set_str(key, val); -} - -void cp0_config_save(void) -{ - hal_config_save(); -} - -int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) -{ - if (!entries || max_entries <= 0) { - return hal_dir_list(path, nullptr, 0, out_count); - } - - hal_dirent_t hal_entries[max_entries]; - int ret = hal_dir_list(path, hal_entries, max_entries, out_count); - int count = out_count ? *out_count : 0; - if (count > max_entries) { - count = max_entries; - } - - for (int i = 0; i < count; ++i) { - std::strncpy(entries[i].name, hal_entries[i].name, sizeof(entries[i].name) - 1); - entries[i].name[sizeof(entries[i].name) - 1] = '\0'; - entries[i].is_dir = hal_entries[i].is_dir; - } - - return ret; -} - -cp0_watcher_t cp0_dir_watch_start(const char *path) -{ - return reinterpret_cast(hal_dir_watch_start(path)); -} - -int cp0_dir_watch_poll(cp0_watcher_t watcher) -{ - return hal_dir_watch_poll(reinterpret_cast(watcher)); -} - -void cp0_dir_watch_stop(cp0_watcher_t watcher) -{ - hal_dir_watch_stop(reinterpret_cast(watcher)); -} - -int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) -{ - if (!entries || max_entries <= 0) { - return hal_network_list(nullptr, 0, out_count); - } - - hal_netif_info_t hal_entries[max_entries]; - int ret = hal_network_list(hal_entries, max_entries, out_count); - int count = out_count ? *out_count : 0; - if (count > max_entries) { - count = max_entries; - } - - for (int i = 0; i < count; ++i) { - std::strncpy(entries[i].iface, hal_entries[i].iface, sizeof(entries[i].iface) - 1); - entries[i].iface[sizeof(entries[i].iface) - 1] = '\0'; - std::strncpy(entries[i].ipv4, hal_entries[i].ipv4, sizeof(entries[i].ipv4) - 1); - entries[i].ipv4[sizeof(entries[i].ipv4) - 1] = '\0'; - std::strncpy(entries[i].netmask, hal_entries[i].netmask, sizeof(entries[i].netmask) - 1); - entries[i].netmask[sizeof(entries[i].netmask) - 1] = '\0'; - entries[i].is_up = hal_entries[i].is_up; - } - - return ret; -} - -int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) -{ - return hal_process_exec_blocking(exec_path, home_key_flag, keep_root); -} - -cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) -{ - return hal_process_spawn(exec_path, keep_root); -} - -void cp0_process_stop(cp0_pid_t pid) -{ - hal_process_stop(pid); -} - -int cp0_process_check_lock(const char *lock_path, int *holder_pid) -{ - return hal_process_check_lock(lock_path, holder_pid); -} - -void cp0_process_kill(int pid, int grace_ms) -{ - hal_process_kill(pid, grace_ms); -} - -void cp0_system_shutdown(void) -{ - hal_system_shutdown(); -} - -void cp0_system_reboot(void) -{ - hal_system_reboot(); -} - -cp0_pty_t cp0_pty_open(const char *cmd, const char *const *args, int cols, int rows) -{ - return reinterpret_cast(hal_pty_open(cmd, args, cols, rows)); -} - -int cp0_pty_read(cp0_pty_t pty, char *buf, size_t buf_size) -{ - return hal_pty_read(reinterpret_cast(pty), buf, buf_size); -} - -int cp0_pty_write(cp0_pty_t pty, const char *buf, size_t len) -{ - return hal_pty_write(reinterpret_cast(pty), buf, len); -} - -int cp0_pty_check_child(cp0_pty_t pty, int *exit_status) -{ - return hal_pty_check_child(reinterpret_cast(pty), exit_status); -} - -void cp0_pty_close(cp0_pty_t pty) -{ - hal_pty_close(reinterpret_cast(pty)); -} - -int cp0_screenshot_save(const char *dir) -{ - return hal_screenshot_save(dir); -} - -cp0_battery_info_t cp0_battery_read(void) -{ - hal_battery_info_t hal = hal_battery_read(); - cp0_battery_info_t info{}; - info.voltage_mv = hal.voltage_mv; - info.current_ma = hal.current_ma; - info.temperature_c10 = hal.temperature_c10; - info.soc = hal.soc; - info.remain_mah = hal.remain_mah; - info.full_mah = hal.full_mah; - info.flags = hal.flags; - info.avg_current_ma = hal.avg_current_ma; - info.valid = hal.valid; - return info; -} - -int cp0_backlight_read(void) -{ - return hal_backlight_read(); -} - -int cp0_backlight_max(void) -{ - return hal_backlight_max(); -} - -int cp0_backlight_write(int val) -{ - return hal_backlight_write(val); -} - -int cp0_volume_read(void) -{ - return hal_volume_read(); -} - -int cp0_volume_write(int val) -{ - return hal_volume_write(val); -} - -cp0_wifi_status_t cp0_wifi_get_status(void) -{ - hal_wifi_status_t hal = hal_wifi_get_status(); - cp0_wifi_status_t st{}; - st.connected = hal.connected; - std::strncpy(st.ssid, hal.ssid, sizeof(st.ssid) - 1); - std::strncpy(st.ip, hal.ip, sizeof(st.ip) - 1); - st.signal = hal.signal; - return st; -} - -int cp0_wifi_scan(cp0_wifi_ap_t *out, int max_aps) -{ - if (!out || max_aps <= 0) { - return hal_wifi_scan(nullptr, 0); - } - - hal_wifi_ap_t hal_aps[max_aps]; - int count = hal_wifi_scan(hal_aps, max_aps); - if (count > max_aps) { - count = max_aps; - } - - for (int i = 0; i < count; ++i) { - std::strncpy(out[i].ssid, hal_aps[i].ssid, sizeof(out[i].ssid) - 1); - out[i].ssid[sizeof(out[i].ssid) - 1] = '\0'; - out[i].signal = hal_aps[i].signal; - std::strncpy(out[i].security, hal_aps[i].security, sizeof(out[i].security) - 1); - out[i].security[sizeof(out[i].security) - 1] = '\0'; - out[i].in_use = hal_aps[i].in_use; - } - - return count; -} - -int cp0_wifi_connect(const char *ssid, const char *password) -{ - return hal_wifi_connect(ssid, password); -} - -int cp0_wifi_disconnect(void) -{ - return hal_wifi_disconnect(); -} - -cp0_bt_status_t cp0_bt_get_status(void) -{ - hal_bt_status_t hal = hal_bt_get_status(); - cp0_bt_status_t st{}; - st.powered = hal.powered; - std::strncpy(st.address, hal.address, sizeof(st.address) - 1); - return st; -} - -int cp0_bt_set_power(int on) -{ - return hal_bt_set_power(on); -} - -int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) -{ - if (!out || max_devices <= 0) { - return hal_bt_scan(nullptr, 0); - } - - hal_bt_device_t hal_devices[max_devices]; - int count = hal_bt_scan(hal_devices, max_devices); - if (count > max_devices) { - count = max_devices; - } - - for (int i = 0; i < count; ++i) { - std::strncpy(out[i].name, hal_devices[i].name, sizeof(out[i].name) - 1); - out[i].name[sizeof(out[i].name) - 1] = '\0'; - std::strncpy(out[i].address, hal_devices[i].address, sizeof(out[i].address) - 1); - out[i].address[sizeof(out[i].address) - 1] = '\0'; - out[i].rssi = hal_devices[i].rssi; - out[i].connected = hal_devices[i].connected; - } - - return count; -} - -void cp0_time_str(char *buf, int buf_size) -{ - hal_time_str(buf, buf_size); -} - -} diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c b/ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c deleted file mode 100644 index 2968e0a3..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c +++ /dev/null @@ -1,86 +0,0 @@ -#include "hal/hal_audio.h" -#include - -#ifdef EMU_HAS_AUDIO -#include -#include - -static int g_audio_ready = 0; -static Mix_Music *g_music = NULL; - -void hal_audio_init(void) -{ - if (g_audio_ready) return; - if (SDL_WasInit(SDL_INIT_AUDIO) == 0) - SDL_InitSubSystem(SDL_INIT_AUDIO); - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) == 0) { - g_audio_ready = 1; - } else { - fprintf(stderr, "[HAL] audio init failed: %s\n", Mix_GetError()); - } -} - -void hal_audio_play(const char *path) -{ - if (!g_audio_ready) hal_audio_init(); - if (!g_audio_ready) return; - if (g_music) { - Mix_HaltMusic(); - Mix_FreeMusic(g_music); - g_music = NULL; - } - g_music = Mix_LoadMUS(path); - if (g_music) { - Mix_PlayMusic(g_music, 1); - printf("[HAL] Playing: %s\n", path); - } else { - fprintf(stderr, "[HAL] Failed to load %s: %s\n", path, Mix_GetError()); - } -} - -void hal_audio_stop(void) -{ - if (g_audio_ready) Mix_HaltMusic(); - if (g_music) { - Mix_FreeMusic(g_music); - g_music = NULL; - } -} - -void hal_audio_deinit(void) -{ - hal_audio_stop(); - if (g_audio_ready) { - Mix_CloseAudio(); - g_audio_ready = 0; - } -} - -void hal_audio_play_sync(const char *path) -{ - if (!g_audio_ready) hal_audio_init(); - if (!g_audio_ready) return; - if (g_music) { - Mix_HaltMusic(); - Mix_FreeMusic(g_music); - g_music = NULL; - } - g_music = Mix_LoadMUS(path); - if (g_music) { - Mix_PlayMusic(g_music, 1); - printf("[HAL] Playing (sync): %s\n", path); - while (Mix_PlayingMusic()) SDL_Delay(50); - Mix_FreeMusic(g_music); - g_music = NULL; - } -} - -#else - -void hal_audio_init(void) {} -void hal_audio_play(const char *path) { (void)path; } -void hal_audio_play_sync(const char *path) { (void)path; } -void hal_audio_stop(void) {} -void hal_audio_deinit(void) {} - -#endif diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp deleted file mode 100644 index 535fffbc..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_config_sdl.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "hal/hal_config.h" -#include -#include -#include - -void hal_config_init(void) {} -int hal_config_get_int(const char *key, int default_val) { (void)key; return default_val; } -void hal_config_set_int(const char *key, int val) { (void)key; (void)val; } -const char *hal_config_get_str(const char *key, const char *default_val) { (void)key; return default_val; } -void hal_config_set_str(const char *key, const char *val) { (void)key; (void)val; } -void hal_config_save(void) {} diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp deleted file mode 100644 index 456a1a9e..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_filesystem_sdl.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "hal/hal_filesystem.h" -#include // snprintf -#include -#include - -#ifdef _WIN32 -#include - -int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) -{ - *out_count = 0; - char pattern[512]; - snprintf(pattern, sizeof(pattern), "%s\\*", path); - WIN32_FIND_DATAA fd; - HANDLE h = FindFirstFileA(pattern, &fd); - if (h == INVALID_HANDLE_VALUE) return -1; - do { - if (fd.cFileName[0] == '.') continue; - if (*out_count >= max_entries) break; - strncpy(entries[*out_count].name, fd.cFileName, 255); - entries[*out_count].name[255] = '\0'; - entries[*out_count].is_dir = (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 0; - (*out_count)++; - } while (FindNextFileA(h, &fd)); - FindClose(h); - return 0; -} - -hal_watcher_t hal_dir_watch_start(const char *path) { (void)path; return NULL; } -int hal_dir_watch_poll(hal_watcher_t w) { (void)w; return 0; } -void hal_dir_watch_stop(hal_watcher_t w) { (void)w; } - -#else -#include -#include -#include -#include - -int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) -{ - *out_count = 0; - DIR *dir = opendir(path); - if (!dir) return -1; - struct dirent *ent; - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') continue; - if (*out_count >= max_entries) break; - strncpy(entries[*out_count].name, ent->d_name, 255); - entries[*out_count].name[255] = '\0'; - entries[*out_count].is_dir = (ent->d_type == DT_DIR) ? 1 : 0; - (*out_count)++; - } - closedir(dir); - return 0; -} - -struct hal_watcher { - char path[512]; - time_t last_mtime; -}; - -hal_watcher_t hal_dir_watch_start(const char *path) -{ - struct stat st; - if (stat(path, &st) != 0) return NULL; - struct hal_watcher *w = (struct hal_watcher *)malloc(sizeof(struct hal_watcher)); - strncpy(w->path, path, 511); - w->path[511] = '\0'; - w->last_mtime = st.st_mtime; - return w; -} - -int hal_dir_watch_poll(hal_watcher_t watcher) -{ - if (!watcher) return -1; - struct stat st; - if (stat(watcher->path, &st) != 0) return -1; - if (st.st_mtime != watcher->last_mtime) { - watcher->last_mtime = st.st_mtime; - return 1; - } - return 0; -} - -void hal_dir_watch_stop(hal_watcher_t watcher) -{ - free(watcher); -} - -#endif diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp deleted file mode 100644 index a4570c63..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "hal/hal_network.h" -#include - -#ifdef _WIN32 - -int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) -{ - (void)entries; (void)max_entries; - *out_count = 0; - return 0; -} - -#else -#include -#include -#include - -int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) -{ - *out_count = 0; - struct ifaddrs *ifap = NULL; - if (getifaddrs(&ifap) != 0) return -1; - - for (struct ifaddrs *ifa = ifap; ifa; ifa = ifa->ifa_next) { - if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) continue; - if (strcmp(ifa->ifa_name, "lo") == 0 || strcmp(ifa->ifa_name, "lo0") == 0) continue; - if (*out_count >= max_entries) break; - - hal_netif_info_t *e = &entries[*out_count]; - strncpy(e->iface, ifa->ifa_name, 31); e->iface[31] = '\0'; - struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; - inet_ntop(AF_INET, &sa->sin_addr, e->ipv4, sizeof(e->ipv4)); - if (ifa->ifa_netmask) { - struct sockaddr_in *nm = (struct sockaddr_in *)ifa->ifa_netmask; - inet_ntop(AF_INET, &nm->sin_addr, e->netmask, sizeof(e->netmask)); - } else { - strcpy(e->netmask, "N/A"); - } - e->is_up = (ifa->ifa_flags & IFF_UP) ? 1 : 0; - (*out_count)++; - } - freeifaddrs(ifap); - return 0; -} - -#endif diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c b/ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c deleted file mode 100644 index 38d52d64..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_paths_sdl.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "hal/hal_paths.h" -#include -#include -#include - -static char s_data_dir[512] = "."; -static char s_applications_dir[512] = "./applications"; -static char s_store_cache_dir[512] = "./store_cache"; -static char s_lock_file[512] = "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; -static char s_font_dir[512] = "./APPLaunch/share/font"; -static char s_font_regular[512] = "./APPLaunch/share/font/AlibabaPuHuiTi-3-55-Regular.ttf"; -static char s_font_mono[512] = "./APPLaunch/share/font/LiberationMono-Regular.ttf"; -static char s_images_dir[512] = "./APPLaunch/share/images"; -static char s_audio_dir[512] = "./APPLaunch/share/audio"; - -static char s_store_sync_cmd[512] = "python store_cache_sync.py"; - -void hal_paths_init(const char *exe_dir) -{ - if (!exe_dir) exe_dir = "."; - snprintf(s_data_dir, sizeof(s_data_dir), "%s", exe_dir); - snprintf(s_applications_dir, sizeof(s_applications_dir), "%s/applications", exe_dir); - snprintf(s_store_cache_dir, sizeof(s_store_cache_dir), "%s/store_cache", exe_dir); - snprintf(s_images_dir, sizeof(s_images_dir), "%s/APPLaunch/share/images", exe_dir); - snprintf(s_font_dir, sizeof(s_font_dir), "%s/APPLaunch/share/font", exe_dir); - snprintf(s_audio_dir, sizeof(s_audio_dir), "%s/APPLaunch/share/audio", exe_dir); - snprintf(s_font_regular, sizeof(s_font_regular), "%s/APPLaunch/share/font/AlibabaPuHuiTi-3-55-Regular.ttf", exe_dir); - snprintf(s_font_mono, sizeof(s_font_mono), "%s/APPLaunch/share/font/LiberationMono-Regular.ttf", exe_dir); - snprintf(s_store_sync_cmd, sizeof(s_store_sync_cmd), "python %s/bin/store_cache_sync.py", exe_dir); -} - -const char *hal_path_data_dir(void) { return s_data_dir; } -const char *hal_path_applications_dir(void) { return s_applications_dir; } -const char *hal_path_store_cache_dir(void) { return s_store_cache_dir; } -const char *hal_path_lock_file(void) { return s_lock_file; } -const char *hal_path_font_dir(void) { return s_font_dir; } -const char *hal_path_font_regular(void) { return s_font_regular; } -const char *hal_path_font_mono(void) { return s_font_mono; } -const char *hal_path_keyboard_device(void) { return NULL; } -const char *hal_path_keyboard_map(void) { return NULL; } -const char *hal_path_store_sync_cmd(void) { return s_store_sync_cmd; } -const char *hal_path_images_dir(void) { return s_images_dir; } -const char *hal_path_audio_dir(void) { return s_audio_dir; } \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp deleted file mode 100644 index 4d42e735..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "hal/hal_process.h" -#include -#include - -#if defined(_WIN32) || defined(__EMSCRIPTEN__) -#ifdef _WIN32 -#include -#endif - -int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, - int keep_root) -{ - (void)home_key_flag; - (void)keep_root; -#ifdef _WIN32 - STARTUPINFOA si = {}; si.cb = sizeof(si); - PROCESS_INFORMATION pi = {}; - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "%s", exec_path); - if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) - return -1; - WaitForSingleObject(pi.hProcess, INFINITE); - DWORD exit_code = 1; - GetExitCodeProcess(pi.hProcess, &exit_code); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - return (int)exit_code; -#else - (void)exec_path; - return -1; -#endif -} - -int hal_process_check_lock(const char *lock_path, int *holder_pid) -{ - (void)lock_path; - *holder_pid = 0; - return 0; -} - -void hal_process_kill(int pid, int grace_ms) -{ - (void)pid; (void)grace_ms; -} - -hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) -{ - (void)exec_path; - (void)keep_root; - return -1; -} - -void hal_process_stop(hal_pid_t pid) -{ - (void)pid; -} - -void hal_system_shutdown(void) -{ - printf("[HAL] shutdown (emulator exit)\n"); - exit(0); -} - -void hal_system_reboot(void) -{ - printf("[HAL] reboot (emulator exit)\n"); - exit(0); -} - -#else -#include -#include -#include -#include -#include -#include -#include -#include - -int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, - int keep_root) -{ - (void)keep_root; - pid_t pid = fork(); - if (pid < 0) return -1; - if (pid == 0) { - execlp("/bin/sh", "sh", "-c", exec_path, (char *)NULL); - _exit(127); - } - int status = 0; - int home_status = 0; /* 0=idle, 1=timing, 2=killing */ - std::chrono::steady_clock::time_point home_start; - while (true) { - int r = waitpid(pid, &status, WNOHANG); - if (r > 0) break; - if (r < 0) { status = 0; break; } - - if (home_key_flag) { - if (home_status == 0) { - if (*home_key_flag) { - home_status = 1; - home_start = std::chrono::steady_clock::now(); - } - } else if (home_status == 1) { - if (*home_key_flag) { - auto elapsed = std::chrono::steady_clock::now() - home_start; - if (std::chrono::duration_cast(elapsed).count() >= 5) { - home_status = 2; - kill(pid, SIGINT); - } - } else { - home_status = 0; - } - } else if (home_status == 2) { - auto elapsed = std::chrono::steady_clock::now() - home_start; - if (std::chrono::duration_cast(elapsed).count() >= 8) { - kill(pid, SIGKILL); - waitpid(pid, &status, 0); - break; - } - } - } - - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - /* Clear home_key_flag to prevent stale state from affecting LVGL after return */ - if (home_key_flag) - *home_key_flag = 0; - if (WIFEXITED(status)) return WEXITSTATUS(status); - return -1; -} - -int hal_process_check_lock(const char *lock_path, int *holder_pid) -{ - *holder_pid = 0; - int fd = open(lock_path, O_CREAT | O_RDWR, 0666); - if (fd < 0) return -1; - struct flock fl; - memset(&fl, 0, sizeof(fl)); - fl.l_type = F_WRLCK; - fl.l_whence = SEEK_SET; - if (fcntl(fd, F_GETLK, &fl) == -1) { close(fd); return -1; } - close(fd); - if (fl.l_type != F_UNLCK) { - *holder_pid = fl.l_pid; - return fl.l_pid; - } - return 0; -} - -void hal_process_kill(int pid, int grace_ms) -{ - if (pid <= 0) return; - kill(pid, SIGINT); - auto start = std::chrono::steady_clock::now(); - while (true) { - int status; - if (waitpid(pid, &status, WNOHANG) != 0) return; - auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - start).count() >= grace_ms) { - kill(pid, SIGKILL); - waitpid(pid, &status, 0); - return; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } -} - -hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) -{ - (void)keep_root; - pid_t pid = fork(); - if (pid < 0) return -1; - if (pid == 0) { - execlp("/bin/sh", "sh", "-c", exec_path, (char *)NULL); - _exit(127); - } - return (hal_pid_t)pid; -} - -void hal_process_stop(hal_pid_t pid) -{ - if (pid <= 0) return; - kill((pid_t)pid, SIGTERM); - int status; - waitpid((pid_t)pid, &status, WNOHANG); -} - -void hal_system_shutdown(void) -{ - printf("[HAL] shutdown (emulator exit)\n"); - exit(0); -} - -void hal_system_reboot(void) -{ - printf("[HAL] reboot (emulator exit)\n"); - exit(0); -} - -#endif diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp deleted file mode 100644 index 14001acc..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "hal/hal_pty.h" - -#if defined(_WIN32) || defined(__EMSCRIPTEN__) -hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int cols, int rows) -{ (void)cmd; (void)args; (void)cols; (void)rows; return NULL; } -int hal_pty_read(hal_pty_t p, char *b, size_t s) { (void)p; (void)b; (void)s; return -1; } -int hal_pty_write(hal_pty_t p, const char *b, size_t l) { (void)p; (void)b; (void)l; return -1; } -int hal_pty_check_child(hal_pty_t p, int *e) { (void)p; (void)e; return -1; } -void hal_pty_close(hal_pty_t p) { (void)p; } - -#else -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#else -#include -#endif - -struct hal_pty { - int master_fd; - pid_t child_pid; -}; - -hal_pty_t hal_pty_open(const char *cmd, const char *const *args, - int cols, int rows) -{ - int master_fd; - pid_t pid; - struct winsize ws = {}; - ws.ws_col = cols; - ws.ws_row = rows; - - pid = forkpty(&master_fd, NULL, NULL, &ws); - if (pid < 0) return NULL; - - if (pid == 0) { - setenv("TERM", "vt100", 1); - if (args) { - execvp(cmd, (char *const *)args); - } else { - execlp(cmd, cmd, (char *)NULL); - } - _exit(127); - } - - int flags = fcntl(master_fd, F_GETFL); - fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); - - struct hal_pty *pty = (struct hal_pty *)malloc(sizeof(struct hal_pty)); - pty->master_fd = master_fd; - pty->child_pid = pid; - return pty; -} - -int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) -{ - if (!pty) return -1; - ssize_t n = read(pty->master_fd, buf, buf_size); - if (n < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; - return -1; - } - return (int)n; -} - -int hal_pty_write(hal_pty_t pty, const char *buf, size_t len) -{ - if (!pty) return -1; - return (int)write(pty->master_fd, buf, len); -} - -int hal_pty_check_child(hal_pty_t pty, int *exit_status) -{ - if (!pty) return -1; - int status; - pid_t r = waitpid(pty->child_pid, &status, WNOHANG); - if (r == 0) return 0; - if (r > 0) { - if (exit_status) *exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1; - return 1; - } - return -1; -} - -void hal_pty_close(hal_pty_t pty) -{ - if (!pty) return; - kill(pty->child_pid, SIGKILL); - waitpid(pty->child_pid, NULL, 0); - close(pty->master_fd); - free(pty); -} - -#endif diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp deleted file mode 100644 index e81d14cd..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "hal/hal_screenshot.h" -#include - -int hal_screenshot_save(const char *dir) -{ - (void)dir; - printf("[SCREENSHOT] Not supported on SDL platform\n"); - return -1; -} diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp deleted file mode 100644 index 1bed3cfc..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "hal/hal_settings.h" -#include -#include -#include - -hal_battery_info_t hal_battery_read(void) -{ - hal_battery_info_t info; - memset(&info, 0, sizeof(info)); - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - constexpr int kMinSoc = 55; - constexpr int kMaxSoc = 96; - constexpr int kRange = kMaxSoc - kMinSoc; - const long tick_100ms = now.tv_sec * 10L + now.tv_nsec / 100000000L; - const int step = static_cast(tick_100ms % (kRange * 2)); - const int soc = (step <= kRange) ? (kMaxSoc - step) : (kMinSoc + step - kRange); - - info.voltage_mv = 3300 + soc * 9; - info.current_ma = soc < 50 ? 200 : -200; - info.temperature_c10 = 350; - info.soc = soc; - info.remain_mah = soc * 30; - info.full_mah = 3000; - info.flags = soc < 50 ? 1 : 0; - info.avg_current_ma = info.current_ma; - info.valid = 1; - return info; -} - -int hal_backlight_read(void) { return 75; } -int hal_backlight_max(void) { return 100; } -int hal_backlight_write(int val) { (void)val; return val; } - -int hal_volume_read(void) { return 39; } -int hal_volume_write(int val) { (void)val; return val; } - -hal_wifi_status_t hal_wifi_get_status(void) -{ - hal_wifi_status_t st; - memset(&st, 0, sizeof(st)); - st.connected = 1; - strncpy(st.ssid, "SimulatedWiFi", WIFI_SSID_MAX - 1); - strncpy(st.ip, "192.168.1.100", sizeof(st.ip) - 1); - st.signal = 80; - return st; -} - -int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) -{ - if (max_aps < 3) return 0; - memset(out, 0, sizeof(hal_wifi_ap_t) * 3); - strncpy(out[0].ssid, "SimulatedWiFi", WIFI_SSID_MAX - 1); - out[0].signal = 80; strncpy(out[0].security, "WPA2", 31); out[0].in_use = 1; - strncpy(out[1].ssid, "Neighbor_5G", WIFI_SSID_MAX - 1); - out[1].signal = 55; strncpy(out[1].security, "WPA2", 31); - strncpy(out[2].ssid, "FreeWiFi", WIFI_SSID_MAX - 1); - out[2].signal = 30; strncpy(out[2].security, "Open", 31); - return 3; -} - -int hal_wifi_connect(const char *ssid, const char *password) -{ - (void)ssid; (void)password; return 0; -} - -int hal_wifi_disconnect(void) { return 0; } - -hal_bt_status_t hal_bt_get_status(void) -{ - hal_bt_status_t st; - memset(&st, 0, sizeof(st)); - st.powered = 0; - strncpy(st.address, "00:00:00:00:00:00", sizeof(st.address) - 1); - return st; -} - -int hal_bt_set_power(int on) { (void)on; return 0; } -int hal_bt_scan(hal_bt_device_t *out, int max_devices) { (void)out; (void)max_devices; return 0; } - -void hal_time_str(char *buf, int buf_size) -{ - time_t now = time(NULL); - struct tm *t = localtime(&now); - snprintf(buf, buf_size, "%02d:%02d", t->tm_hour, t->tm_min); -} diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp deleted file mode 100644 index ebfa3d7a..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_audio.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "../cp0/cp0_lvgl_audio.cpp" \ No newline at end of file diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp b/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp deleted file mode 100644 index fd6035ba..00000000 --- a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "hal_lvgl_bsp.h" - - -extern "C" void init_camera(void) -{ - // std::shared_ptr camera = std::make_shared(); - - // cp0_signal_camera_api.append([camera](std::list arg, std::function callback) - // { camera->api_call(arg, callback); }); -} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c index 036b6a9e..fbe4ab98 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c @@ -9,12 +9,26 @@ #include "sdl_lvgl.h" #include "commount.h" + void cp0_lvgl_init(void) { + init_lvgl_env(); init_lvgl_event(); - init_sdl_disp(); - init_sdl_input(); + init_filesystem(); + init_config(); + init_pty(); + init_freambuffer_disp(); + init_input(); init_audio(); + init_process(); + init_osinfo(); + init_screenshot(); + init_lora(); + init_wifi(); + init_settings(); + init_bq27220(); + init_imu(); + init_lvgl_saved_settings(); init_battery(); init_camera(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h index 0706ff4a..1225fe19 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl.h @@ -6,8 +6,23 @@ extern "C" #endif void init_sdl_disp(); void init_sdl_input(); +void init_freambuffer_disp(); +void init_input(); +void init_lvgl_env(void); +void init_filesystem(void); void init_audio(); +void init_config(void); +void init_pty(void); +void init_process(void); +void init_screenshot(void); +void init_lora(void); +void init_wifi(); +void init_settings(void); +void init_osinfo(void); +void init_bq27220(void); void init_battery(); +void init_imu(void); +void init_lvgl_saved_settings(void); void init_camera(void); #ifdef __cplusplus } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp new file mode 100644 index 00000000..d41dabf5 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp @@ -0,0 +1,376 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class AudioSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + int play(const std::string &wav) + { + (void)wav; + playing_ = true; + return 0; + } + + void cap(bool enable) + { + capturing_ = enable; + if (!enable) + cap_paused_ = false; + } + + void setup(arg_t arg, callback_t callback) + { + if (arg.empty()) + return; + + const std::string &cmd = arg.front(); + if (cmd == "set_callback") { + status_callback_ = std::move(callback); + } else if (cmd == "set_waveform" || cmd == "waveform") { + auto value = std::next(arg.begin()); + waveform_enabled_ = value == arg.end() || arg_is_enable(*value); + if (value != arg.end() && arg_is_disable(*value)) + waveform_enabled_ = false; + emit_waveform_once(); + } else if (cmd == "stop_play") { + playing_ = false; + } + } + + void system_play(const std::string &name) + { + (void)name; + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty audio api\n"); + return; + } + + const std::string &cmd = arg.front(); + if (cmd == "PlayFile") { + PlayFile(arg, callback); + } else if (cmd == "Play") { + Play(arg, callback); + } else if (cmd == "PlayPause") { + PlayPause(arg, callback); + } else if (cmd == "PlayContinue") { + PlayContinue(arg, callback); + } else if (cmd == "PlayEnd") { + PlayEnd(arg, callback); + } else if (cmd == "Cap") { + Cap(arg, callback); + } else if (cmd == "CapPause") { + CapPause(arg, callback); + } else if (cmd == "CapContinue") { + CapContinue(arg, callback); + } else if (cmd == "CapEnd") { + CapEnd(arg, callback); + } else if (cmd == "CapFileSave") { + CapFileSave(arg, callback); + } else if (cmd == "SetCallback") { + SetCallback(arg, callback); + } else if (cmd == "VolumeRead") { + VolumeRead(arg, callback); + } else if (cmd == "VolumeWrite") { + VolumeWrite(arg, callback); + } else { + report(callback, -1, "unknown audio api\n"); + } + } + +private: + static constexpr int kDefaultVolume = 39; + static constexpr int kMaxVolume = 63; + static constexpr int kRecWaveformSize = 128; + + callback_t status_callback_; + bool playing_ = false; + bool play_paused_ = false; + bool capturing_ = false; + bool cap_paused_ = false; + bool waveform_enabled_ = false; + int volume_ = kDefaultVolume; + + void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + else if (status_callback_) + status_callback_(code, data); + } + + static std::string first_arg_after_command(const arg_t &arg) + { + if (arg.size() < 2) + return ""; + return *std::next(arg.begin()); + } + + static bool arg_is_enable(const std::string &arg) + { + return arg == "1" || arg == "on" || arg == "true" || arg == "enable" || arg == "enabled"; + } + + static bool arg_is_disable(const std::string &arg) + { + return arg == "0" || arg == "off" || arg == "false" || arg == "disable" || arg == "disabled"; + } + + static int parse_volume_arg(const arg_t &arg) + { + std::string value = first_arg_after_command(arg); + if (value.empty()) + return 0; + return std::atoi(value.c_str()); + } + + void emit_waveform_once() + { + if (!waveform_enabled_ || !status_callback_) + return; + std::string waveform(sizeof(float) * kRecWaveformSize, '\0'); + status_callback_(1, waveform); + } + + static void put_u16_le(std::uint8_t *p, std::uint16_t v) + { + p[0] = static_cast(v & 0xFF); + p[1] = static_cast((v >> 8) & 0xFF); + } + + static void put_u32_le(std::uint8_t *p, std::uint32_t v) + { + p[0] = static_cast(v & 0xFF); + p[1] = static_cast((v >> 8) & 0xFF); + p[2] = static_cast((v >> 16) & 0xFF); + p[3] = static_cast((v >> 24) & 0xFF); + } + + static int write_silent_wav(const std::string &path) + { + if (path.empty()) + return -1; + + std::uint8_t header[44] = {}; + std::memcpy(header + 0, "RIFF", 4); + put_u32_le(header + 4, 36); + std::memcpy(header + 8, "WAVEfmt ", 8); + put_u32_le(header + 16, 16); + put_u16_le(header + 20, 1); + put_u16_le(header + 22, 2); + put_u32_le(header + 24, 48000); + put_u32_le(header + 28, 48000 * 2 * 2); + put_u16_le(header + 32, 4); + put_u16_le(header + 34, 16); + std::memcpy(header + 36, "data", 4); + put_u32_le(header + 40, 0); + + FILE *fp = std::fopen(path.c_str(), "wb"); + if (!fp) + return -2; + size_t written = std::fwrite(header, 1, sizeof(header), fp); + int close_ret = std::fclose(fp); + return (written == sizeof(header) && close_ret == 0) ? 0 : -3; + } + + void PlayFile(arg_t arg, callback_t callback) + { + std::string file = first_arg_after_command(arg); + if (file.empty()) { + report(callback, -1, "PlayFile need file\n"); + return; + } + int ret = play(file); + report(callback, ret, ret == 0 ? "play start\n" : "play failed\n"); + } + + void Play(arg_t arg, callback_t callback) + { + std::string file = first_arg_after_command(arg); + if (file.empty()) { + report(callback, -1, "Play need file\n"); + return; + } + int ret = play(file); + report(callback, ret, ret == 0 ? "play start\n" : "play failed\n"); + } + + void PlayPause(arg_t arg, callback_t callback) + { + (void)arg; + if (!playing_) { + report(callback, -1, "play not started\n"); + return; + } + play_paused_ = true; + report(callback, 0, "play pause\n"); + } + + void PlayContinue(arg_t arg, callback_t callback) + { + (void)arg; + if (!playing_) { + report(callback, -1, "play not started\n"); + return; + } + play_paused_ = false; + report(callback, 0, "play continue\n"); + } + + void PlayEnd(arg_t arg, callback_t callback) + { + (void)arg; + playing_ = false; + play_paused_ = false; + report(callback, 0, "play stop\n"); + } + + void Cap(arg_t arg, callback_t callback) + { + (void)arg; + capturing_ = true; + cap_paused_ = false; + emit_waveform_once(); + report(callback, 0, "cap start\n"); + } + + void CapPause(arg_t arg, callback_t callback) + { + (void)arg; + if (!capturing_) { + report(callback, -1, "cap not started\n"); + return; + } + cap_paused_ = true; + report(callback, 0, "cap pause\n"); + } + + void CapContinue(arg_t arg, callback_t callback) + { + (void)arg; + if (!capturing_) { + report(callback, -1, "cap not started\n"); + return; + } + cap_paused_ = false; + emit_waveform_once(); + report(callback, 0, "cap continue\n"); + } + + void CapEnd(arg_t arg, callback_t callback) + { + (void)arg; + capturing_ = false; + cap_paused_ = false; + report(callback, 0, "cap stop\n"); + } + + void CapFileSave(arg_t arg, callback_t callback) + { + std::string file = first_arg_after_command(arg); + if (file.empty()) { + report(callback, -1, "CapFileSave need file\n"); + return; + } + capturing_ = false; + cap_paused_ = false; + int ret = write_silent_wav(file); + report(callback, ret, ret == 0 ? "cap file saved\n" : "cap file save failed\n"); + } + + void SetCallback(arg_t arg, callback_t callback) + { + (void)arg; + status_callback_ = std::move(callback); + } + + void VolumeRead(arg_t arg, callback_t callback) + { + (void)arg; + report(callback, 0, std::to_string(volume_)); + } + + void VolumeWrite(arg_t arg, callback_t callback) + { + volume_ = std::max(0, std::min(kMaxVolume, parse_volume_arg(arg))); + report(callback, 0, std::to_string(volume_)); + } +}; + +std::shared_ptr g_audio; + +} // namespace + +extern "C" void init_audio(void) +{ + if (g_audio) + return; + + g_audio = std::make_shared(); + cp0_signal_audio_play.append([](std::string wav) { + g_audio->play(wav); + }); + + cp0_signal_audio_cap.append([](bool enable) { + g_audio->cap(enable); + }); + + cp0_signal_audio_setup.append([](std::list arg, std::function callback) { + g_audio->setup(std::move(arg), std::move(callback)); + }); + + cp0_signal_audio_api.append([](std::list arg, std::function callback) { + g_audio->api_call(std::move(arg), std::move(callback)); + }); + + cp0_signal_system_play.append([](std::string name) { + g_audio->system_play(name); + }); +} + +extern "C" void hal_audio_init(void) +{ + init_audio(); +} + +extern "C" void hal_audio_play(const char *path) +{ + init_audio(); + if (g_audio) + g_audio->play(path ? path : ""); +} + +extern "C" void hal_audio_play_sync(const char *path) +{ + hal_audio_play(path); +} + +extern "C" void hal_audio_stop(void) +{ + if (g_audio) + g_audio->api_call({"PlayEnd"}, nullptr); +} + +extern "C" void hal_audio_deinit(void) +{ + hal_audio_stop(); +} diff --git a/ext_components/cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_battery.cpp similarity index 100% rename from ext_components/cp0_lvgl/src/sdl/cp0_lvgl_battery.cpp rename to ext_components/cp0_lvgl/src/sdl/sdl_lvgl_battery.cpp diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_bq27220.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_bq27220.cpp new file mode 100644 index 00000000..8d44d537 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_bq27220.cpp @@ -0,0 +1,159 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +cp0_battery_info_t simulated_battery_info() +{ + cp0_battery_info_t info{}; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + constexpr int kMinSoc = 55; + constexpr int kMaxSoc = 96; + constexpr int kRange = kMaxSoc - kMinSoc; + const long tick_100ms = now.tv_sec * 10L + now.tv_nsec / 100000000L; + const int step = static_cast(tick_100ms % (kRange * 2)); + const int soc = (step <= kRange) ? (kMaxSoc - step) : (kMinSoc + step - kRange); + + info.voltage_mv = 3300 + soc * 9; + info.current_ma = soc < 50 ? 200 : -200; + info.temperature_c10 = 350; + info.soc = soc; + info.remain_mah = soc * 30; + info.full_mah = 3000; + info.flags = soc < 50 ? 1 : 0; + info.avg_current_ma = info.current_ma; + info.valid = 1; + return info; +} + +class Bq27220System +{ +public: + using arg_t = std::list; + using callback_t = std::function; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "Read") { + cp0_battery_info_t info = read(); + report(callback, info.valid ? 0 : -1, encode(info)); + } else if (cmd == "Calibrate") { + const int index = arg.size() >= 2 ? std::atoi(nth_arg(arg, 1).c_str()) : -1; + report(callback, calibrate(index), ""); + } else { + report(callback, -1, "unknown bq27220 api command"); + } + } + + cp0_battery_info_t read() + { + return simulated_battery_info(); + } + + int calibrate(int command_index) + { + return (command_index >= 0 && command_index < 4) ? 0 : -1; + } + + static bool decode_info(const std::string &data, cp0_battery_info_t *info) + { + if (!info) + return false; + cp0_battery_info_t parsed{}; + char comma; + std::istringstream is(data); + if (is >> parsed.voltage_mv >> comma && + is >> parsed.current_ma >> comma && + is >> parsed.temperature_c10 >> comma && + is >> parsed.soc >> comma && + is >> parsed.remain_mah >> comma && + is >> parsed.full_mah >> comma && + is >> parsed.flags >> comma && + is >> parsed.avg_current_ma >> comma && + is >> parsed.valid) { + *info = parsed; + return true; + } + return false; + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static std::string encode(const cp0_battery_info_t &info) + { + std::ostringstream os; + os << info.voltage_mv << ',' + << info.current_ma << ',' + << info.temperature_c10 << ',' + << info.soc << ',' + << info.remain_mah << ',' + << info.full_mah << ',' + << info.flags << ',' + << info.avg_current_ma << ',' + << info.valid; + return os.str(); + } +}; + +std::shared_ptr g_bq27220; + +} // namespace + +extern "C" { + +cp0_battery_info_t cp0_battery_read(void) +{ + cp0_battery_info_t info{}; + cp0_signal_bq27220_api({"Read"}, [&](int code, std::string data) { + if (code == 0) + Bq27220System::decode_info(data, &info); + }); + return info; +} + +int cp0_bq27220_calibrate(int command_index) +{ + int ret = -1; + cp0_signal_bq27220_api({"Calibrate", std::to_string(command_index)}, [&](int code, std::string data) { + (void)data; + ret = code; + }); + return ret; +} + +void init_bq27220(void) +{ + if (g_bq27220) + return; + + g_bq27220 = std::make_shared(); + cp0_signal_bq27220_api.append([](std::list arg, std::function callback) { + g_bq27220->api_call(std::move(arg), std::move(callback)); + }); +} + +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_camera.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_camera.cpp new file mode 100644 index 00000000..b7efbd72 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_camera.cpp @@ -0,0 +1,259 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class CameraSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + ~CameraSystem() + { + close_camera(); + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty camera api\n"); + return; + } + + const std::string command = arg.front(); + if (command == "SetCallback") { + std::lock_guard lock(mutex_); + status_callback_ = std::move(callback); + report_locked(status_callback_, 0, "camera callback set\n"); + return; + } + if (command == "SetFrameCallback") { + std::lock_guard lock(mutex_); + frame_callback_ = std::move(callback); + report_locked(frame_callback_, 0, "camera frame callback set\n"); + return; + } + if (command == "Open" || command == "Start") { + const int width = to_int(nth_arg(arg, 1), 320); + const int height = to_int(nth_arg(arg, 2), 150); + open_camera(width, height); + report(callback, 0, "SDL camera open\n"); + return; + } + if (command == "Close" || command == "Stop") { + close_camera(); + report(callback, 0, "camera close\n"); + return; + } + if (command == "Capture" || command == "Photo") { + const std::string path = nth_arg(arg, 1); + const int width = to_int(nth_arg(arg, 2), width_); + const int height = to_int(nth_arg(arg, 3), height_); + const int ret = capture(path, width, height); + report(callback, ret, ret == 0 ? path + "\n" : "camera capture failed\n"); + return; + } + if (command == "Status") { + report(callback, running_.load() ? 0 : 1, running_.load() ? "camera streaming\n" : "camera stopped\n"); + return; + } + if (command == "ZoomIn") { + std::lock_guard lock(mutex_); + zoom_percent_ = zoom_percent_ < 250 ? 250 : 500; + report_locked(callback, 0, zoom_payload_locked()); + return; + } + if (command == "ZoomOut") { + std::lock_guard lock(mutex_); + zoom_percent_ = zoom_percent_ > 250 ? 250 : 100; + if (zoom_percent_ == 100) + view_x_percent_ = view_y_percent_ = 50; + report_locked(callback, 0, zoom_payload_locked()); + return; + } + if (command == "Pan") { + std::lock_guard lock(mutex_); + const int dx = to_int(nth_arg(arg, 1), 0); + const int dy = to_int(nth_arg(arg, 2), 0); + view_x_percent_ = clamp(view_x_percent_ + dx * 8, 0, 100); + view_y_percent_ = clamp(view_y_percent_ + dy * 8, 0, 100); + report_locked(callback, 0, zoom_payload_locked()); + return; + } + + report(callback, -1, "unknown camera api\n"); + } + +private: + callback_t status_callback_; + callback_t frame_callback_; + std::mutex mutex_; + std::thread worker_; + std::atomic running_{false}; + int width_ = 320; + int height_ = 150; + int zoom_percent_ = 100; + int view_x_percent_ = 50; + int view_y_percent_ = 50; + uint32_t frame_index_ = 0; + + static int clamp(int value, int lo, int hi) + { + return std::max(lo, std::min(hi, value)); + } + + static std::string nth_arg(const arg_t &arg, size_t n) + { + if (arg.size() <= n) + return ""; + auto it = arg.begin(); + std::advance(it, n); + return *it; + } + + static int to_int(const std::string &value, int fallback) + { + if (value.empty()) + return fallback; + char *end = nullptr; + long ret = std::strtol(value.c_str(), &end, 10); + return end && *end == '\0' ? static_cast(ret) : fallback; + } + + static uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) + { + return static_cast(((r & 0xf8) << 8) | ((g & 0xfc) << 3) | (b >> 3)); + } + + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static void report_locked(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + std::string zoom_payload_locked() const + { + char buf[64]; + std::snprintf(buf, sizeof(buf), "ZOOM %d %d %d\n", zoom_percent_, view_x_percent_, view_y_percent_); + return buf; + } + + void open_camera(int width, int height) + { + close_camera(); + { + std::lock_guard lock(mutex_); + width_ = std::max(1, width); + height_ = std::max(1, height); + frame_index_ = 0; + } + running_.store(true); + worker_ = std::thread([this]() { frame_loop(); }); + } + + void close_camera() + { + running_.store(false); + if (worker_.joinable()) + worker_.join(); + } + + void frame_loop() + { + while (running_.load()) { + callback_t callback; + std::string payload; + { + std::lock_guard lock(mutex_); + callback = frame_callback_; + if (callback) + payload = make_frame_payload_locked(); + } + if (callback) + callback(0, payload); + std::this_thread::sleep_for(std::chrono::milliseconds(80)); + } + } + + std::string make_frame_payload_locked() + { + std::vector pixels(static_cast(width_) * height_); + const uint32_t tick = frame_index_++; + const int pan_x = (view_x_percent_ - 50) * 2; + const int pan_y = (view_y_percent_ - 50) * 2; + for (int y = 0; y < height_; ++y) { + for (int x = 0; x < width_; ++x) { + int sx = (x * zoom_percent_) / 100 + pan_x + static_cast(tick); + int sy = (y * zoom_percent_) / 100 + pan_y; + uint8_t r = static_cast((sx * 255) / std::max(1, width_)); + uint8_t g = static_cast((sy * 255) / std::max(1, height_)); + uint8_t b = static_cast((sx + sy + static_cast(tick) * 3) & 0xff); + if (((x / 16) + (y / 16) + (tick / 8)) & 1) + b = static_cast(255 - b); + pixels[static_cast(y) * width_ + x] = rgb565(r, g, b); + } + } + + char header[64]; + const int header_len = std::snprintf(header, sizeof(header), "FRAME %d %d RGB565\n", width_, height_); + std::string payload(header, header_len); + payload.append(reinterpret_cast(pixels.data()), pixels.size() * sizeof(uint16_t)); + return payload; + } + + int capture(const std::string &path, int width, int height) + { + if (path.empty()) + return -1; + FILE *file = std::fopen(path.c_str(), "wb"); + if (!file) + return -1; + + width = std::max(1, width); + height = std::max(1, height); + std::fprintf(file, "P6\n%d %d\n255\n", width, height); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + uint8_t rgb[3] = { + static_cast((x * 255) / width), + static_cast((y * 255) / height), + static_cast((x + y + frame_index_) & 0xff), + }; + std::fwrite(rgb, 1, sizeof(rgb), file); + } + } + return std::fclose(file) == 0 ? 0 : -1; + } +}; + +} // namespace + +extern "C" void init_camera(void) +{ + std::shared_ptr camera = std::make_shared(); + cp0_signal_camera_api.append([camera](std::list arg, std::function callback) { + camera->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp new file mode 100644 index 00000000..ca98e7bb --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp @@ -0,0 +1,253 @@ +#include "cp0_lvgl_app.h" +#include "hal/hal_paths.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int kMaxEntries = 64; +constexpr int kKeyMax = 64; +constexpr int kValMax = 256; + +static void copy_cstr(char *dst, size_t dst_size, const char *src) +{ + if (!dst || dst_size == 0) + return; + if (!src) + src = ""; + std::strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; +} + +static std::string join_path(const std::string &base, const char *leaf) +{ + std::string out = base.empty() ? std::string(".") : base; + if (!out.empty() && out.back() != '/') + out.push_back('/'); + out += leaf; + return out; +} + +static int mkdir_p(const std::string &path) +{ + if (path.empty()) + return -1; + char tmp[512]; + copy_cstr(tmp, sizeof(tmp), path.c_str()); + for (char *p = tmp + 1; *p; ++p) { + if (*p == '/') { + *p = '\0'; + if (mkdir(tmp, 0755) != 0 && errno != EEXIST) + return -1; + *p = '/'; + } + } + return (mkdir(tmp, 0755) == 0 || errno == EEXIST) ? 0 : -1; +} + +static std::string config_dir() +{ + const char *env = std::getenv("APPLAUNCH_SDL_CONFIG_DIR"); + if (env && env[0]) + return env; + const char *xdg = std::getenv("XDG_CONFIG_HOME"); + if (xdg && xdg[0]) + return join_path(xdg, "applaunch-sdl"); + const char *home = std::getenv("HOME"); + if (home && home[0]) + return join_path(join_path(home, ".config"), "applaunch-sdl"); + return join_path(hal_path_data_dir() ? hal_path_data_dir() : ".", "sdl_config"); +} + +class ConfigSystem { +public: + using callback_t = std::function; + using arg_t = std::list; + + ConfigSystem() { init(); } + + void init() + { + std::lock_guard lock(mutex_); + load_locked(); + } + + int get_int(const char *key, int default_val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = find_entry_locked(key); + return idx < 0 ? default_val : std::atoi(entries_[idx].val); + } + + void set_int(const char *key, int val) + { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%d", val); + set_str(key, buf); + } + + std::string get_str(const char *key, const char *default_val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = find_entry_locked(key); + return idx < 0 ? std::string(default_val ? default_val : "") : std::string(entries_[idx].val); + } + + void set_str(const char *key, const char *val) + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + int idx = get_or_create_entry_locked(key); + if (idx >= 0) + copy_cstr(entries_[idx].val, sizeof(entries_[idx].val), val); + } + + int save() + { + std::lock_guard lock(mutex_); + ensure_loaded_locked(); + const std::string dir = config_dir(); + if (mkdir_p(dir) != 0) + return -1; + FILE *fp = std::fopen(join_path(dir, "settings").c_str(), "w"); + if (!fp) + return -1; + for (int i = 0; i < count_; ++i) + std::fprintf(fp, "%s=%s\n", entries_[i].key, entries_[i].val); + std::fclose(fp); + return 0; + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty config api"); + return; + } + const std::string cmd = arg.front(); + if (cmd == "Init") { + init(); + report(callback, 0, "ok"); + } else if (cmd == "GetInt") { + report(callback, 0, std::to_string(get_int(nth_arg(arg, 1).c_str(), parse_int(nth_arg(arg, 2), 0)))); + } else if (cmd == "SetInt") { + set_int(nth_arg(arg, 1).c_str(), parse_int(nth_arg(arg, 2), 0)); + report(callback, 0, "ok"); + } else if (cmd == "GetStr") { + report(callback, 0, get_str(nth_arg(arg, 1).c_str(), nth_arg(arg, 2).c_str())); + } else if (cmd == "SetStr") { + set_str(nth_arg(arg, 1).c_str(), nth_arg(arg, 2).c_str()); + report(callback, 0, "ok"); + } else if (cmd == "Save") { + int ret = save(); + report(callback, ret, ret == 0 ? "ok" : "save failed"); + } else { + report(callback, -1, "unknown config api"); + } + } + +private: + struct Entry { + char key[kKeyMax]; + char val[kValMax]; + }; + + Entry entries_[kMaxEntries] = {}; + int count_ = 0; + bool loaded_ = false; + std::mutex mutex_; + + void ensure_loaded_locked() + { + if (!loaded_) + load_locked(); + } + + void load_locked() + { + count_ = 0; + loaded_ = true; + FILE *fp = std::fopen(join_path(config_dir(), "settings").c_str(), "r"); + if (!fp) + return; + char line[kKeyMax + kValMax + 4]; + while (std::fgets(line, sizeof(line), fp) && count_ < kMaxEntries) { + line[std::strcspn(line, "\r\n")] = '\0'; + char *eq = std::strchr(line, '='); + if (!eq || line[0] == '\0') + continue; + *eq = '\0'; + copy_cstr(entries_[count_].key, sizeof(entries_[count_].key), line); + copy_cstr(entries_[count_].val, sizeof(entries_[count_].val), eq + 1); + ++count_; + } + std::fclose(fp); + } + + int find_entry_locked(const char *key) const + { + if (!key || !key[0]) + return -1; + for (int i = 0; i < count_; ++i) { + if (std::strcmp(entries_[i].key, key) == 0) + return i; + } + return -1; + } + + int get_or_create_entry_locked(const char *key) + { + int idx = find_entry_locked(key); + if (idx >= 0) + return idx; + if (!key || !key[0] || count_ >= kMaxEntries) + return -1; + idx = count_++; + copy_cstr(entries_[idx].key, sizeof(entries_[idx].key), key); + entries_[idx].val[0] = '\0'; + return idx; + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static int parse_int(const std::string &value, int fallback) + { + return value.empty() ? fallback : std::atoi(value.c_str()); + } + + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } +}; + +} // namespace + +extern "C" void init_config(void) +{ + auto config = std::make_shared(); + cp0_signal_config_api.append([config](std::list arg, std::function callback) { + config->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c index 41067c51..00159370 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c @@ -21,6 +21,10 @@ static const char *getenv_default(const char *name, const char *dflt) void init_sdl_disp(void) { + static int display_initialized = 0; + if (display_initialized) + return; + int width = atoi(getenv_default("LV_SDL_VIDEO_WIDTH", "320")); int height = atoi(getenv_default("LV_SDL_VIDEO_HEIGHT", "170")); lv_display_t *disp = lv_sdl_window_create(width, height); @@ -30,4 +34,10 @@ void init_sdl_disp(void) } lv_sdl_window_set_title(disp, getenv_default("LV_SDL_WINDOW_TITLE", "M5CardputerZero")); + display_initialized = 1; +} + +void init_freambuffer_disp(void) +{ + init_sdl_disp(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp deleted file mode 100644 index d56cd2c9..00000000 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include "cp0_lvgl_app.h" -#include "cp0_lvgl_file.hpp" - -#include -#include -#include -#include -#include -#include -#include - -namespace { -std::string dirname_of(const std::string &path) -{ - size_t slash = path.find_last_of('/'); - return (slash == std::string::npos) ? "." : path.substr(0, slash); -} - -bool path_exists(const std::string &path) -{ - return access(path.c_str(), F_OK) == 0; -} - -std::string get_exe_dir() -{ - char exe_path[PATH_MAX] = {0}; - ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); - if (len <= 0) { - return "."; - } - - exe_path[len] = '\0'; - return dirname_of(exe_path); -} - -std::string get_cwd() -{ - char cwd[PATH_MAX] = {0}; - if (getcwd(cwd, sizeof(cwd)) == nullptr) { - return "."; - } - return cwd; -} - -std::string normalize_path(const std::string &path) -{ - if (path.empty()) { - return "."; - } - - const bool absolute = path[0] == '/'; - std::vector parts; - size_t start = 0; - while (start < path.size()) { - size_t end = path.find('/', start); - std::string part = path.substr(start, end == std::string::npos ? std::string::npos : end - start); - if (part.empty() || part == ".") { - // Skip. - } else if (part == "..") { - if (!parts.empty() && parts.back() != "..") { - parts.pop_back(); - } else if (!absolute) { - parts.push_back(part); - } - } else { - parts.push_back(part); - } - if (end == std::string::npos) { - break; - } - start = end + 1; - } - - std::string out = absolute ? "/" : ""; - for (size_t i = 0; i < parts.size(); ++i) { - if (i > 0) { - out += "/"; - } - out += parts[i]; - } - return out.empty() ? (absolute ? "/" : ".") : out; -} - -std::string make_absolute_path(const std::string &path) -{ - if (!path.empty() && path[0] == '/') { - return normalize_path(path); - } - return normalize_path(get_cwd() + "/" + path); -} - -std::vector split_path(const std::string &path) -{ - std::vector parts; - size_t start = (path.size() > 0 && path[0] == '/') ? 1 : 0; - while (start < path.size()) { - size_t end = path.find('/', start); - std::string part = path.substr(start, end == std::string::npos ? std::string::npos : end - start); - if (!part.empty()) { - parts.push_back(part); - } - if (end == std::string::npos) { - break; - } - start = end + 1; - } - return parts; -} - -std::string make_relative_to_cwd(const std::string &path) -{ - const std::string abs_path = make_absolute_path(path); - const std::string abs_cwd = make_absolute_path(get_cwd()); - if (abs_path == abs_cwd) { - return "."; - } - - const auto path_parts = split_path(abs_path); - const auto cwd_parts = split_path(abs_cwd); - - size_t common = 0; - while (common < path_parts.size() && common < cwd_parts.size() && path_parts[common] == cwd_parts[common]) { - ++common; - } - - std::string rel; - for (size_t i = common; i < cwd_parts.size(); ++i) { - if (!rel.empty()) { - rel += "/"; - } - rel += ".."; - } - for (size_t i = common; i < path_parts.size(); ++i) { - if (!rel.empty()) { - rel += "/"; - } - rel += path_parts[i]; - } - - return rel.empty() ? "." : rel; -} - -std::string get_app_root_path() -{ - const std::string exe_dir = get_exe_dir(); - const std::string cwd = get_cwd(); - const std::string exe_parent = dirname_of(exe_dir); - - std::vector candidates = { - exe_dir + "/APPLaunch", - cwd + "/APPLaunch", - exe_parent + "/APPLaunch", - }; - - for (const auto &candidate : candidates) { - if (path_exists(candidate + "/share")) { - return make_absolute_path(candidate); - } - } - - return make_absolute_path(exe_dir + "/APPLaunch"); -} - -std::string lower_ext(const std::string &file) -{ - const auto dot = file.find_last_of('.'); - if (dot == std::string::npos) { - return ""; - } - - std::string ext = file.substr(dot + 1); - std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - return ext; -} - -bool is_image_ext(const std::string &ext) -{ - return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg" || ext == "svg"; -} - -bool is_audio_ext(const std::string &ext) -{ - return ext == "wav" || ext == "mp3" || ext == "ogg"; -} - -bool is_font_ext(const std::string &ext) -{ - return ext == "ttf" || ext == "otf"; -} - -bool is_absolute_path(const std::string &file) -{ - return !file.empty() && file[0] == '/'; -} - -bool starts_with(const std::string &value, const char *prefix) -{ - const std::string p(prefix); - return value.compare(0, p.size(), p) == 0; -} - -std::string app_relative_path(const std::string &root_path, const std::string &file, const char *dir) -{ - std::string absolute_path; - if (starts_with(file, "APPLaunch/")) { - absolute_path = dirname_of(root_path) + "/" + file; - } else if (is_absolute_path(file)) { - absolute_path = file; - } else if (starts_with(file, dir)) { - absolute_path = root_path + "/" + file; - } else { - absolute_path = root_path + "/" + dir + file; - } - - return make_relative_to_cwd(absolute_path); -} - -} // namespace - -std::string cp0_file_path(std::string file) -{ - if (file.empty()) { - return ""; - } - - const std::string root_path = get_app_root_path(); - if (file == "applications") { - return root_path + "/applications"; - } - if (file == "lock_file") { - return "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; - } - if (file == "keyboard_device") { - return "/dev/input/by-path/platform-3f804000.i2c-event"; - } - if (file == "keyboard_map") { - return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; - } - if (file == "store_sync_cmd") { - return "python " + root_path + "/bin/store_cache_sync.py"; - } - - const std::string ext = lower_ext(file); - if (is_image_ext(ext)) { - return app_relative_path(root_path, file, "share/images/"); - } - if (is_audio_ext(ext)) { - return app_relative_path(root_path, file, "share/audio/"); - } - if (is_font_ext(ext)) { - return app_relative_path(root_path, file, "share/font/"); - } - - return file; -} - -extern "C" const char *cp0_file_path_c(const char *file) -{ - static thread_local std::unordered_map paths; - std::string key = file ? std::string(file) : std::string(); - auto it = paths.find(key); - if (it == paths.end()) { - it = paths.emplace(key, cp0_file_path(key)).first; - } - return it->second.c_str(); -} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_filesystem.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_filesystem.cpp new file mode 100644 index 00000000..d10a31d9 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_filesystem.cpp @@ -0,0 +1,383 @@ +#include "cp0_lvgl_app.h" +#include "cp0_lvgl_filesystem.hpp" +#include "hal/hal_paths.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char s_data_dir[512] = "."; +static char s_applications_dir[512] = "./applications"; +static char s_store_cache_dir[512] = "./store_cache"; +static char s_lock_file[512] = "/tmp/M5CardputerZero-APPLaunch_fcntl.lock"; +static char s_font_dir[512] = "./APPLaunch/share/font"; +static char s_font_regular[512] = "./APPLaunch/share/font/AlibabaPuHuiTi-3-55-Regular.ttf"; +static char s_font_mono[512] = "./APPLaunch/share/font/LiberationMono-Regular.ttf"; +static char s_images_dir[512] = "./APPLaunch/share/images"; +static char s_audio_dir[512] = "./APPLaunch/share/audio"; +static char s_store_sync_cmd[512] = "python store_cache_sync.py"; + +extern "C" void hal_paths_init(const char *exe_dir) +{ + if (!exe_dir) + exe_dir = "."; + std::snprintf(s_data_dir, sizeof(s_data_dir), "%s", exe_dir); + std::snprintf(s_applications_dir, sizeof(s_applications_dir), "%s/applications", exe_dir); + std::snprintf(s_store_cache_dir, sizeof(s_store_cache_dir), "%s/store_cache", exe_dir); + std::snprintf(s_images_dir, sizeof(s_images_dir), "%s/APPLaunch/share/images", exe_dir); + std::snprintf(s_font_dir, sizeof(s_font_dir), "%s/APPLaunch/share/font", exe_dir); + std::snprintf(s_audio_dir, sizeof(s_audio_dir), "%s/APPLaunch/share/audio", exe_dir); + std::snprintf(s_font_regular, sizeof(s_font_regular), + "%s/APPLaunch/share/font/AlibabaPuHuiTi-3-55-Regular.ttf", exe_dir); + std::snprintf(s_font_mono, sizeof(s_font_mono), "%s/APPLaunch/share/font/LiberationMono-Regular.ttf", exe_dir); + std::snprintf(s_store_sync_cmd, sizeof(s_store_sync_cmd), "python %s/bin/store_cache_sync.py", exe_dir); +} + +extern "C" const char *hal_path_data_dir(void) { return s_data_dir; } +extern "C" const char *hal_path_applications_dir(void) { return s_applications_dir; } +extern "C" const char *hal_path_store_cache_dir(void) { return s_store_cache_dir; } +extern "C" const char *hal_path_lock_file(void) { return s_lock_file; } +extern "C" const char *hal_path_font_dir(void) { return s_font_dir; } +extern "C" const char *hal_path_font_regular(void) { return s_font_regular; } +extern "C" const char *hal_path_font_mono(void) { return s_font_mono; } +extern "C" const char *hal_path_keyboard_device(void) { return nullptr; } +extern "C" const char *hal_path_keyboard_map(void) { return nullptr; } +extern "C" const char *hal_path_store_sync_cmd(void) { return s_store_sync_cmd; } +extern "C" const char *hal_path_images_dir(void) { return s_images_dir; } +extern "C" const char *hal_path_audio_dir(void) { return s_audio_dir; } + +namespace { + +static void copy_cstr(char *dst, size_t dst_size, const char *src) +{ + if (!dst || dst_size == 0) + return; + if (!src) + src = ""; + std::strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; +} + +static bool starts_with(const std::string &value, const char *prefix) +{ + const std::string p(prefix ? prefix : ""); + return value.compare(0, p.size(), p) == 0; +} + +static bool has_lvgl_drive(const std::string &file) +{ + return file.size() >= 2 && std::isalpha(static_cast(file[0])) && file[1] == ':'; +} + +static std::string lower_ext(const std::string &file) +{ + const size_t dot = file.find_last_of('.'); + if (dot == std::string::npos) + return ""; + std::string ext = file.substr(dot + 1); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return ext; +} + +static bool is_image_ext(const std::string &ext) +{ + return ext == "png" || ext == "gif" || ext == "jpg" || ext == "jpeg" || ext == "svg"; +} + +static bool is_audio_ext(const std::string &ext) +{ + return ext == "wav" || ext == "mp3" || ext == "ogg"; +} + +static bool is_font_ext(const std::string &ext) +{ + return ext == "ttf" || ext == "otf"; +} + +static std::string strip_app_root_prefix(const std::string &file) +{ + static constexpr const char *kAppRoot = "/usr/share/APPLaunch/"; + if (starts_with(file, kAppRoot)) + return file.substr(std::strlen(kAppRoot)); + if (starts_with(file, "APPLaunch/")) + return file.substr(std::strlen("APPLaunch/")); + return file; +} + +static std::string join_path(const char *base, const std::string &rel) +{ + if (rel.empty()) + return base ? std::string(base) : std::string(); + if (rel.front() == '/' || has_lvgl_drive(rel)) + return rel; + std::string out = base ? std::string(base) : std::string("."); + if (!out.empty() && out.back() != '/') + out.push_back('/'); + out += rel; + return out; +} + +static std::string resource_path(const char *base, const char *prefix, const std::string &file) +{ + std::string rel = strip_app_root_prefix(file); + if (has_lvgl_drive(rel)) { + rel = rel.substr(2); + while (!rel.empty() && rel.front() == '/') + rel.erase(rel.begin()); + } + const std::string prefix_with_slash = std::string(prefix) + "/"; + if (starts_with(rel, prefix_with_slash.c_str())) + rel = rel.substr(prefix_with_slash.size()); + return join_path(base, rel); +} + +struct SdlWatcher { + std::string path; + time_t mtime = 0; + off_t size = 0; + nlink_t nlink = 0; + bool valid = false; +}; + +class SdlFilesystem { +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + auto report = [&](int code, const std::string &data) { + if (callback) + callback(code, data); + }; + + if (arg.empty()) { + report(-1, "missing command"); + return; + } + + const std::string cmd = arg.front(); + const std::string value = arg.size() >= 2 ? *std::next(arg.begin()) : std::string(); + if (cmd == "Path") { + report(0, resolve_path(value)); + } else if (cmd == "DirList") { + std::string data; + report(encode_dir_entries(value.c_str(), data), data); + } else if (cmd == "WatchStart") { + cp0_watcher_t watcher = watch_start(value.c_str()); + report(watcher ? 0 : -1, std::to_string(reinterpret_cast(watcher))); + } else if (cmd == "WatchPoll") { + report(0, std::to_string(watch_poll(parse_handle(value)))); + } else if (cmd == "WatchStop") { + watch_stop(parse_handle(value)); + report(0, ""); + } else { + report(-2, "unknown command: " + cmd); + } + } + + std::string path(std::string file) { return resolve_path(file); } + + int dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) + { + return list_dir(path, entries, max_entries, out_count); + } + + cp0_watcher_t dir_watch_start(const char *path) { return watch_start(path); } + int dir_watch_poll(cp0_watcher_t watcher) { return watch_poll(watcher); } + void dir_watch_stop(cp0_watcher_t watcher) { watch_stop(watcher); } + +private: + static cp0_watcher_t parse_handle(const std::string &value) + { + return reinterpret_cast(static_cast(std::strtoull(value.c_str(), nullptr, 0))); + } + + static std::string resolve_path(const std::string &file) + { + if (file.empty()) + return ""; + if (file == "applications") + return hal_path_applications_dir(); + if (file == "lock_file") + return hal_path_lock_file(); + if (file == "keyboard_device") + return hal_path_keyboard_device() ? hal_path_keyboard_device() : ""; + if (file == "keyboard_map") + return hal_path_keyboard_map() ? hal_path_keyboard_map() : ""; + if (file == "store_sync_cmd") + return hal_path_store_sync_cmd(); + + const std::string ext = lower_ext(file); + if (is_image_ext(ext)) + return resource_path(hal_path_images_dir(), "share/images", file); + if (is_audio_ext(ext)) + return resource_path(hal_path_audio_dir(), "share/audio", file); + if (is_font_ext(ext)) + return resource_path(hal_path_font_dir(), "share/font", file); + if (has_lvgl_drive(file)) { + std::string rel = file.substr(2); + while (!rel.empty() && rel.front() == '/') + rel.erase(rel.begin()); + return rel; + } + return file; + } + + static int list_dir(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) + { + if (out_count) + *out_count = 0; + if (!path || !out_count) + return -1; + if (!entries || max_entries <= 0) + return 0; + + DIR *dir = opendir(path); + if (!dir) + return -errno; + + struct dirent *ent = nullptr; + while ((ent = readdir(dir)) != nullptr) { + if (ent->d_name[0] == '.') + continue; + if (*out_count >= max_entries) + break; + cp0_dirent_t &entry = entries[*out_count]; + copy_cstr(entry.name, sizeof(entry.name), ent->d_name); + entry.is_dir = ent->d_type == DT_DIR ? 1 : 0; + if (ent->d_type == DT_UNKNOWN) { + std::string child = join_path(path, ent->d_name); + struct stat st; + entry.is_dir = stat(child.c_str(), &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0; + } + ++(*out_count); + } + closedir(dir); + return 0; + } + + static int encode_dir_entries(const char *path, std::string &data) + { + cp0_dirent_t entries[512]; + int count = 0; + int ret = list_dir(path, entries, 512, &count); + if (ret != 0) + return ret; + std::ostringstream out; + for (int i = 0; i < count; ++i) + out << (entries[i].is_dir ? 'D' : 'F') << '\t' << entries[i].name << '\n'; + data = out.str(); + return 0; + } + + static void snapshot(SdlWatcher *watcher) + { + if (!watcher) + return; + struct stat st; + if (stat(watcher->path.c_str(), &st) == 0) { + watcher->mtime = st.st_mtime; + watcher->size = st.st_size; + watcher->nlink = st.st_nlink; + watcher->valid = true; + } else { + watcher->valid = false; + } + } + + static cp0_watcher_t watch_start(const char *path) + { + if (!path || !path[0]) + return nullptr; + SdlWatcher *watcher = new SdlWatcher(); + watcher->path = path; + snapshot(watcher); + return watcher; + } + + static int watch_poll(cp0_watcher_t handle) + { + if (!handle) + return -1; + SdlWatcher *watcher = static_cast(handle); + SdlWatcher now; + now.path = watcher->path; + snapshot(&now); + const bool changed = now.valid != watcher->valid || now.mtime != watcher->mtime || + now.size != watcher->size || now.nlink != watcher->nlink; + *watcher = now; + return changed ? 1 : 0; + } + + static void watch_stop(cp0_watcher_t watcher) + { + delete static_cast(watcher); + } +}; + +SdlFilesystem &filesystem() +{ + static SdlFilesystem instance; + return instance; +} + +} // namespace + +std::string cp0_file_path(std::string file) +{ + return filesystem().path(std::move(file)); +} + +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::unordered_map paths; + std::string key = file ? file : ""; + auto it = paths.find(key); + if (it == paths.end()) + it = paths.emplace(key, cp0_file_path(key)).first; + return it->second.c_str(); +} + +extern "C" int cp0_dir_list(const char *path, cp0_dirent_t *entries, int max_entries, int *out_count) +{ + return filesystem().dir_list(path, entries, max_entries, out_count); +} + +extern "C" cp0_watcher_t cp0_dir_watch_start(const char *path) +{ + return filesystem().dir_watch_start(path); +} + +extern "C" int cp0_dir_watch_poll(cp0_watcher_t watcher) +{ + return filesystem().dir_watch_poll(watcher); +} + +extern "C" void cp0_dir_watch_stop(cp0_watcher_t watcher) +{ + filesystem().dir_watch_stop(watcher); +} + +extern "C" void init_filesystem(void) +{ + cp0_signal_filesystem_api.append([](std::list arg, std::function callback) { + filesystem().api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp new file mode 100644 index 00000000..07c1db35 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp @@ -0,0 +1,111 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +void copy_cstr(char *dst, size_t dst_size, const char *src) +{ + if (!dst || dst_size == 0) + return; + if (!src) + src = ""; + std::strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; +} + +void fill_simulated_info(cp0_compass_info_t *info) +{ + if (!info) + return; + + std::memset(info, 0, sizeof(*info)); + copy_cstr(info->status, sizeof(info->status), "SDL simulated IMU"); + info->yaw = 90.0f; + info->pitch = 1.5f; + info->roll = -2.0f; + info->acc_x = 0.0f; + info->acc_y = 0.0f; + info->acc_z = 1.0f; + info->gyr_x = 0.0f; + info->gyr_y = 0.0f; + info->gyr_z = 0.0f; + info->mag_x = 32.0f; + info->mag_y = 0.0f; + info->mag_z = 4.0f; + info->sensor_ready = 1; +} + +class ImuSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty imu api\n"); + return; + } + + if (arg.front() == "Read") { + cp0_compass_info_t info{}; + int ret = read_info(&info); + report(callback, ret, std::string(reinterpret_cast(&info), sizeof(info))); + return; + } + + report(callback, -1, "unknown imu api\n"); + } + + int read_info(cp0_compass_info_t *info) + { + if (!info) + return -1; + fill_simulated_info(info); + return 0; + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } +}; + +std::shared_ptr g_imu; + +} // namespace + +extern "C" int cp0_compass_read(cp0_compass_read_cb_t callback, void *user) +{ + cp0_compass_info_t info{}; + int ret = -1; + cp0_signal_imu_api({"Read"}, [&](int code, std::string data) { + ret = code; + if (data.size() == sizeof(info)) + std::memcpy(&info, data.data(), sizeof(info)); + }); + if (callback) + callback(ret, &info, user); + return ret; +} + +extern "C" void init_imu(void) +{ + if (g_imu) + return; + + g_imu = std::make_shared(); + cp0_signal_imu_api.append([](std::list arg, std::function callback) { + g_imu->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c index 4f0d9320..3599633b 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c @@ -15,28 +15,20 @@ #include #include -#ifndef KEYBOARD_BUFFER_SIZE -#define KEYBOARD_BUFFER_SIZE 32 -#endif - - typedef struct { - char buf[KEYBOARD_BUFFER_SIZE]; - bool dummy_read; - struct key_item current; - struct key_item last; - size_t last_utf8_len; + SDL_Scancode current_scancode; bool current_valid; + struct key_item active_keys[SDL_NUM_SCANCODES]; + bool active_valid[SDL_NUM_SCANCODES]; } cp0_sdl_keyboard_t; static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data); static void cp0_sdl_keyboard_delete_cb(lv_event_t *event); -static const char *getenv_default(const char *name, const char *dflt) +__attribute__((weak)) void ui_global_hint_on_key(const struct key_item *elm) { - const char *value = getenv(name); - return (value && value[0] != '\0') ? value : dflt; + (void)elm; } static uint32_t cp0_utf8_first_codepoint(const char *s) @@ -58,20 +50,38 @@ static uint32_t cp0_utf8_first_codepoint(const char *s) return 0; } -static size_t cp0_utf8_first_len(const char *s) +static uint32_t cp0_evdev_process_key(uint16_t code) { - const unsigned char ch = (unsigned char)s[0]; - if (ch == '\0') - return 0; - if (ch < 0x80) - return 1; - if ((ch & 0xe0) == 0xc0) - return 2; - if ((ch & 0xf0) == 0xe0) - return 3; - if ((ch & 0xf8) == 0xf0) - return 4; - return 1; + switch (code) { + case KEY_UP: + return LV_KEY_UP; + case KEY_DOWN: + return LV_KEY_DOWN; + case KEY_RIGHT: + return LV_KEY_RIGHT; + case KEY_LEFT: + return LV_KEY_LEFT; + case KEY_ESC: + return LV_KEY_ESC; + case KEY_DELETE: + return LV_KEY_DEL; + case KEY_BACKSPACE: + return LV_KEY_BACKSPACE; + case KEY_ENTER: + return LV_KEY_ENTER; + case KEY_NEXT: + return LV_KEY_NEXT; + case KEY_TAB: + return KEY_TAB; + case KEY_PREVIOUS: + return LV_KEY_PREV; + case KEY_HOME: + return LV_KEY_HOME; + case KEY_END: + return LV_KEY_END; + default: + return code; + } } static uint32_t cp0_sdl_ctrl_to_lv_key(SDL_Keycode key) @@ -385,22 +395,34 @@ const char *kbd_state_name(int state) } } -static void cp0_send_keyboard_event(const struct key_item *event) +static bool cp0_sdl_queue_has_data(void) { - struct key_item *elm = calloc(1, sizeof(*elm)); + bool has_data; + pthread_mutex_lock(&keyboard_mutex); + has_data = !STAILQ_EMPTY(&keyboard_queue); + pthread_mutex_unlock(&keyboard_mutex); + return has_data; +} + +static void cp0_sdl_enqueue_key(const struct key_item *event) +{ + struct key_item *elm = malloc(sizeof(*elm)); if (elm == NULL) return; *elm = *event; - elm->flage = 1; + elm->flage = 0; if (elm->key_code == KEY_ESC) LVGL_HOME_KEY_FLAG = elm->key_state; - lv_obj_t *root = lv_screen_active(); - if (root != NULL) - lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); - - free(elm); + if (LVGL_RUN_FLAGE) { + pthread_mutex_lock(&keyboard_mutex); + STAILQ_INSERT_TAIL(&keyboard_queue, elm, entries); + pthread_mutex_unlock(&keyboard_mutex); + } + else { + free(elm); + } } static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEvent *event) @@ -411,12 +433,12 @@ static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEve const char *name = SDL_GetKeyName(sym); memset(&kbd->current, 0, sizeof(kbd->current)); + kbd->current_scancode = scancode; kbd->current.key_code = cp0_sdl_scancode_to_linux_key(scancode); kbd->current.keysym = (uint32_t)sym; kbd->current.mods = 0; - kbd->current.key_state = LV_INDEV_STATE_PRESSED; - uint32_t lv_key = cp0_sdl_ctrl_to_lv_key(sym); - kbd->current.codepoint = lv_key; + kbd->current.key_state = event->repeat ? KBD_KEY_REPEATED : KBD_KEY_PRESSED; + kbd->current.codepoint = cp0_sdl_ctrl_to_lv_key(sym); if (mods & KMOD_SHIFT) kbd->current.mods |= KBD_MOD_SHIFT; if (mods & KMOD_CTRL) @@ -435,29 +457,27 @@ static void cp0_sdl_fill_key_meta(cp0_sdl_keyboard_t *kbd, const SDL_KeyboardEve kbd->current_valid = true; } -static void cp0_sdl_set_text_key(cp0_sdl_keyboard_t *kbd, const char *utf8, size_t len) +static void cp0_sdl_set_text_key(cp0_sdl_keyboard_t *kbd, const char *utf8) { if (!kbd->current_valid) { memset(&kbd->current, 0, sizeof(kbd->current)); - kbd->current.key_state = LV_INDEV_STATE_PRESSED; + kbd->current.key_state = KBD_KEY_PRESSED; kbd->current_valid = true; } + size_t len = strlen(utf8); size_t n = len < sizeof(kbd->current.utf8) - 1 ? len : sizeof(kbd->current.utf8) - 1; memcpy(kbd->current.utf8, utf8, n); kbd->current.utf8[n] = '\0'; kbd->current.codepoint = cp0_utf8_first_codepoint(kbd->current.utf8); } -static void cp0_sdl_enqueue_text(cp0_sdl_keyboard_t *kbd, const char *text) +static void cp0_sdl_remember_active_key(cp0_sdl_keyboard_t *kbd, SDL_Scancode scancode, + const struct key_item *item) { - size_t used = strlen(kbd->buf); - size_t incoming = strlen(text); - if (used + incoming >= sizeof(kbd->buf)) - incoming = sizeof(kbd->buf) - used - 1; - if (incoming > 0) { - memcpy(kbd->buf + used, text, incoming); - kbd->buf[used + incoming] = '\0'; + if (scancode < SDL_NUM_SCANCODES) { + kbd->active_keys[scancode] = *item; + kbd->active_valid[scancode] = true; } } @@ -483,43 +503,30 @@ static lv_indev_t *cp0_sdl_keyboard_create(void) static void cp0_sdl_keyboard_read(lv_indev_t *indev, lv_indev_data_t *data) { - cp0_sdl_keyboard_t *kbd = lv_indev_get_driver_data(indev); - size_t len = strlen(kbd->buf); - data->continue_reading = false; + (void)indev; - if (kbd->dummy_read) { - kbd->dummy_read = false; - kbd->last.key_state = LV_INDEV_STATE_RELEASED; - data->key = kbd->last.codepoint; - data->state = LV_INDEV_STATE_RELEASED; - cp0_send_keyboard_event(&kbd->last); - memset(&kbd->last, 0, sizeof(kbd->last)); - kbd->last_utf8_len = 0; - return; - } - - if (len == 0) { - data->state = LV_INDEV_STATE_RELEASED; - return; - } + data->state = LV_INDEV_STATE_RELEASED; + data->continue_reading = false; - size_t char_len = cp0_utf8_first_len(kbd->buf); - if (char_len > len) - char_len = len; + pthread_mutex_lock(&keyboard_mutex); + if (!STAILQ_EMPTY(&keyboard_queue)) { + struct key_item *elm = STAILQ_FIRST(&keyboard_queue); + STAILQ_REMOVE_HEAD(&keyboard_queue, entries); - cp0_sdl_set_text_key(kbd, kbd->buf, char_len); - kbd->current.key_state = LV_INDEV_STATE_PRESSED; + lv_obj_t *root = lv_screen_active(); + if (root != NULL) + lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); - kbd->last = kbd->current; - kbd->last_utf8_len = char_len; - data->key = kbd->current.codepoint; - data->state = LV_INDEV_STATE_PRESSED; - kbd->dummy_read = true; - cp0_send_keyboard_event(&kbd->current); + ui_global_hint_on_key(elm); - memmove(kbd->buf, kbd->buf + char_len, len - char_len + 1); - kbd->current_valid = false; - data->continue_reading = kbd->buf[0] != '\0'; + data->key = cp0_evdev_process_key(elm->key_code); + if (data->key) { + data->state = (lv_indev_state_t)elm->key_state; + data->continue_reading = !STAILQ_EMPTY(&keyboard_queue); + } + free(elm); + } + pthread_mutex_unlock(&keyboard_mutex); } static void cp0_sdl_keyboard_delete_cb(lv_event_t *event) @@ -538,6 +545,7 @@ void lv_sdl_keyboard_handler(SDL_Event *event) uint32_t win_id = UINT32_MAX; switch (event->type) { case SDL_KEYDOWN: + case SDL_KEYUP: win_id = event->key.windowID; break; case SDL_TEXTINPUT: @@ -564,27 +572,63 @@ void lv_sdl_keyboard_handler(SDL_Event *event) cp0_sdl_fill_key_meta(kbd, &event->key); uint32_t ctrl_key = cp0_sdl_ctrl_to_lv_key(event->key.keysym.sym); char ctrl_char = 0; - if (ctrl_key == 0 && !cp0_sdl_ctrl_letter(&event->key, &ctrl_char)) - return; - - char ctrl_buf[2] = {ctrl_char != 0 ? ctrl_char : (char)ctrl_key, '\0'}; - cp0_sdl_enqueue_text(kbd, ctrl_buf); + if (ctrl_key != 0 || cp0_sdl_ctrl_letter(&event->key, &ctrl_char)) { + if (ctrl_char != 0) { + kbd->current.utf8[0] = ctrl_char; + kbd->current.utf8[1] = '\0'; + kbd->current.codepoint = (uint32_t)ctrl_char; + } + cp0_sdl_enqueue_key(&kbd->current); + cp0_sdl_remember_active_key(kbd, event->key.keysym.scancode, &kbd->current); + kbd->current_valid = false; + } } else if (event->type == SDL_TEXTINPUT) { - cp0_sdl_enqueue_text(kbd, event->text.text); + cp0_sdl_set_text_key(kbd, event->text.text); + cp0_sdl_enqueue_key(&kbd->current); + cp0_sdl_remember_active_key(kbd, kbd->current_scancode, &kbd->current); + kbd->current_valid = false; + } + else if (event->type == SDL_KEYUP) { + SDL_Scancode scancode = event->key.keysym.scancode; + struct key_item item; + if (scancode < SDL_NUM_SCANCODES && kbd->active_valid[scancode]) { + item = kbd->active_keys[scancode]; + kbd->active_valid[scancode] = false; + } + else { + cp0_sdl_fill_key_meta(kbd, &event->key); + item = kbd->current; + } + item.key_state = KBD_KEY_RELEASED; + cp0_sdl_enqueue_key(&item); + if (kbd->current_scancode == scancode) + kbd->current_valid = false; } - while (kbd->buf[0] != '\0') { - lv_indev_read(indev); + while (cp0_sdl_queue_has_data()) lv_indev_read(indev); - } } - void init_sdl_input(void) { + static int input_initialized = 0; + if (input_initialized) + return; + + STAILQ_INIT(&keyboard_queue); + if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + lv_sdl_mouse_create(); if (cp0_sdl_keyboard_create() == NULL) fprintf(stderr, "cp0_lvgl: failed to create SDL keyboard input\n"); + + input_initialized = 1; +} + +void init_input(void) +{ + init_sdl_input(); } diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_lara.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_lara.cpp new file mode 100644 index 00000000..2ceb46d4 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_lara.cpp @@ -0,0 +1,132 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class LoraSystem +{ +public: + void api_call(std::list arg, std::function callback) + { + if (arg.empty()) { + report(callback, -1, "missing lora api command\n"); + return; + } + + const std::string command = arg.front(); + if (command == "Init") { + initialized_ = true; + hw_ready_ = true; + std::snprintf(diag_, sizeof(diag_), "SDL simulated LoRa ready"); + report(callback, 0, ""); + return; + } + if (command == "Poll" || command == "Info") { + cp0_lora_info_t info{}; + fill_info(&info, command == "Poll"); + report(callback, 0, std::string(reinterpret_cast(&info), sizeof(info))); + return; + } + if (command == "SendText") { + std::snprintf(last_tx_, sizeof(last_tx_), "%s", first_arg_after_command(arg).c_str()); + std::snprintf(last_rx_, sizeof(last_rx_), "SDL echo: %.117s", last_tx_); + has_sent_message_ = true; + tx_event_ = true; + rx_event_ = true; + rssi_ = -42.0f; + snr_ = 9.5f; + std::snprintf(diag_, sizeof(diag_), "SDL simulated packet sent"); + report(callback, 0, ""); + return; + } + if (command == "StartReceive") { + tx_mode_ = false; + std::snprintf(diag_, sizeof(diag_), "SDL simulated receive mode"); + report(callback, 0, ""); + return; + } + if (command == "SetTxMode") { + tx_mode_ = std::atoi(first_arg_after_command(arg).c_str()) != 0; + std::snprintf(diag_, sizeof(diag_), tx_mode_ ? "SDL simulated TX mode" : "SDL simulated RX mode"); + report(callback, 0, ""); + return; + } + if (command == "Shutdown") { + initialized_ = false; + hw_ready_ = false; + tx_mode_ = false; + std::snprintf(diag_, sizeof(diag_), "SDL simulated LoRa shutdown"); + report(callback, 0, ""); + return; + } + + report(callback, -1, "unknown lora api\n"); + } + +private: + bool initialized_ = false; + bool hw_ready_ = false; + bool tx_mode_ = false; + bool has_sent_message_ = false; + bool rx_event_ = false; + bool tx_event_ = false; + float rssi_ = -48.0f; + float snr_ = 7.0f; + char last_rx_[128] = "SDL simulated receive buffer"; + char last_tx_[128] = "Hello from SDL LoRa"; + char diag_[256] = "SDL simulated LoRa idle"; + + static std::string first_arg_after_command(const std::list& arg) + { + if (arg.size() < 2) + return ""; + return *std::next(arg.begin()); + } + + void fill_info(cp0_lora_info_t *info, bool drain_events) + { + if (!info) return; + std::memset(info, 0, sizeof(*info)); + info->initialized = initialized_ ? 1 : 0; + info->hw_ready = hw_ready_ ? 1 : 0; + info->tx_mode = tx_mode_ ? 1 : 0; + info->tx_in_progress = 0; + info->has_sent_message = has_sent_message_ ? 1 : 0; + info->rx_event = rx_event_ ? 1 : 0; + info->tx_event = tx_event_ ? 1 : 0; + std::snprintf(info->spi_device, sizeof(info->spi_device), "sdl://lora"); + std::snprintf(info->last_rx, sizeof(info->last_rx), "%s", last_rx_); + std::snprintf(info->last_tx, sizeof(info->last_tx), "%s", last_tx_); + std::snprintf(info->diag, sizeof(info->diag), "%s", diag_); + std::snprintf(info->probe_summary, sizeof(info->probe_summary), "SDL simulated SPI/GPIO"); + std::snprintf(info->probe_display, sizeof(info->probe_display), "LoRa: SDL simulation"); + std::snprintf(info->pi4io_status, sizeof(info->pi4io_status), "SDL no-op PI4IO"); + info->rssi = rssi_; + info->snr = snr_; + if (drain_events) { + rx_event_ = false; + tx_event_ = false; + } + } + + static void report(std::function callback, int code, const std::string& data) + { + if (callback) callback(code, data); + } +}; + +extern "C" void init_lora(void) +{ + std::shared_ptr lora = std::make_shared(); + cp0_signal_lora_api.append([lora](std::list arg, std::function callback) { + lora->api_call(arg, callback); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp new file mode 100644 index 00000000..7ac9917b --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp @@ -0,0 +1,190 @@ +#include "cp0_lvgl_app.h" +#include "hal/hal_network.h" +#include "hal/hal_settings.h" +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static void copy_cstr(char *dst, size_t dst_size, const char *src) +{ + if (!dst || dst_size == 0) + return; + if (!src) + src = ""; + std::strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; +} + +class WifiSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "Status") { + report(callback, 0, encode_status(get_status())); + } else if (cmd == "Scan") { + int max_count = arg.size() >= 2 ? std::atoi(nth_arg(arg, 1).c_str()) : CP0_WIFI_AP_MAX; + std::vector aps(static_cast(std::max(0, max_count))); + int count = scan(aps.empty() ? nullptr : aps.data(), static_cast(aps.size())); + report(callback, count, encode_scan(aps.data(), count)); + } else if (cmd == "Connect") { + report(callback, hal_wifi_connect(nth_arg(arg, 1).c_str(), nth_arg(arg, 2).c_str()), ""); + } else if (cmd == "Disconnect" || cmd == "ProfileDisconnectActive") { + report(callback, hal_wifi_disconnect(), ""); + } else if (cmd == "ProfileForget") { + report(callback, 0, ""); + } else if (cmd == "ProfileExists") { + const std::string ssid = nth_arg(arg, 1); + cp0_wifi_status_t st = get_status(); + report(callback, ssid == st.ssid ? 1 : 0, ""); + } else { + report(callback, -1, "unknown wifi api command"); + } + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static cp0_wifi_status_t get_status() + { + hal_wifi_status_t hal = hal_wifi_get_status(); + cp0_wifi_status_t st{}; + st.connected = hal.connected; + copy_cstr(st.ssid, sizeof(st.ssid), hal.ssid); + copy_cstr(st.ip, sizeof(st.ip), hal.ip); + st.signal = hal.signal; + return st; + } + + static int scan(cp0_wifi_ap_t *out, int max_aps) + { + if (!out || max_aps <= 0) + return hal_wifi_scan(nullptr, 0); + + std::vector hal_aps(static_cast(max_aps)); + int count = hal_wifi_scan(hal_aps.data(), max_aps); + count = std::min(count, max_aps); + for (int i = 0; i < count; ++i) { + copy_cstr(out[i].ssid, sizeof(out[i].ssid), hal_aps[static_cast(i)].ssid); + copy_cstr(out[i].security, sizeof(out[i].security), hal_aps[static_cast(i)].security); + out[i].signal = hal_aps[static_cast(i)].signal; + out[i].in_use = hal_aps[static_cast(i)].in_use; + } + return count; + } + + static std::string encode_status(const cp0_wifi_status_t &st) + { + std::ostringstream oss; + oss << st.connected << ':' << st.ssid << ':' << st.ip << ':' << st.signal; + return oss.str(); + } + + static std::string encode_scan(const cp0_wifi_ap_t *aps, int count) + { + std::ostringstream oss; + for (int i = 0; aps && i < count; ++i) + oss << aps[i].ssid << ':' << aps[i].signal << ':' << aps[i].security << ':' << aps[i].in_use << '\n'; + return oss.str(); + } +}; + +} // namespace + +extern "C" int cp0_network_list(cp0_netif_info_t *entries, int max_entries, int *out_count) +{ + if (!out_count) + return -1; + *out_count = 0; + + if (!entries || max_entries <= 0) + return hal_network_list(nullptr, 0, out_count); + + std::vector hal_entries(static_cast(max_entries)); + int ret = hal_network_list(hal_entries.data(), max_entries, out_count); + int count = std::min(*out_count, max_entries); + for (int i = 0; i < count; ++i) { + copy_cstr(entries[i].iface, sizeof(entries[i].iface), hal_entries[static_cast(i)].iface); + copy_cstr(entries[i].ipv4, sizeof(entries[i].ipv4), hal_entries[static_cast(i)].ipv4); + copy_cstr(entries[i].netmask, sizeof(entries[i].netmask), hal_entries[static_cast(i)].netmask); + entries[i].is_up = hal_entries[static_cast(i)].is_up; + } + *out_count = count; + return ret; +} + +extern "C" void init_wifi(void) +{ + auto wifi = std::make_shared(); + cp0_signal_wifi_api.append([wifi](std::list arg, std::function callback) { + wifi->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int hal_network_list(hal_netif_info_t *entries, int max_entries, int *out_count) +{ + if (!out_count) + return -1; + *out_count = 0; + + if (!entries || max_entries <= 0) + return 0; + + struct ifaddrs *ifap = nullptr; + if (getifaddrs(&ifap) != 0) + return -1; + + for (struct ifaddrs *ifa = ifap; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) + continue; + if (std::strcmp(ifa->ifa_name, "lo") == 0 || std::strcmp(ifa->ifa_name, "lo0") == 0) + continue; + if (*out_count >= max_entries) + break; + + hal_netif_info_t *entry = &entries[*out_count]; + copy_cstr(entry->iface, sizeof(entry->iface), ifa->ifa_name); + auto *sa = reinterpret_cast(ifa->ifa_addr); + inet_ntop(AF_INET, &sa->sin_addr, entry->ipv4, sizeof(entry->ipv4)); + if (ifa->ifa_netmask) { + auto *nm = reinterpret_cast(ifa->ifa_netmask); + inet_ntop(AF_INET, &nm->sin_addr, entry->netmask, sizeof(entry->netmask)); + } else { + copy_cstr(entry->netmask, sizeof(entry->netmask), "N/A"); + } + entry->is_up = (ifa->ifa_flags & IFF_UP) ? 1 : 0; + ++(*out_count); + } + freeifaddrs(ifap); + return 0; +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_osinfo.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_osinfo.cpp new file mode 100644 index 00000000..58230eab --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_osinfo.cpp @@ -0,0 +1,341 @@ +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" +#include "../cp0_app_internal_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static time_t g_sdl_time_offset = 0; + +static time_t parse_timestamp(const char *timestamp, bool *ok) +{ + if (ok) + *ok = false; + if (!timestamp || !timestamp[0]) + return 0; + + std::tm tm{}; + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + if (std::sscanf(timestamp, "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second) != 6) + return 0; + + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_isdst = -1; + + time_t parsed = std::mktime(&tm); + if (parsed == static_cast(-1)) + return 0; + if (ok) + *ok = true; + return parsed; +} + +class SdlOsInfoSystem { +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = nth_arg(arg, 0); + if (cmd == "NetworkDefaultInfoRead" || cmd == "EthInfoRead") { + cp0_eth_info_t info{}; + int ret = network_default_info_read(&info); + report(callback, ret, encode_eth_info(info)); + } else if (cmd == "AccountInfoRead") { + cp0_account_info_t info{}; + int ret = account_info_read(&info); + report(callback, ret, encode_account_info(info)); + } else if (cmd == "TimeSet") { + report(callback, time_set(nth_arg(arg, 1).c_str()), ""); + } else if (cmd == "AptUpdateBackground") { + report(callback, apt_update_background(), ""); + } else if (cmd == "UpdateLauncherBackground") { + report(callback, update_launcher_background(), ""); + } else { + report(callback, -1, "unknown osinfo api command"); + } + } + + static int api_simple(const arg_t &arg, std::string *out = nullptr) + { + int result = -1; + cp0_signal_osinfo_api(arg, [&](int code, std::string data) { + result = code; + if (out) + *out = std::move(data); + }); + return result; + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static void clear_net_info(cp0_eth_info_t *info) + { + if (!info) + return; + std::memset(info, 0, sizeof(*info)); + cp0_copy_cstr(info->ipv4, sizeof(info->ipv4), "N/A"); + cp0_copy_cstr(info->gateway, sizeof(info->gateway), "N/A"); + cp0_copy_cstr(info->mac, sizeof(info->mac), "N/A"); + } + + static std::string default_iface_from_route() + { + std::ifstream route("/proc/net/route"); + std::string line; + std::getline(route, line); + while (std::getline(route, line)) { + std::istringstream iss(line); + std::string iface; + unsigned long dest = 0; + if (iss >> iface >> std::hex >> dest && dest == 0) + return iface; + } + return ""; + } + + static std::string default_gateway_from_route(const std::string &iface) + { + std::ifstream route("/proc/net/route"); + std::string line; + std::getline(route, line); + while (std::getline(route, line)) { + std::istringstream iss(line); + std::string row_iface; + unsigned long dest = 0; + unsigned long gateway = 0; + if (!(iss >> row_iface >> std::hex >> dest >> gateway)) + continue; + if (dest != 0 || (!iface.empty() && row_iface != iface)) + continue; + struct in_addr addr; + addr.s_addr = static_cast(gateway); + char buf[INET_ADDRSTRLEN] = {}; + if (inet_ntop(AF_INET, &addr, buf, sizeof(buf))) + return buf; + } + return "N/A"; + } + + static std::string mac_for_iface(const std::string &iface) + { + if (iface.empty()) + return "N/A"; + std::ifstream file("/sys/class/net/" + iface + "/address"); + std::string mac; + if (std::getline(file, mac) && !mac.empty()) + return mac; + return "N/A"; + } + + static int network_default_info_read(cp0_eth_info_t *info) + { + clear_net_info(info); + if (!info) + return -1; + + std::string iface = default_iface_from_route(); + struct ifaddrs *ifap = nullptr; + if (getifaddrs(&ifap) == 0) { + for (struct ifaddrs *ifa = ifap; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) + continue; + if (std::strcmp(ifa->ifa_name, "lo") == 0 || std::strcmp(ifa->ifa_name, "lo0") == 0) + continue; + if (!iface.empty() && iface != ifa->ifa_name) + continue; + iface = ifa->ifa_name; + struct sockaddr_in *sa = reinterpret_cast(ifa->ifa_addr); + char ip[INET_ADDRSTRLEN] = {}; + if (inet_ntop(AF_INET, &sa->sin_addr, ip, sizeof(ip))) + cp0_copy_cstr(info->ipv4, sizeof(info->ipv4), ip); + break; + } + freeifaddrs(ifap); + } + + cp0_copy_string(info->gateway, sizeof(info->gateway), default_gateway_from_route(iface)); + cp0_copy_string(info->mac, sizeof(info->mac), mac_for_iface(iface)); + return 0; + } + + static int account_info_read(cp0_account_info_t *info) + { + if (!info) + return -1; + std::memset(info, 0, sizeof(*info)); + const char *user = getlogin(); + if (!user || !user[0]) { + struct passwd *pw = getpwuid(getuid()); + user = pw ? pw->pw_name : nullptr; + } + cp0_copy_cstr(info->user, sizeof(info->user), user && user[0] ? user : "N/A"); + char host[sizeof(info->hostname)] = {}; + if (gethostname(host, sizeof(host) - 1) == 0 && host[0]) + cp0_copy_cstr(info->hostname, sizeof(info->hostname), host); + else + cp0_copy_cstr(info->hostname, sizeof(info->hostname), "SDL"); + return 0; + } + + static int time_set(const char *timestamp) + { + bool ok = false; + const time_t target = parse_timestamp(timestamp, &ok); + if (!ok) + return -1; + g_sdl_time_offset = target - std::time(nullptr); + return 0; + } + + static int apt_update_background() + { + return 0; + } + + static int update_launcher_background() + { + return 0; + } + + static std::string encode_eth_info(const cp0_eth_info_t &info) + { + return std::string(info.ipv4) + "\n" + info.gateway + "\n" + info.mac; + } + + static void decode_eth_info(const std::string &data, cp0_eth_info_t *info) + { + clear_net_info(info); + if (!info) + return; + std::istringstream lines(data); + std::string line; + if (std::getline(lines, line)) cp0_copy_string(info->ipv4, sizeof(info->ipv4), line); + if (std::getline(lines, line)) cp0_copy_string(info->gateway, sizeof(info->gateway), line); + if (std::getline(lines, line)) cp0_copy_string(info->mac, sizeof(info->mac), line); + } + + static std::string encode_account_info(const cp0_account_info_t &info) + { + return std::string(info.user) + "\n" + info.hostname; + } + + static void decode_account_info(const std::string &data, cp0_account_info_t *info) + { + if (!info) + return; + std::memset(info, 0, sizeof(*info)); + std::istringstream lines(data); + std::string line; + if (std::getline(lines, line)) cp0_copy_string(info->user, sizeof(info->user), line); + if (std::getline(lines, line)) cp0_copy_string(info->hostname, sizeof(info->hostname), line); + } + +public: + static int api_eth_info(const char *command, cp0_eth_info_t *info) + { + if (!info) + return -1; + std::string data; + int ret = api_simple({command}, &data); + if (ret == 0) + decode_eth_info(data, info); + return ret; + } + + static int api_account_info(cp0_account_info_t *info) + { + if (!info) + return -1; + std::string data; + int ret = api_simple({"AccountInfoRead"}, &data); + if (ret == 0) + decode_account_info(data, info); + return ret; + } +}; + +} // namespace + +extern "C" time_t cp0_sdl_time_now(void) +{ + return std::time(nullptr) + g_sdl_time_offset; +} + +extern "C" void init_osinfo(void) +{ + auto osinfo = std::make_shared(); + cp0_signal_osinfo_api.append([osinfo](std::list arg, std::function callback) { + osinfo->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int cp0_network_default_info_read(cp0_eth_info_t *info) +{ + return SdlOsInfoSystem::api_eth_info("NetworkDefaultInfoRead", info); +} + +extern "C" int cp0_eth_info_read(cp0_eth_info_t *info) +{ + return SdlOsInfoSystem::api_eth_info("EthInfoRead", info); +} + +extern "C" int cp0_account_info_read(cp0_account_info_t *info) +{ + return SdlOsInfoSystem::api_account_info(info); +} + +extern "C" int cp0_time_set(const char *timestamp) +{ + return SdlOsInfoSystem::api_simple({"TimeSet", timestamp ? timestamp : ""}); +} + +extern "C" int cp0_system_apt_update_background(void) +{ + return SdlOsInfoSystem::api_simple({"AptUpdateBackground"}); +} + +extern "C" int cp0_system_update_launcher_background(void) +{ + return SdlOsInfoSystem::api_simple({"UpdateLauncherBackground"}); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp new file mode 100644 index 00000000..377c5460 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp @@ -0,0 +1,778 @@ +#include "cp0_lvgl_app.h" +#include "hal/hal_process.h" +#include "hal_lvgl_bsp.h" +#include "../cp0_app_internal_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#include +#include +#include +#include +#endif + +#if !defined(HAL_PLATFORM_SDL) && !defined(_WIN32) +#include +#endif + +extern "C" { + extern void keyboard_pause(void); + extern void keyboard_resume(void); +} + +extern "C" void __attribute__((weak)) keyboard_pause(void) {} +extern "C" void __attribute__((weak)) keyboard_resume(void) {} + +class ProcessSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "ExecBlocking") { + const std::string exec_path = nth_arg(arg, 1); + volatile int *home_key_flag = decode_flag_ptr(nth_arg(arg, 2)); + int keep_root = std::atoi(nth_arg(arg, 3).c_str()); + report(callback, exec_blocking(exec_path.c_str(), home_key_flag, keep_root), ""); + } else if (cmd == "Spawn") { + const std::string exec_path = nth_arg(arg, 1); + int keep_root = std::atoi(nth_arg(arg, 2).c_str()); + cp0_pid_t pid = spawn(exec_path.c_str(), keep_root); + report(callback, pid < 0 ? -1 : 0, std::to_string(pid)); + } else if (cmd == "Stop") { + stop(static_cast(std::atoi(nth_arg(arg, 1).c_str()))); + report(callback, 0, ""); + } else if (cmd == "CheckLock") { + int holder_pid = 0; + int ret = check_lock(nth_arg(arg, 1).c_str(), &holder_pid); + report(callback, ret, std::to_string(holder_pid)); + } else if (cmd == "Kill") { + int pid = std::atoi(nth_arg(arg, 1).c_str()); + int grace_ms = std::atoi(nth_arg(arg, 2).c_str()); + kill_process(pid, grace_ms); + report(callback, 0, ""); + } else if (cmd == "RunArgv") { + int background = std::atoi(nth_arg(arg, 1).c_str()); + std::vector argv = args_from(arg, 2); + report(callback, run_argv(argv, background), ""); + } else if (cmd == "CaptureArgv") { + std::vector argv = args_from(arg, 1); + std::string output; + int ret = capture_argv(argv, output); + report(callback, ret, output); + } else if (cmd == "Shutdown") { + system_shutdown(); + report(callback, 0, ""); + } else if (cmd == "Reboot") { + system_reboot(); + report(callback, 0, ""); + } else { + report(callback, -1, "unknown process api command"); + } + } + + int exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) + { +#if defined(_WIN32) + (void)exec_path; + (void)home_key_flag; + (void)keep_root; + return -1; +#elif defined(HAL_PLATFORM_SDL) + return exec_blocking_sdl(exec_path, home_key_flag, keep_root); +#else + return exec_blocking_cp0(exec_path, home_key_flag, keep_root); +#endif + } + + cp0_pid_t spawn(const char *exec_path, int keep_root) + { +#if defined(_WIN32) + (void)exec_path; + (void)keep_root; + return -1; +#else + pid_t pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + setpgid(0, 0); + if (keep_root) + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); + else + exec_as_user(exec_path); + _exit(127); + } + setpgid(pid, pid); + return static_cast(pid); +#endif + } + + void stop(cp0_pid_t pid) + { +#if !defined(_WIN32) + if (pid <= 0) + return; + killpg(static_cast(pid), SIGTERM); + int status = 0; + waitpid(static_cast(pid), &status, WNOHANG); +#else + (void)pid; +#endif + } + + int check_lock(const char *lock_path, int *holder_pid) + { + if (holder_pid) + *holder_pid = 0; +#if defined(_WIN32) + (void)lock_path; + return 0; +#else + if (!lock_path || !holder_pid) + return -1; + int fd = open(lock_path, O_CREAT | O_RDWR, 0666); + if (fd < 0) + return -1; + struct flock fl; + std::memset(&fl, 0, sizeof(fl)); + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + if (fcntl(fd, F_GETLK, &fl) == -1) { + close(fd); + return -1; + } + close(fd); + if (fl.l_type != F_UNLCK) { + *holder_pid = fl.l_pid; + return fl.l_pid; + } + return 0; +#endif + } + + void kill_process(int pid, int grace_ms) + { +#if !defined(_WIN32) + if (pid <= 0) + return; + killpg(pid, SIGINT); + auto start = std::chrono::steady_clock::now(); + while (true) { + int status = 0; + if (waitpid(pid, &status, WNOHANG) != 0) + return; + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - start).count() >= grace_ms) { + killpg(pid, SIGKILL); + waitpid(pid, &status, 0); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +#else + (void)pid; + (void)grace_ms; +#endif + } + + int run_argv(const std::vector &argv, int background) + { +#if defined(_WIN32) + (void)argv; + (void)background; + return -1; +#else + if (argv.empty() || argv[0].empty()) + return -EINVAL; + + pid_t pid = fork(); + if (pid < 0) + return -errno; + + if (pid == 0) { + if (background) + redirect_to_devnull(); + auto raw = make_argv(argv); + execvp(raw[0], raw.data()); + _exit(127); + } + + if (background) + return 0; + + int status = 0; + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + return -errno; + } + if (WIFEXITED(status)) + return WEXITSTATUS(status); + if (WIFSIGNALED(status)) + return 128 + WTERMSIG(status); + return -1; +#endif + } + + int capture_argv(const std::vector &argv, std::string &output) + { + output.clear(); +#if defined(_WIN32) + (void)argv; + return -1; +#else + if (argv.empty() || argv[0].empty()) + return -EINVAL; + + int pipefd[2]; + if (pipe(pipefd) != 0) + return -errno; + + pid_t pid = fork(); + if (pid < 0) { + close(pipefd[0]); + close(pipefd[1]); + return -errno; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + dup2(devnull, STDERR_FILENO); + if (devnull > STDERR_FILENO) + close(devnull); + } + if (pipefd[1] > STDERR_FILENO) + close(pipefd[1]); + auto raw = make_argv(argv); + execvp(raw[0], raw.data()); + _exit(127); + } + + close(pipefd[1]); + char buf[256]; + ssize_t n = 0; + while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) + output.append(buf, static_cast(n)); + close(pipefd[0]); + + int status = 0; + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + return -errno; + } + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +#endif + } + + void system_shutdown() + { + std::printf("[SDL] shutdown requested (simulated no-op)\n"); + } + + void system_reboot() + { + std::printf("[SDL] reboot requested (simulated no-op)\n"); + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + return it == arg.end() ? std::string() : *it; + } + + static std::vector args_from(const arg_t &arg, size_t index) + { + std::vector out; + auto it = arg.begin(); + for (size_t i = 0; i < index && it != arg.end(); ++i) + ++it; + for (; it != arg.end(); ++it) + out.push_back(*it); + return out; + } + + static volatile int *decode_flag_ptr(const std::string &text) + { + if (text.empty() || text == "0") + return nullptr; + uintptr_t raw = static_cast(std::strtoull(text.c_str(), nullptr, 10)); + return reinterpret_cast(raw); + } + + static bool is_nologin_shell(const char *shell) + { + if (!shell || !shell[0]) + return true; + return std::strstr(shell, "nologin") != nullptr || std::strstr(shell, "/bin/false") != nullptr; + } + + static std::string config_get_str(const char *key, const char *default_val) + { + std::string value = default_val ? default_val : ""; + cp0_signal_config_api({"GetStr", key ? std::string(key) : std::string(), value}, + [&](int code, std::string data) { + if (code == 0) value = std::move(data); + }); + return value; + } + + static const char *get_run_user() + { + static thread_local std::string cfg; + cfg = config_get_str("run_as_user", nullptr); + if (!cfg.empty()) + return cfg.c_str(); + + struct passwd *pwd; + setpwent(); + while ((pwd = getpwent()) != nullptr) { + if (pwd->pw_uid >= 1000 && pwd->pw_uid < 65534 && !is_nologin_shell(pwd->pw_shell)) { + endpwent(); + return pwd->pw_name; + } + } + endpwent(); + return "pi"; + } + + static void exec_as_user(const char *exec_path) + { +#if defined(_WIN32) + (void)exec_path; +#else + const char *user = get_run_user(); + if (getuid() == 0 && std::strcmp(user, "root") != 0) { + struct passwd *pw = getpwnam(user); + if (pw) { + initgroups(pw->pw_name, pw->pw_gid); + setgid(pw->pw_gid); + setuid(pw->pw_uid); + setenv("HOME", pw->pw_dir, 1); + setenv("USER", pw->pw_name, 1); + setenv("LOGNAME", pw->pw_name, 1); + setenv("SHELL", pw->pw_shell[0] ? pw->pw_shell : "/bin/bash", 1); + chdir(pw->pw_dir); + } + } + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); +#endif + } + + static std::vector make_argv(const std::vector &argv) + { + std::vector raw; + raw.reserve(argv.size() + 1); + for (const auto &arg : argv) + raw.push_back(const_cast(arg.c_str())); + raw.push_back(nullptr); + return raw; + } + + static void redirect_to_devnull() + { +#if !defined(_WIN32) + int fd = open("/dev/null", O_RDWR); + if (fd < 0) + return; + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) + close(fd); +#endif + } + + int exec_blocking_sdl(const char *exec_path, volatile int *home_key_flag, int keep_root) + { +#if defined(_WIN32) + (void)exec_path; + (void)home_key_flag; + (void)keep_root; + return -1; +#else + (void)keep_root; + pid_t pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); + _exit(127); + } + int status = 0; + int home_status = 0; + std::chrono::steady_clock::time_point home_start; + while (true) { + int r = waitpid(pid, &status, WNOHANG); + if (r > 0) + break; + if (r < 0) { + status = 0; + break; + } + + if (home_key_flag) { + if (home_status == 0 && *home_key_flag) { + home_status = 1; + home_start = std::chrono::steady_clock::now(); + } else if (home_status == 1) { + if (*home_key_flag) { + auto elapsed = std::chrono::steady_clock::now() - home_start; + if (std::chrono::duration_cast(elapsed).count() >= 5) { + home_status = 2; + kill(pid, SIGINT); + } + } else { + home_status = 0; + } + } else if (home_status == 2) { + auto elapsed = std::chrono::steady_clock::now() - home_start; + if (std::chrono::duration_cast(elapsed).count() >= 8) { + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + break; + } + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + if (home_key_flag) + *home_key_flag = 0; + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +#endif + } + + int exec_blocking_cp0(const char *exec_path, volatile int *home_key_flag, int keep_root) + { +#if defined(_WIN32) || defined(HAL_PLATFORM_SDL) + return exec_blocking_sdl(exec_path, home_key_flag, keep_root); +#else + (void)home_key_flag; + keyboard_pause(); + + int evfd = open(get_kbd_device(), O_RDONLY | O_NONBLOCK); + if (evfd < 0) { + std::perror("[cp0] open evdev"); + keyboard_resume(); + return -1; + } + std::printf("[cp0] Opened evdev %s (no EVIOCGRAB; shared with child)\n", get_kbd_device()); + std::fflush(stdout); + + pid_t pid = fork(); + if (pid < 0) { + close(evfd); + keyboard_resume(); + return -1; + } + if (pid == 0) { + close(evfd); + setpgid(0, 0); + if (keep_root) + execlp("/bin/sh", "sh", "-c", exec_path, static_cast(nullptr)); + else + exec_as_user(exec_path); + _exit(127); + } + setpgid(pid, pid); + + auto esc_down_since = std::chrono::steady_clock::time_point{}; + bool esc_down = false; + int status = 0; + + while (true) { + int r = waitpid(pid, &status, WNOHANG); + if (r > 0) + break; + if (r < 0) { + status = -1; + break; + } + + struct input_event ev; + while (read(evfd, &ev, sizeof(ev)) == static_cast(sizeof(ev))) { + if (ev.type == EV_KEY && ev.code == KEY_ESC) { + if (ev.value == 1) { + esc_down = true; + esc_down_since = std::chrono::steady_clock::now(); + } else if (ev.value == 0) { + esc_down = false; + } + } + } + + if (esc_down) { + auto held_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - esc_down_since).count(); + if (held_ms >= 3000) { + killpg(pid, SIGTERM); + auto t0 = std::chrono::steady_clock::now(); + while (waitpid(pid, &status, WNOHANG) == 0) { + if (std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count() >= 3) { + killpg(pid, SIGKILL); + waitpid(pid, &status, 0); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + close(evfd); + keyboard_resume(); + std::printf("[cp0] Returned to launcher\n"); + std::fflush(stdout); + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +#endif + } + + static const char *get_kbd_device() + { + const char *env = std::getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); + return env ? env : "/dev/input/by-path/platform-3f804000.i2c-event"; + } + +public: + static int api_simple(const arg_t &arg, std::string *data = nullptr) + { + int result = -1; + cp0_signal_process_api(arg, [&](int code, std::string out) { + result = code; + if (data) + *data = std::move(out); + }); + return result; + } +}; + +static bool contains_shell_meta(const char *s) +{ + if (!s) + return true; + static const char *kMeta = "|&;<>`$\\\n\r"; + return std::strpbrk(s, kMeta) != nullptr; +} + +static std::string first_token(const char *exec) +{ + std::istringstream iss(exec ? exec : ""); + std::string token; + iss >> token; + return token; +} + +static bool file_executable(const std::string &path) +{ +#if defined(_WIN32) + (void)path; + return false; +#else + struct stat st; + return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode) && access(path.c_str(), X_OK) == 0; +#endif +} + +extern "C" void init_process(void) +{ + auto process = std::make_shared(); + cp0_signal_process_api.append([process](std::list arg, std::function callback) { + process->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int cp0_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ + return ProcessSystem::api_simple({"ExecBlocking", exec_path ? exec_path : "", + std::to_string(reinterpret_cast(home_key_flag)), + std::to_string(keep_root)}); +} + +extern "C" cp0_pid_t cp0_process_spawn(const char *exec_path, int keep_root) +{ + std::string data; + int ret = ProcessSystem::api_simple({"Spawn", exec_path ? exec_path : "", std::to_string(keep_root)}, &data); + return ret == 0 ? static_cast(std::atoi(data.c_str())) : -1; +} + +extern "C" void cp0_process_stop(cp0_pid_t pid) +{ + ProcessSystem::api_simple({"Stop", std::to_string(pid)}); +} + +extern "C" int cp0_process_check_lock(const char *lock_path, int *holder_pid) +{ + std::string data; + int ret = ProcessSystem::api_simple({"CheckLock", lock_path ? lock_path : ""}, &data); + if (holder_pid) + *holder_pid = std::atoi(data.c_str()); + return ret; +} + +extern "C" void cp0_process_kill(int pid, int grace_ms) +{ + ProcessSystem::api_simple({"Kill", std::to_string(pid), std::to_string(grace_ms)}); +} + +extern "C" int cp0_process_run_argv(const char *const *argv, int background) +{ + std::list args = {"RunArgv", std::to_string(background)}; + if (argv) { + for (int i = 0; argv[i]; ++i) + args.push_back(argv[i]); + } + return ProcessSystem::api_simple(args); +} + +extern "C" int cp0_process_capture_argv(const char *const *argv, char *out, int out_size) +{ + if (out && out_size > 0) + out[0] = '\0'; + std::list args = {"CaptureArgv"}; + if (argv) { + for (int i = 0; argv[i]; ++i) + args.push_back(argv[i]); + } + std::string data; + int ret = ProcessSystem::api_simple(args, &data); + if (out && out_size > 0) + cp0_copy_string(out, out_size, data); + return ret; +} + +extern "C" int cp0_file_read_first_line(const char *path, char *out, int out_size) +{ + if (out && out_size > 0) + out[0] = '\0'; + if (!path || !out || out_size <= 0) + return -1; + std::ifstream file(path); + if (!file.is_open()) + return -1; + std::string line; + if (!std::getline(file, line)) + return -1; + if (!line.empty() && line.back() == '\r') + line.pop_back(); + cp0_copy_string(out, out_size, line); + return 0; +} + +extern "C" int cp0_desktop_exec_is_safe(const char *exec, char *reason, int reason_size) +{ + auto fail = [reason, reason_size](const char *msg) { + cp0_copy_cstr(reason, reason_size, msg ? msg : "unsafe Exec"); + return 0; + }; + + if (!exec || !exec[0]) + return fail("empty Exec"); + if (std::strlen(exec) > 512) + return fail("Exec too long"); + if (contains_shell_meta(exec)) + return fail("Exec contains shell metacharacters"); + + const std::string token = first_token(exec); + if (token.empty()) + return fail("missing executable"); + + if (token.find('/') != std::string::npos) { + if (!file_executable(token)) + return fail("executable path is not executable"); + return 1; + } + + static const char *kAllowedNames[] = {"bash", "python3", "vim", "vi", "nano", "sh"}; + if (std::find(std::begin(kAllowedNames), std::end(kAllowedNames), token) != std::end(kAllowedNames)) + return 1; + + return fail("executable name is not allowlisted"); +} + +extern "C" void cp0_system_shutdown(void) +{ + ProcessSystem::api_simple({"Shutdown"}); +} + +extern "C" void cp0_system_reboot(void) +{ + ProcessSystem::api_simple({"Reboot"}); +} + +extern "C" int hal_process_exec_blocking(const char *exec_path, volatile int *home_key_flag, int keep_root) +{ + return cp0_process_exec_blocking(exec_path, home_key_flag, keep_root); +} + +extern "C" hal_pid_t hal_process_spawn(const char *exec_path, int keep_root) +{ + return cp0_process_spawn(exec_path, keep_root); +} + +extern "C" void hal_process_stop(hal_pid_t pid) +{ + cp0_process_stop(pid); +} + +extern "C" int hal_process_check_lock(const char *lock_path, int *holder_pid) +{ + return cp0_process_check_lock(lock_path, holder_pid); +} + +extern "C" void hal_process_kill(int pid, int grace_ms) +{ + cp0_process_kill(pid, grace_ms); +} + +extern "C" void hal_system_shutdown(void) +{ + cp0_system_shutdown(); +} + +extern "C" void hal_system_reboot(void) +{ + cp0_system_reboot(); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp new file mode 100644 index 00000000..78aea037 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp @@ -0,0 +1,336 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +namespace { + +typedef void *pty_handle_t; + +struct cp0_pty_handle { + int master_fd; + pid_t child_pid; +}; + +class PtySystem { +public: + typedef std::function callback_t; + typedef std::list arg_t; + + pty_handle_t open(const char *cmd, const char *const *args, int cols, int rows) + { +#if defined(__linux__) + if (!cmd || !cmd[0]) return NULL; + + int master_fd = -1; + struct winsize ws = {}; + ws.ws_col = cols; + ws.ws_row = rows; + std::string run_as_user = config_get_str("run_as_user", ""); + + pid_t pid = forkpty(&master_fd, NULL, NULL, &ws); + if (pid < 0) return NULL; + + if (pid == 0) { + setenv("TERM", "vt100", 1); + drop_root_user(run_as_user); + + if (args) + execvp(cmd, const_cast(args)); + else + execlp(cmd, cmd, static_cast(NULL)); + _exit(127); + } + + int flags = fcntl(master_fd, F_GETFL); + if (flags >= 0) fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); + + cp0_pty_handle *pty = static_cast(std::malloc(sizeof(cp0_pty_handle))); + if (!pty) { + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + ::close(master_fd); + return NULL; + } + pty->master_fd = master_fd; + pty->child_pid = pid; + return pty; +#else + (void)cmd; + (void)args; + (void)cols; + (void)rows; + return NULL; +#endif + } + + int read(pty_handle_t pty, char *buf, size_t buf_size) + { +#if defined(__linux__) + if (!pty || !buf || buf_size == 0) return -1; + cp0_pty_handle *h = static_cast(pty); + ssize_t n = ::read(h->master_fd, buf, buf_size); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; + return -1; + } + return static_cast(n); +#else + (void)pty; + (void)buf; + (void)buf_size; + return -1; +#endif + } + + int write(pty_handle_t pty, const char *buf, size_t len) + { +#if defined(__linux__) + if (!pty || !buf) return -1; + cp0_pty_handle *h = static_cast(pty); + return static_cast(::write(h->master_fd, buf, len)); +#else + (void)pty; + (void)buf; + (void)len; + return -1; +#endif + } + + int check_child(pty_handle_t pty, int *exit_status) + { +#if defined(__linux__) + if (!pty) return -1; + cp0_pty_handle *h = static_cast(pty); + int status = 0; + pid_t r = waitpid(h->child_pid, &status, WNOHANG); + if (r == 0) return 0; + if (r > 0) { + if (exit_status) *exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + return 1; + } + return -1; +#else + (void)pty; + (void)exit_status; + return -1; +#endif + } + + void close(pty_handle_t pty) + { +#if defined(__linux__) + if (!pty) return; + cp0_pty_handle *h = static_cast(pty); + kill(h->child_pid, SIGKILL); + waitpid(h->child_pid, NULL, 0); + ::close(h->master_fd); + std::free(h); +#else + (void)pty; +#endif + } + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty pty api\n"); + return; + } + + const std::string cmd = arg.front(); + if (cmd == "Open") { + api_open(arg, callback); + } else if (cmd == "Read") { + api_read(arg, callback); + } else if (cmd == "Write") { + api_write(arg, callback); + } else if (cmd == "CheckChild") { + api_check_child(arg, callback); + } else if (cmd == "Close") { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + close(pty); + report(callback, 0, ""); + } else { + report(callback, -1, "unknown pty api: " + cmd + "\n"); + } + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + if (index >= arg.size()) return ""; + auto it = arg.begin(); + std::advance(it, index); + return *it; + } + + static std::string handle_to_string(pty_handle_t pty) + { + std::ostringstream os; + os << reinterpret_cast(pty); + return os.str(); + } + + static pty_handle_t parse_handle(const std::string &value) + { + if (value.empty()) return NULL; + char *end = NULL; + uintptr_t raw = static_cast(std::strtoull(value.c_str(), &end, 0)); + if (!end || *end != '\0') return NULL; + return reinterpret_cast(raw); + } + + static int parse_int(const std::string &value, int fallback) + { + if (value.empty()) return fallback; + char *end = NULL; + long parsed = std::strtol(value.c_str(), &end, 10); + return (end && *end == '\0') ? static_cast(parsed) : fallback; + } + + static std::string config_get_str(const char *key, const char *default_val) + { + std::string value = default_val ? default_val : ""; + cp0_signal_config_api({"GetStr", key ? std::string(key) : std::string(), value}, + [&](int code, std::string data) { + if (code == 0) value = std::move(data); + }); + return value; + } + + static void drop_root_user(const std::string &configured_user) + { +#if defined(__linux__) + if (getuid() != 0) return; + + const char *username = configured_user.empty() ? NULL : configured_user.c_str(); + if (!username) { + struct passwd *p = NULL; + setpwent(); + while ((p = getpwent()) != NULL) { + if (p->pw_uid >= 1000 && p->pw_uid < 65534 && + p->pw_shell && p->pw_shell[0] && + !std::strstr(p->pw_shell, "nologin") && + !std::strstr(p->pw_shell, "/bin/false")) { + username = p->pw_name; + break; + } + } + endpwent(); + } + if (!username) username = "pi"; + + struct passwd *pw = getpwnam(username); + if (pw && std::strcmp(username, "root") != 0) { + initgroups(pw->pw_name, pw->pw_gid); + setgid(pw->pw_gid); + setuid(pw->pw_uid); + setenv("HOME", pw->pw_dir, 1); + setenv("USER", pw->pw_name, 1); + setenv("LOGNAME", pw->pw_name, 1); + setenv("SHELL", pw->pw_shell[0] ? pw->pw_shell : "/bin/bash", 1); + chdir(pw->pw_dir); + } +#endif + } + + void api_open(const arg_t &arg, callback_t callback) + { + std::string exec = nth_arg(arg, 1); + int cols = parse_int(nth_arg(arg, 2), 80); + int rows = parse_int(nth_arg(arg, 3), 24); + if (exec.empty()) { + report(callback, -1, "empty pty command\n"); + return; + } + + std::vector argv_storage; + for (auto it = arg.begin(); it != arg.end(); ++it) { + size_t index = static_cast(std::distance(arg.begin(), it)); + if (index >= 4) argv_storage.push_back(*it); + } + + std::vector argv; + if (!argv_storage.empty()) { + for (const std::string &item : argv_storage) argv.push_back(item.c_str()); + argv.push_back(NULL); + } + + pty_handle_t pty = open(exec.c_str(), argv.empty() ? NULL : argv.data(), cols, rows); + if (!pty) { + report(callback, -1, "open pty failed\n"); + return; + } + report(callback, 0, handle_to_string(pty)); + } + + void api_read(const arg_t &arg, callback_t callback) + { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + int max_len = parse_int(nth_arg(arg, 2), 4096); + if (max_len <= 0) max_len = 4096; + + std::string out(static_cast(max_len), '\0'); + int n = read(pty, &out[0], out.size()); + if (n < 0) { + report(callback, -1, "read pty failed\n"); + return; + } + out.resize(static_cast(n)); + report(callback, n, out); + } + + void api_write(const arg_t &arg, callback_t callback) + { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + std::string data = nth_arg(arg, 2); + int n = write(pty, data.c_str(), data.size()); + report(callback, n, n < 0 ? "write pty failed\n" : ""); + } + + void api_check_child(const arg_t &arg, callback_t callback) + { + pty_handle_t pty = parse_handle(nth_arg(arg, 1)); + int status = 0; + int ret = check_child(pty, &status); + report(callback, ret, std::to_string(status)); + } +}; + +} // namespace + +extern "C" void init_pty(void) +{ + std::shared_ptr pty = std::make_shared(); + cp0_signal_pty_api.append([pty](std::list arg, std::function callback) { + pty->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp new file mode 100644 index 00000000..dee2e594 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp @@ -0,0 +1,132 @@ +#include "hal_lvgl_bsp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static void write_le16(FILE *f, uint16_t v) { fwrite(&v, 2, 1, f); } +static void write_le32(FILE *f, uint32_t v) { fwrite(&v, 4, 1, f); } + +static int mkdir_p(const char *dir) +{ + if (!dir || !dir[0]) + return -1; + char tmp[512]; + std::snprintf(tmp, sizeof(tmp), "%s", dir); + for (char *p = tmp + 1; *p; ++p) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755) == 0 || errno == EEXIST ? 0 : -1; +} + +class ScreenshotSystem { +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + if (arg.empty()) { + report(callback, -1, "empty screenshot api"); + return; + } + if (arg.front() == "Save") { + const std::string dir = arg.size() >= 2 ? *std::next(arg.begin()) : std::string(); + int ret = save_to_bmp(dir.c_str()); + report(callback, ret, ret == 0 ? "screenshot saved\n" : "screenshot failed\n"); + return; + } + report(callback, -1, "unknown screenshot api"); + } + +private: + static void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static int save_to_bmp(const char *dir) + { + if (mkdir_p(dir) != 0) + return -1; + + std::time_t now = std::time(nullptr); + std::tm *t = std::localtime(&now); + if (!t) + return -1; + + char filename[512]; + std::snprintf(filename, sizeof(filename), "%s/scr_%04d%02d%02d_%02d%02d%02d_sdl.bmp", + dir, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec); + + FILE *fp = std::fopen(filename, "wb"); + if (!fp) + return -1; + + constexpr int w = 320; + constexpr int h = 240; + const int row_size = w * 3; + const int pad = (4 - (row_size % 4)) % 4; + const int bmp_row = row_size + pad; + const uint32_t img_size = static_cast(bmp_row * h); + const uint32_t file_size = 54 + img_size; + + fputc('B', fp); fputc('M', fp); + write_le32(fp, file_size); + write_le16(fp, 0); write_le16(fp, 0); + write_le32(fp, 54); + write_le32(fp, 40); + write_le32(fp, w); + write_le32(fp, h); + write_le16(fp, 1); + write_le16(fp, 24); + write_le32(fp, 0); + write_le32(fp, img_size); + write_le32(fp, 2835); write_le32(fp, 2835); + write_le32(fp, 0); write_le32(fp, 0); + + uint8_t padding[3] = {0, 0, 0}; + for (int y = h - 1; y >= 0; --y) { + for (int x = 0; x < w; ++x) { + const uint8_t r = static_cast((x * 255) / (w - 1)); + const uint8_t g = static_cast((y * 255) / (h - 1)); + const uint8_t b = static_cast(((x / 20 + y / 20) % 2) ? 0x66 : 0x22); + uint8_t bgr[3] = {b, g, r}; + fwrite(bgr, 1, 3, fp); + } + if (pad > 0) + fwrite(padding, 1, static_cast(pad), fp); + } + std::fclose(fp); + std::printf("[SDL SCREENSHOT] Saved simulated screenshot: %s\n", filename); + return 0; + } +}; + +} // namespace + +extern "C" void init_screenshot(void) +{ + auto screenshot = std::make_shared(); + cp0_signal_screenshot_api.append([screenshot](std::list arg, std::function callback) { + screenshot->api_call(std::move(arg), std::move(callback)); + }); +} diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp new file mode 100644 index 00000000..0ddc8409 --- /dev/null +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp @@ -0,0 +1,510 @@ +#include "hal_lvgl_bsp.h" +#include "cp0_lvgl_app.h" + +#ifdef HAL_PLATFORM_SDL +#include "hal/hal_settings.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" time_t cp0_sdl_time_now(void); + +namespace { + +class SettingsSystem +{ +public: + using callback_t = std::function; + using arg_t = std::list; + + void api_call(arg_t arg, callback_t callback) + { + const std::string cmd = arg.empty() ? "" : arg.front(); + if (cmd == "BacklightRead") { + int val = backlight_read(); + report(callback, val < 0 ? -1 : 0, std::to_string(val)); + } else if (cmd == "BacklightMax") { + int val = backlight_max(); + report(callback, val < 0 ? -1 : 0, std::to_string(val)); + } else if (cmd == "BacklightWrite") { + int val = backlight_write(std::atoi(nth_arg(arg, 1).c_str())); + report(callback, val < 0 ? -1 : 0, std::to_string(val)); + } else if (cmd == "BtStatus") { + report(callback, 0, encode_bt_status(bt_get_status())); + } else if (cmd == "BtPower") { + report(callback, bt_set_power(std::atoi(nth_arg(arg, 1).c_str())), ""); + } else if (cmd == "BtScan") { + int max_count = arg.size() >= 2 ? std::atoi(nth_arg(arg, 1).c_str()) : CP0_BT_DEVICE_MAX; + std::vector devices(std::max(0, max_count)); + int count = bt_scan(devices.empty() ? nullptr : devices.data(), static_cast(devices.size())); + report(callback, count, encode_bt_scan(devices.data(), count)); + } else if (cmd == "TimeStr") { + char buf[32] = {}; + time_str(buf, sizeof(buf)); + report(callback, 0, buf); + } else { + report(callback, -1, "unknown settings api command"); + } + } + + static int api_int(const arg_t &arg, int default_value = -1) + { + int result = default_value; + cp0_signal_settings_api(arg, [&](int code, std::string data) { + if (code >= 0) + result = std::atoi(data.c_str()); + }); + return result; + } + + static cp0_bt_status_t api_bt_status() + { + cp0_bt_status_t st{}; + cp0_signal_settings_api({"BtStatus"}, [&](int code, std::string data) { + if (code == 0) + decode_bt_status(data, st); + }); + return st; + } + + static int api_bt_power(int on) + { + int result = -1; + cp0_signal_settings_api({"BtPower", std::to_string(on)}, [&](int code, std::string) { + result = code; + }); + return result; + } + + static int api_bt_scan(cp0_bt_device_t *out, int max_devices) + { + int count = 0; + cp0_signal_settings_api({"BtScan", std::to_string(max_devices)}, [&](int code, std::string data) { + count = out && max_devices > 0 ? decode_bt_scan(data, out, max_devices) : code; + }); + return count; + } + + static void api_time_str(char *buf, int buf_size) + { + if (!buf || buf_size <= 0) + return; + buf[0] = '\0'; + cp0_signal_settings_api({"TimeStr"}, [&](int code, std::string data) { + if (code == 0) + copy_string(buf, static_cast(buf_size), data); + }); + if (buf[0] == '\0') + fallback_time_str(buf, buf_size); + } + +private: + void report(callback_t callback, int code, const std::string &data) + { + if (callback) + callback(code, data); + } + + static std::string nth_arg(const arg_t &arg, size_t index) + { + auto it = arg.begin(); + std::advance(it, std::min(index, arg.size())); + return it == arg.end() ? std::string() : *it; + } + + static void copy_string(char *dst, size_t dst_size, const std::string &src) + { + if (!dst || dst_size == 0) + return; + std::strncpy(dst, src.c_str(), dst_size - 1); + dst[dst_size - 1] = '\0'; + } + + static std::vector split_char(const std::string &line, char delimiter) + { + std::vector cols; + std::string item; + std::istringstream iss(line); + while (std::getline(iss, item, delimiter)) + cols.push_back(item); + return cols; + } + + static std::string encode_bt_status(const cp0_bt_status_t &st) + { + std::ostringstream oss; + oss << st.powered << '\t' << st.address; + return oss.str(); + } + + static bool decode_bt_status(const std::string &data, cp0_bt_status_t &st) + { + auto cols = split_char(data, '\t'); + if (cols.size() < 2) + return false; + st = {}; + st.powered = std::atoi(cols[0].c_str()); + copy_string(st.address, sizeof(st.address), cols[1]); + return true; + } + + static std::string encode_bt_scan(const cp0_bt_device_t *devices, int count) + { + std::ostringstream oss; + for (int i = 0; devices && i < count; ++i) { + oss << devices[i].address << '\t' << devices[i].rssi << '\t' << devices[i].connected << '\t' << devices[i].name << '\n'; + } + return oss.str(); + } + + static int decode_bt_scan(const std::string &data, cp0_bt_device_t *out, int max_devices) + { + if (!out || max_devices <= 0) + return 0; + int count = 0; + std::istringstream lines(data); + std::string line; + while (count < max_devices && std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + auto cols = split_char(line, '\t'); + if (cols.size() < 4 || cols[0].empty()) + continue; + cp0_bt_device_t dev{}; + copy_string(dev.address, sizeof(dev.address), cols[0]); + dev.rssi = std::atoi(cols[1].c_str()); + dev.connected = std::atoi(cols[2].c_str()); + copy_string(dev.name, sizeof(dev.name), cols[3]); + out[count++] = dev; + } + return count; + } + + static void fallback_time_str(char *buf, int buf_size) + { + if (!buf || buf_size <= 0) + return; + std::time_t now = std::time(nullptr); + std::tm *t = std::localtime(&now); + if (!t) { + buf[0] = '\0'; + return; + } + std::snprintf(buf, static_cast(buf_size), "%02d:%02d", t->tm_hour, t->tm_min); + } + +#ifdef HAL_PLATFORM_SDL + int backlight_read() { return hal_backlight_read(); } + int backlight_max() { return hal_backlight_max(); } + int backlight_write(int val) { return hal_backlight_write(val); } + + cp0_bt_status_t bt_get_status() + { + hal_bt_status_t hal = hal_bt_get_status(); + cp0_bt_status_t st{}; + st.powered = hal.powered; + copy_string(st.address, sizeof(st.address), hal.address); + return st; + } + + int bt_set_power(int on) { return hal_bt_set_power(on); } + + int bt_scan(cp0_bt_device_t *out, int max_devices) + { + if (!out || max_devices <= 0) + return hal_bt_scan(nullptr, 0); + + std::vector hal_devices(static_cast(max_devices)); + int count = hal_bt_scan(hal_devices.data(), max_devices); + count = std::min(count, max_devices); + for (int i = 0; i < count; ++i) { + copy_string(out[i].name, sizeof(out[i].name), hal_devices[static_cast(i)].name); + copy_string(out[i].address, sizeof(out[i].address), hal_devices[static_cast(i)].address); + out[i].rssi = hal_devices[static_cast(i)].rssi; + out[i].connected = hal_devices[static_cast(i)].connected; + } + return count; + } + + void time_str(char *buf, int buf_size) { hal_time_str(buf, buf_size); } +#else + static int read_int_file(const char *path, int default_value) + { + FILE *f = std::fopen(path, "r"); + if (!f) + return default_value; + int val = default_value; + if (std::fscanf(f, "%d", &val) != 1) + val = default_value; + std::fclose(f); + return val; + } + + int backlight_read() + { + return read_int_file("/sys/class/backlight/backlight/brightness", -1); + } + + int backlight_max() + { + return read_int_file("/sys/class/backlight/backlight/max_brightness", 100); + } + + int backlight_write(int val) + { + if (val < 0) + val = 0; + int mx = backlight_max(); + if (val > mx) + val = mx; + FILE *f = std::fopen("/sys/class/backlight/backlight/brightness", "w"); + if (!f) + return -1; + std::fprintf(f, "%d", val); + std::fclose(f); + return val; + } + + cp0_bt_status_t bt_get_status() + { + cp0_bt_status_t st{}; + char output[4096] = {}; + const char *argv[] = {"bluetoothctl", "show", nullptr}; + if (cp0_process_capture_argv(argv, output, sizeof(output)) != 0) + return st; + + std::istringstream lines(output); + std::string line; + while (std::getline(lines, line)) { + if (line.find("Powered:") != std::string::npos) + st.powered = line.find("yes") != std::string::npos ? 1 : 0; + std::string marker = "Controller "; + size_t pos = line.find(marker); + if (pos != std::string::npos) { + std::string addr = line.substr(pos + marker.size()); + size_t sp = addr.find(' '); + if (sp != std::string::npos) + addr.resize(sp); + copy_string(st.address, sizeof(st.address), addr); + } + } + return st; + } + + int bt_set_power(int on) + { + const char *argv_on[] = {"bluetoothctl", "power", "on", nullptr}; + const char *argv_off[] = {"bluetoothctl", "power", "off", nullptr}; + char output[1024] = {}; + int ret = cp0_process_capture_argv(on ? argv_on : argv_off, output, sizeof(output)); + if (ret != 0) + return -1; + std::string data(output); + return (data.find("succeeded") != std::string::npos || data.find("Changing") != std::string::npos) ? 0 : -1; + } + + int bt_scan(cp0_bt_device_t *out, int max_devices) + { + const char *scan_on[] = {"bluetoothctl", "scan", "on", nullptr}; + const char *scan_off[] = {"bluetoothctl", "scan", "off", nullptr}; + cp0_process_run_argv(scan_on, 1); + std::this_thread::sleep_for(std::chrono::seconds(4)); + cp0_process_run_argv(scan_off, 0); + + char output[8192] = {}; + const char *devices[] = {"bluetoothctl", "devices", nullptr}; + if (cp0_process_capture_argv(devices, output, sizeof(output)) != 0) + return 0; + + int count = 0; + std::istringstream lines(output); + std::string line; + while (out && count < max_devices && std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line.rfind("Device ", 0) != 0) + continue; + std::string rest = line.substr(7); + size_t sp = rest.find(' '); + if (sp == std::string::npos) + continue; + + cp0_bt_device_t dev{}; + std::string addr = rest.substr(0, sp); + std::string name = rest.substr(sp + 1); + copy_string(dev.address, sizeof(dev.address), addr); + copy_string(dev.name, sizeof(dev.name), name.empty() ? addr : name); + dev.rssi = 0; + dev.connected = 0; + out[count++] = dev; + } + return count; + } + + void time_str(char *buf, int buf_size) { fallback_time_str(buf, buf_size); } +#endif +}; + +} // namespace + +extern "C" void init_settings(void) +{ + auto settings = std::make_shared(); + cp0_signal_settings_api.append([settings](std::list arg, std::function callback) { + settings->api_call(std::move(arg), std::move(callback)); + }); +} + +extern "C" int cp0_backlight_read(void) +{ + return SettingsSystem::api_int({"BacklightRead"}); +} + +extern "C" int cp0_backlight_max(void) +{ + return SettingsSystem::api_int({"BacklightMax"}, 100); +} + +extern "C" int cp0_backlight_write(int val) +{ + return SettingsSystem::api_int({"BacklightWrite", std::to_string(val)}); +} + +extern "C" cp0_bt_status_t cp0_bt_get_status(void) +{ + return SettingsSystem::api_bt_status(); +} + +extern "C" int cp0_bt_set_power(int on) +{ + return SettingsSystem::api_bt_power(on); +} + +extern "C" int cp0_bt_scan(cp0_bt_device_t *out, int max_devices) +{ + return SettingsSystem::api_bt_scan(out, max_devices); +} + +extern "C" void cp0_time_str(char *buf, int buf_size) +{ + SettingsSystem::api_time_str(buf, buf_size); +} + +extern "C" int hal_backlight_read(void) { return 75; } +extern "C" int hal_backlight_max(void) { return 100; } +extern "C" int hal_backlight_write(int val) +{ + return std::max(0, std::min(hal_backlight_max(), val)); +} + +extern "C" hal_battery_info_t hal_battery_read(void) +{ + hal_battery_info_t info{}; + cp0_battery_info_t cp0 = cp0_battery_read(); + info.voltage_mv = cp0.voltage_mv; + info.current_ma = cp0.current_ma; + info.temperature_c10 = cp0.temperature_c10; + info.soc = cp0.soc; + info.remain_mah = cp0.remain_mah; + info.full_mah = cp0.full_mah; + info.flags = cp0.flags; + info.avg_current_ma = cp0.avg_current_ma; + info.valid = cp0.valid; + return info; +} + +extern "C" int hal_volume_read(void) { return 39; } +extern "C" int hal_volume_write(int val) { return std::max(0, std::min(63, val)); } + +extern "C" hal_wifi_status_t hal_wifi_get_status(void) +{ + hal_wifi_status_t st{}; + st.connected = 1; + std::strncpy(st.ssid, "SimulatedWiFi", WIFI_SSID_MAX - 1); + std::strncpy(st.ip, "192.168.1.100", sizeof(st.ip) - 1); + st.signal = 80; + return st; +} + +extern "C" int hal_wifi_scan(hal_wifi_ap_t *out, int max_aps) +{ + if (!out || max_aps <= 0) + return 0; + + const int count = std::min(max_aps, 3); + std::memset(out, 0, sizeof(hal_wifi_ap_t) * static_cast(count)); + if (count > 0) { + std::strncpy(out[0].ssid, "SimulatedWiFi", WIFI_SSID_MAX - 1); + std::strncpy(out[0].security, "WPA2", sizeof(out[0].security) - 1); + out[0].signal = 80; + out[0].in_use = 1; + } + if (count > 1) { + std::strncpy(out[1].ssid, "Neighbor_5G", WIFI_SSID_MAX - 1); + std::strncpy(out[1].security, "WPA2", sizeof(out[1].security) - 1); + out[1].signal = 55; + } + if (count > 2) { + std::strncpy(out[2].ssid, "FreeWiFi", WIFI_SSID_MAX - 1); + std::strncpy(out[2].security, "Open", sizeof(out[2].security) - 1); + out[2].signal = 30; + } + return count; +} + +extern "C" int hal_wifi_connect(const char *ssid, const char *password) +{ + (void)ssid; + (void)password; + return 0; +} + +extern "C" int hal_wifi_disconnect(void) { return 0; } + +extern "C" hal_bt_status_t hal_bt_get_status(void) +{ + hal_bt_status_t st{}; + st.powered = 0; + std::strncpy(st.address, "00:00:00:00:00:00", sizeof(st.address) - 1); + return st; +} + +extern "C" int hal_bt_set_power(int on) +{ + (void)on; + return 0; +} + +extern "C" int hal_bt_scan(hal_bt_device_t *out, int max_devices) +{ + (void)out; + (void)max_devices; + return 0; +} + +extern "C" void hal_time_str(char *buf, int buf_size) +{ + if (!buf || buf_size <= 0) + return; + std::time_t now = cp0_sdl_time_now(); + std::tm *t = std::localtime(&now); + if (!t) { + buf[0] = '\0'; + return; + } + std::snprintf(buf, static_cast(buf_size), "%02d:%02d", t->tm_hour, t->tm_min); +} diff --git a/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c b/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c index c768d49f..86cf0e23 100644 --- a/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c +++ b/ext_components/cp0_lvgl/src/web/cp0_hal_stubs_web.c @@ -1,11 +1,8 @@ #include "hal/hal_audio.h" -#include "hal/hal_config.h" #include "hal/hal_filesystem.h" #include "hal/hal_network.h" #include "hal/hal_paths.h" #include "hal/hal_process.h" -#include "hal/hal_pty.h" -#include "hal/hal_screenshot.h" #include "hal/hal_settings.h" #include @@ -18,13 +15,6 @@ void hal_audio_play_sync(const char *path) { (void)path; } void hal_audio_stop(void) {} void hal_audio_deinit(void) {} -void hal_config_init(void) {} -int hal_config_get_int(const char *key, int default_val) { (void)key; return default_val; } -void hal_config_set_int(const char *key, int val) { (void)key; (void)val; } -const char *hal_config_get_str(const char *key, const char *default_val) { (void)key; return default_val; } -void hal_config_set_str(const char *key, const char *val) { (void)key; (void)val; } -void hal_config_save(void) {} - int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) { (void)path; (void)entries; (void)max_entries; *out_count = 0; return -1; } hal_watcher_t hal_dir_watch_start(const char *path) { (void)path; return 0; } @@ -59,19 +49,6 @@ void hal_process_kill(int pid, int grace_ms) { (void)pid; (void)grace_ms; } void hal_system_shutdown(void) {} void hal_system_reboot(void) {} -hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int cols, int rows) -{ (void)cmd; (void)args; (void)cols; (void)rows; return 0; } -int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) -{ (void)pty; (void)buf; (void)buf_size; return -1; } -int hal_pty_write(hal_pty_t pty, const char *buf, size_t len) -{ (void)pty; (void)buf; (void)len; return -1; } -int hal_pty_check_child(hal_pty_t pty, int *exit_status) -{ (void)pty; (void)exit_status; return -1; } -void hal_pty_close(hal_pty_t pty) { (void)pty; } - -int hal_screenshot_save(const char *dir) -{ (void)dir; return -1; } - hal_battery_info_t hal_battery_read(void) { hal_battery_info_t info; memset(&info, 0, sizeof(info)); return info; } int hal_backlight_read(void) { return -1; } diff --git a/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c b/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c index c768d49f..86cf0e23 100644 --- a/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c +++ b/ext_components/cp0_lvgl/src/win32/cp0_hal_stubs_win32.c @@ -1,11 +1,8 @@ #include "hal/hal_audio.h" -#include "hal/hal_config.h" #include "hal/hal_filesystem.h" #include "hal/hal_network.h" #include "hal/hal_paths.h" #include "hal/hal_process.h" -#include "hal/hal_pty.h" -#include "hal/hal_screenshot.h" #include "hal/hal_settings.h" #include @@ -18,13 +15,6 @@ void hal_audio_play_sync(const char *path) { (void)path; } void hal_audio_stop(void) {} void hal_audio_deinit(void) {} -void hal_config_init(void) {} -int hal_config_get_int(const char *key, int default_val) { (void)key; return default_val; } -void hal_config_set_int(const char *key, int val) { (void)key; (void)val; } -const char *hal_config_get_str(const char *key, const char *default_val) { (void)key; return default_val; } -void hal_config_set_str(const char *key, const char *val) { (void)key; (void)val; } -void hal_config_save(void) {} - int hal_dir_list(const char *path, hal_dirent_t *entries, int max_entries, int *out_count) { (void)path; (void)entries; (void)max_entries; *out_count = 0; return -1; } hal_watcher_t hal_dir_watch_start(const char *path) { (void)path; return 0; } @@ -59,19 +49,6 @@ void hal_process_kill(int pid, int grace_ms) { (void)pid; (void)grace_ms; } void hal_system_shutdown(void) {} void hal_system_reboot(void) {} -hal_pty_t hal_pty_open(const char *cmd, const char *const *args, int cols, int rows) -{ (void)cmd; (void)args; (void)cols; (void)rows; return 0; } -int hal_pty_read(hal_pty_t pty, char *buf, size_t buf_size) -{ (void)pty; (void)buf; (void)buf_size; return -1; } -int hal_pty_write(hal_pty_t pty, const char *buf, size_t len) -{ (void)pty; (void)buf; (void)len; return -1; } -int hal_pty_check_child(hal_pty_t pty, int *exit_status) -{ (void)pty; (void)exit_status; return -1; } -void hal_pty_close(hal_pty_t pty) { (void)pty; } - -int hal_screenshot_save(const char *dir) -{ (void)dir; return -1; } - hal_battery_info_t hal_battery_read(void) { hal_battery_info_t info; memset(&info, 0, sizeof(info)); return info; } int hal_backlight_read(void) { return -1; } diff --git a/projects/APPLaunch/docs/architecture_review.md b/projects/APPLaunch/docs/architecture_review.md new file mode 100644 index 00000000..1f87282a --- /dev/null +++ b/projects/APPLaunch/docs/architecture_review.md @@ -0,0 +1,397 @@ +# APPLaunch 架构审查报告 + +日期:2026-06-15 +范围:`projects/APPLaunch` +方式:主线程梳理 + 3 个并行 agent 分别审查 UI/应用层、平台/构建层、质量/生命周期。 + +## 1. 总体判断 + +APPLaunch 当前更像一个快速演进后的产品型单体:功能能跑起来,但应用注册、平台能力、页面 UI、硬件访问、构建适配、资源路径之间互相穿透。 + +最主要的不和谐不是单点代码风格问题,而是三条架构边界不清: + +1. **应用模型边界不清**:Launcher 列表、Settings 开关、动态 `.desktop` 应用、页面工厂、资源图标各自维护状态。 +2. **平台能力边界不清**:UI 页面中直接出现 `system()`、`popen()`、`/dev`、`/sys`、GPIO/SPI/I2C、平台宏和硬编码 Linux 路径。 +3. **生命周期边界不清**:LVGL 对象、timer、watcher、PTY、动画、页面对象多处裸持有,部分清理依赖路径或进程退出。 + +建议优先修复 P0/P1 的一致性、安全和生命周期问题,再做大规模模块拆分。 + +## 2. 代码结构概览 + +- `projects/APPLaunch/SConstruct`:项目级构建入口,选择默认配置、SDK 路径、交叉编译 static lib/sysroot 等。 +- `projects/APPLaunch/main/SConstruct`:组件构建入口,运行 `main/ui/generate_page_app_includes.py`,编译 `src/*.c*` 和 `ui` 目录。 +- `projects/APPLaunch/main/src/main.cpp`:运行时入口,初始化 LVGL/CP0,注册键盘事件,进入 `APPLaunch_lock()` + `lv_timer_handler()` 主循环。 +- `projects/APPLaunch/main/ui/ui.cpp`:全局 UI 根对象 `std::unique_ptr home`,`launcher_ui::init()` 创建并启动。 +- `projects/APPLaunch/main/ui/zero_lvgl_os.cpp`:应用层组装器,创建主题、字体缓存、`Launch`、`UILaunchPage`,决定启动 GIF 或首页。 +- `projects/APPLaunch/main/ui/Launch.*`:应用列表、应用启动、`.desktop` 扫描、目录 watch、回主页逻辑。 +- `projects/APPLaunch/main/ui/UILaunchPage.*`:首页 UI、轮播控件、键盘/点击事件、启动动画、页面切换动画。 +- `projects/APPLaunch/main/ui/ui_app_page.hpp`:应用页基类和通用 top/content/bottom layout。 +- `projects/APPLaunch/main/ui/page_app/*.hpp`:内置页面,多数是 header-only 千行级实现。 +- `projects/APPLaunch/APPLaunch/share`:运行时资源目录,包含 images/audio/font。 +- `projects/APPLaunch/dist.bak`:备份资源/产物副本,体积和资源来源容易造成歧义。 + +## 3. P0 高优先级问题 + +### 3.1 应用模型不一致 + +- `app::Exec` 字段在 `projects/APPLaunch/main/ui/Launch.h` 中声明,但构造函数没有赋值。 +- `projects/APPLaunch/main/ui/Launch.cpp` 中动态应用去重依赖 `it.Exec == app_exec`,但 `Exec` 实际为空或默认值,导致去重逻辑基本失效。 +- `Exec` 被移动进 lambda 捕获后没有保存在模型对象中,后续无法用于诊断、去重、刷新、权限检查。 + +建议:建立明确的 `AppDescriptor`,至少包含: + +```cpp +name +icon +exec +launch_type // InternalPage / Terminal / ExternalProcess +register_app // appending function/factory for built-in apps +source // Builtin / DesktopFile / Store +config_key +default_enabled +always_on +required_features +``` + +### 3.2 Settings 与 Launcher 列表不一致 + +- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` 维护一份 `app_keys/app_labels/always_on`。 +- `projects/APPLaunch/main/ui/Launch.cpp` 维护另一份实际 Launcher app 列表。 +- Settings 中存在 `Music`,但 Launcher 中没有对应入口。 +- `Compass` 在 Launcher 中存在,但没有统一受配置控制。 +- `Python/Store/CLI/Game/Setting` 的固定/可配置语义与 Settings 展示不完全一致。 + +建议:Settings 不再维护独立 app 清单,只从 `AppRegistry` 读取 `configurable` 项。 + +### 3.3 配置变更不会刷新主页模型 + +- Settings 保存 `app_*` 后没有通知 `Launch`。 +- `Launch::applications_reload()` 当前只保留固定段并重扫 `.desktop`,不会重新应用内置 app enable/disable。 +- 用户在设置页切换 app 后,主页行为可能不变或需要重启。 + +建议:保存配置后发布事件或回调,`Launch` 执行: + +1. 重建内置 app 列表。 +2. 重扫 `.desktop` app。 +3. 规范化当前 selection。 +4. 刷新 carousel。 + +### 3.4 外部命令执行边界弱 + +- `.desktop` 的 `Exec` 从应用目录读取后直接用于终端或外部进程执行。 +- Setup 页面大量使用 `system()` / `popen()` 拼接 shell。 +- SSID、配置字符串、更新命令等可能进入 shell 命令。 +- 阻塞式外部命令会卡住 UI 线程或让恢复路径依赖顺序执行。 + +相关文件: + +- `projects/APPLaunch/main/ui/Launch.cpp` +- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` +- `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` + +建议: + +- 引入统一进程 API,使用 argv 形式执行,避免 shell 拼接。 +- 对 `.desktop Exec` 做 allowlist、owner/permission 校验、路径规范化、最大文件大小/条目数量限制。 +- 对 Setup 的网络、时间、更新等动作抽到 platform service。 +- 外部命令统一返回结构化错误,并显示到 UI hint/诊断页面。 + +### 3.5 对象生命周期存在环和裸资源 + +- `zero_lvgl_os` 持有 `Launch` 和 `UILaunchPage`。 +- `Launch` 持有 `std::shared_ptr`。 +- `UILaunchPage` 持有 `std::shared_ptr`。 +- 这形成 `shared_ptr` 环,`Launch::~Launch()` 中 watcher/timer 清理理论上不可达。 + +相关文件: + +- `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` +- `projects/APPLaunch/main/ui/Launch.h` +- `projects/APPLaunch/main/ui/UILaunchPage.h` +- `projects/APPLaunch/main/ui/Launch.cpp` + +建议: + +- 由 `zero_lvgl_os` 唯一拥有 model/controller/view。 +- 子对象之间使用裸观察指针或 `weak_ptr`。 +- 明确析构顺序:先停 timer/watch/async,再释放页面,再释放 model。 + +## 4. P1 中高优先级问题 + +### 4.1 `Launch` 职责过宽 + +`Launch` 当前同时承担: + +- 内置 app 注册。 +- `.desktop` 解析。 +- 动态 app 去重。 +- 外部进程启动。 +- 内部页面启动。 +- 当前 selection 状态。 +- 目录 watch。 +- 回主页和 UI 刷新协调。 + +建议拆分: + +- `AppRegistry`:内置/动态 app 加载、过滤、排序、去重。 +- `AppLauncher`:外部进程、终端、内部页面启动。 +- `LauncherController`:selection、reload、home navigation。 +- `UILaunchPage`:纯 view + input adapter。 + +### 4.2 平台模型不统一 + +- 构建层使用 `CONFIG_APPLAUNCH_*`。 +- 源码层同时使用 `__linux__`、`HAL_PLATFORM_SDL`、`_WIN32`。 +- 交叉编译时宿主 OS 宏、目标平台宏、后端宏容易混淆。 + +建议建立语义平台宏: + +```cpp +APPLAUNCH_TARGET_CP0 +APPLAUNCH_BACKEND_SDL +APPLAUNCH_HAS_LORA +APPLAUNCH_HAS_CAMERA +APPLAUNCH_HAS_SYSTEM_UPDATE +APPLAUNCH_HAS_WIFI_CONFIG +``` + +源码只消费这些产品语义宏,不直接用宿主 OS 判断功能可用性。 + +### 4.3 构建时生成源码文件 + +- `projects/APPLaunch/main/SConstruct` 每次构建运行 `generate_page_app_includes.py`。 +- 脚本写回 `projects/APPLaunch/main/ui/page_app.h`。 +- 构建副作用会造成脏工作区、IDE/静态分析不稳定、并行构建竞争。 +- 调用端没有检查 return code/stderr,失败时可能继续使用旧文件。 + +建议二选一: + +1. 将 `page_app.h` 作为源码手动维护,禁止构建改写。 +2. 生成到 `build/generated/include`,加入 include path,并使用 `subprocess.run(..., check=True)`。 + +### 4.4 构建脚本承担依赖下载 + +- `projects/APPLaunch/SConstruct` 在交叉编译时检查并下载 static BSP 包。 +- 构建和依赖获取耦合,影响离线构建、CI 可重复性、供应链校验。 + +建议: + +- 新增 `tools/fetch_sysroot.py` 或 SDK bootstrap 步骤。 +- 下载逻辑带 checksum、版本文件、离线缓存。 +- SCons 只校验 sysroot 是否存在和版本是否匹配。 + +### 4.5 平台配置重复严重 + +多个 `*config_defaults.mk` 中 LVGL、font、freetype、thread、asset path 配置大段重复,仅少量后端/toolchain/NEON 差异。 + +建议改成: + +- `config/base_lvgl.mk` +- `config/base_cp0.mk` +- `config/backend_sdl.mk` +- `config/backend_fbdev.mk` +- `config/host_linux.mk` +- `config/host_windows.mk` +- `config/target_aarch64_cp0.mk` + +再由目标配置组合 overlay。 + +### 4.6 SDK 组件内部被应用构建脚本修改 + +`main/SConstruct` 直接过滤 `lv_sdl_keyboard.c`,并向 `lvgl_component` 追加 include/definitions。这让 APPLaunch 知道 SDK 组件内部结构,SDK 升级容易破。 + +建议: + +- SDK 暴露正式配置项或 component hook。 +- APPLaunch 不直接修改 SDK component 内部 `SRCS/DEFINITIONS`。 + +## 5. P2 中优先级问题 + +### 5.1 页面实现 header-only 且体量过大 + +典型文件: + +- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` +- `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` +- `projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp` +- `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` + +问题: + +- UI、状态机、硬件访问、命令执行混在一个文件。 +- 编译边界大,改一个页面牵动 `page_app.h` 聚合 include。 +- 难以 mock 硬件服务,难以写单元测试。 + +建议: + +- `.hpp` 只保留类声明和小型 inline。 +- 复杂实现迁到 `.cpp`。 +- 硬件访问迁到 service/platform 层。 +- UI 只订阅状态和派发 intent。 + +### 5.2 首页轮播状态和 LVGL 对象数组强耦合 + +`UILaunchPage` 使用一个 enum 同时索引 card/title/dot,并在动画中旋转 LVGL 对象数组。UI 对象顺序就是状态机,维护难度高。 + +建议: + +- 抽象 `CarouselModel`:维护当前 index、visible slots、pending switch。 +- `UILaunchPage` 只根据 model 渲染 slots。 +- 动画层只处理 view transition,不修改应用选择状态。 + +### 5.3 全局/静态 UI 状态较多 + +典型状态: + +- `ui.cpp` 的全局 `home`。 +- `UILaunchPage.cpp` 的 `active_launch_page`。 +- `UILaunchPage.cpp` 的全局 `home_input_group`。 +- `ui_loading.cpp` / `ui_global_hint.cpp` 的静态对象。 + +建议: + +- 统一归入 `LauncherRuntime` 或 `UiContext`。 +- 明确 owner、init、shutdown。 +- 测试或热重启时可以完整重置。 + +### 5.4 页面 timer/动画取消不统一 + +- 多数页面手动删除 timer,但一致性不足。 +- `UIIpPanelPage` 等页面存在析构不清理 timer 的风险。 +- 动画回调捕获裸 `lv_obj_t*` 和 lambda,页面删除时缺少统一取消 token。 + +建议: + +- 页面或模块在自身内部直接持有并释放 timer/event/animation 资源。 +- 页面析构统一取消回调。 +- 回调校验 generation/token,而不是只依赖裸指针有效。 + +### 5.5 资源命名和路径语义不统一 + +资源目录中存在大小写、拼写、尺寸后缀混杂: + +- `game_100.png` / `gmae.png` +- `e_mail_100.png` / `email.png` +- `unitENV.png` / `unitenv_100.png` +- `compass_needle_80.png` + +静态图标使用 `cp0_file_path("xxx.png")`,动态 `.desktop` 的 `Icon` 直接保存字符串,两者路径语义不同。 + +建议: + +- 建立资源 manifest 或 `resources.hpp`。 +- 统一命名规则:`snake_case[_size].png`。 +- `.desktop Icon` 统一解析为绝对路径或基于 `cp0_file_path` 的资源相对路径。 + +### 5.6 状态栏和基础布局重复 + +`UIAppTopBar` 和 `home_base` 各自实现 top/status 区域,时间/WiFi/电池样式和刷新逻辑分叉。 + +建议: + +- 提取单一 `TopBar` 组件。 +- 首页和 App 页通过参数配置 logo/title/颜色。 +- 电池、时间、WiFi 刷新逻辑复用。 + +## 6. 安全与可靠性关注点 + +- `.desktop` 文件解析缺少 owner/permission/大小/数量限制。 +- `Exec` 缺少 allowlist 和路径规范化。 +- Console 命令按空格拆分,不支持引用/转义,语义与 shell 不一致。 +- Setup 页面 shell 拼接风险高。 +- `launch_Exec()` 关闭 LVGL timer/input 后没有 RAII guard;异常或卡死时恢复边界弱。 +- 主循环无限轮询,无退出信号、无健康状态、无顶层错误隔离。 +- 字体未初始化时 `launcher_fonts()` 直接 `abort()`,生产恢复性弱。 +- 启动 GIF 对象 pause/load home 后缺少显式删除或空指针清理。 + +## 7. 测试与可观测性缺口 + +当前未看到 `projects/APPLaunch` 下存在系统性的 test/spec。 + +建议优先补最小测试层: + +1. `.desktop` parser:合法/非法 key、重复 Exec、Icon 路径、Terminal/Sysplause 解析。 +2. `AppRegistry`:平台 feature filter、Settings enable/disable、always-on 规则。 +3. Console tokenizer:空命令、参数、路径、错误反馈。 +4. Setup 命令构造:SSID 转义、禁止 shell 注入。 +5. 页面生命周期:timer 创建/析构、reload、go home。 +6. Launch reload:动态应用增删、selection 保持、carousel 刷新。 + +可观测性建议: + +- 统一 `LauncherErrorCode`。 +- 所有 process/file/hardware 失败进入同一日志路径。 +- UI 增加最近 N 条错误诊断页面。 +- 清理 `SLOG*`、`fprintf(stderr)`、`perror` 混用。 + +## 8. 建议落地路线 + +### 第一阶段:修一致性和安全边界 + +1. 修复 `app::Exec` 未赋值问题。 +2. 引入 `AppDescriptor` / `AppRegistry`。 +3. Settings 和 Launcher 共用同一份 app registry。 +4. Settings 保存后通知 Launcher 重建列表。 +5. `.desktop Exec` 做基础 allowlist/权限/路径检查。 +6. Setup 中高风险 `system()/popen()` 先替换为 argv API 或 service wrapper。 + +### 第二阶段:修生命周期和构建副作用 + +1. 打破 `Launch` / `UILaunchPage` 的 `shared_ptr` 环。 +2. 目录 watcher/timer 由 `Launch` 内部直接管理,PTY 由 `UIConsolePage` 自行管理。 +3. `launch_Exec()` 在外部进程返回后直接恢复 LVGL timer/input/display flag。 +4. `page_app.h` 生成到 build 目录,或改为手动维护。 +5. `generate_page_app_includes.py` 失败即中断构建。 + +### 第三阶段:平台和页面架构重构 + +1. 建立语义平台宏和 feature set。 +2. 配置文件改 base + overlay。 +3. BSP/sysroot 下载移出 SCons 主构建。 +4. 大页面拆分为 model/service/view/controller。 +5. LoRa、Camera、Setup 等硬件/系统能力迁到 platform service。 + +### 第四阶段:资源、布局、测试完善 + +1. 资源 manifest 化,统一命名。 +2. 首页和 App 页共用 `TopBar`。 +3. 建立 SDL2 CI 构建和核心单元测试。 +4. 增加诊断页面和错误历史。 +5. 补充“资源所有权图”“页面生命周期图”“平台能力矩阵”。 + +## 9. 首批建议修改文件 + +优先处理: + +- `projects/APPLaunch/main/ui/Launch.h` +- `projects/APPLaunch/main/ui/Launch.cpp` +- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` +- `projects/APPLaunch/main/ui/page_app.h` +- `projects/APPLaunch/main/SConstruct` +- `projects/APPLaunch/SConstruct` +- `projects/APPLaunch/main/ui/UILaunchPage.h` +- `projects/APPLaunch/main/ui/UILaunchPage.cpp` + +## 10. 结论 + +APPLaunch 当前最大收益的第一刀不是全面重写,而是先把应用模型统一:`AppRegistry + AppDescriptor + Settings/Launcher 同源 + 配置变更刷新主页`。这一刀可以直接消除用户可见的不一致,也为后续平台拆分、测试和生命周期治理建立核心支点。 + +## 2026-06-15 zero_lvgl_os 启动职责整理 + +本轮对 `zero_lvgl_os` 的启动职责做了语义拆分: + +- 构造函数继续只负责对象装配:display/theme/fonts、`Launch`、`UILaunchPage`、二者引用关系。 +- `start()` 负责运行期 UI 启动,内部拆成: + - `build_launcher_home()`:创建 LVGL screen、绑定 `Launch` UI、初始化输入 group。 + - `show_initial_screen()`:根据启动动画配置加载首页或启动 GIF。 +- 原 `creat_display()` 更正为 `create_display()`。 +- 原 `create_launcher_home()` 移除,避免名字误导为单纯构建函数;现在创建和显示职责分开。 + +验证: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed +``` + +结果:均通过。 diff --git a/projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md b/projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md new file mode 100644 index 00000000..0a6ebde8 --- /dev/null +++ b/projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md @@ -0,0 +1,242 @@ +# cp0_lvgl / APPLaunch 平台解耦计划 + +日期:2026-06-15 + +## 目标 + +让 APPLaunch 保持平台无关:页面和业务逻辑只调用 `cp0_*` 接口,不直接包含或调用 Linux/macOS/Windows/SDL 特有 API、系统命令、`/dev`、`/sys`、GPIO/SPI/I2C、RadioLib HAL 等实现细节。 + +`cp0_lvgl` 负责承载平台和硬件实现,并保持现有风格: + +- APP 层使用 `cp0_*` facade。 +- 平台层能力尽量靠近已有 `hal_*`/service 实现。 +- C ABI、POD struct、简单返回码。 +- SDL/CP0/Win/Web 可以分别提供实现或 stub。 + +## 当前状态 + +已完成: + +- Settings 页中的 `system()`/`popen()`/`nmcli`/账号信息/eth0 信息/BQ27220 I2C 校准已下沉到 `cp0_lvgl`。 +- APPLaunch 使用 `cp0_*` 调用。 +- `Launch` 的 `.desktop Exec` 安全校验使用 `cp0_desktop_exec_is_safe()`。 + +仍需处理: + +- `cp0_app_platform.cpp` 是杂物文件,职责过宽。 +- `cp0_lvgl_app.h` 继续膨胀,需要后续分组收敛。 +- `ui_app_compass.hpp` 仍直接枚举 `/sys/bus/iio/devices`。 +- `ui_app_lora.hpp` 仍包含 GPIO/SPI/I2C/RadioLib 实现。 +- `AppRegistry.cpp`/`Launch.cpp` 仍有少量平台宏判断。 + +## 阶段计划 + +### 阶段 1:收敛新增 cp0 平台服务 + +目的:不改变 APPLaunch 行为,先让 `cp0_lvgl` 内部职责清晰。 + +动作: + +1. 拆分 `ext_components/cp0_lvgl/src/cp0_app_platform.cpp`: + - `cp0_app_process_utils.cpp`:argv 执行、capture、文件首行读取、desktop Exec 校验。 + - `cp0_app_system_info.cpp`:Ethernet、Account 信息。 + - `cp0_app_wifi_profile.cpp`:WiFi profile 查询、删除、断开 active profile。 + - `cp0_app_update.cpp`:apt update、launcher update。 + - `cp0_app_bq27220.cpp`:BQ27220 calibration。 +2. 维持 `cp0_lvgl_app.h` 当前 ABI,避免 APPLaunch 二次改动。 +3. 构建验证 SDL2。 + +### 阶段 2:命名和边界收敛 + +目的:把新增能力放进更合理的服务边界。 + +动作: + +1. 评估是否新增/扩展 `hal_process`:`run_argv`、`capture_argv`。 +2. 评估 `cp0_eth_info_read()` 是否改为扩展 `cp0_network_list()` 或新增 generic `cp0_network_default_info_read()`。 +3. 将 WiFi profile 命名稳定为: + - `cp0_wifi_profile_exists()` + - `cp0_wifi_profile_forget()` + - `cp0_wifi_profile_disconnect_active()` +4. 将 APPLaunch 专属 update 策略从 cp0_lvgl 长期接口中剥离:优先改成通用 package/service API。 + +### 阶段 3:迁移 Compass IIO + +目的:APPLaunch 不再直接读 `/sys/bus/iio/devices`。 + +动作: + +1. 在 `cp0_lvgl` 新增 sensor/imu/compass 数据接口。 +2. 把 IIO 枚举、raw/scale 读取移动到 cp0_lvgl。 +3. `ui_app_compass.hpp` 只保留 UI 和状态渲染。 +4. SDL 提供 fake/stub sensor 数据。 + +### 阶段 4:迁移 LoRa 硬件 HAL + +目的:APPLaunch 不再包含 GPIO/SPI/I2C/RadioLib HAL。 + +动作: + +1. 在 `cp0_lvgl` 定义 `cp0_lora_*` service:init/deinit/status/send/poll/diag。 +2. 移动 GPIO/SPI/I2C/PI4IO/RadioLib HAL 到 cp0_lvgl。 +3. `ui_app_lora.hpp` 改为纯 UI controller,调用 `cp0_lora_*`。 +4. SDL 提供不可用或模拟实现。 + +### 阶段 5:平台 feature 模型 + +目的:APPLaunch 不再用 `__linux__`/`HAL_PLATFORM_SDL` 判断产品功能。 + +动作: + +1. 新增 `cp0_feature_available(feature)` 或枚举 API。 +2. AppRegistry 用 feature gate 控制 Camera/LoRa/IP panel 等 app。 +3. 移除 APPLaunch 平台宏分支。 + +## 验证标准 + +每阶段至少运行: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +``` + +并扫描 APPLaunch 平台残留: + +```sh +rg -n "(/dev/|/sys/|ioctl\(|system\(|popen\(|fork\(|execvp|nmcli|RadioLib|linux/i2c|spidev|gpio|__linux__|HAL_PLATFORM_SDL)" projects/APPLaunch/main +``` + +## 2026-06-15 执行进展 + +本轮已完成: + +- `cp0_app_platform.cpp` 已拆散为 process/system-info/wifi/update/BQ27220 等独立实现文件。 +- `cp0_lvgl_app.h` 新增 C facade:process argv、desktop Exec 安全检查、network/account、WiFi profile、update、time、BQ27220、Compass、LoRa。 +- `ui_app_compass.hpp` 已移除 IIO `/sys/bus/iio/devices` 枚举和 raw/scale 读取,只消费 `cp0_compass_read()`。 +- `ui_app_lora.hpp` 已移除 Linux GPIO/SPI/I2C/RadioLib HAL、`/dev`、`/sys`、`ioctl()` 等实现,只保留 UI、键盘和状态渲染,硬件层改由 `cp0_lora_*` 提供。 +- LoRa 的 GPIO/SPI/I2C/PI4IO/RadioLib 逻辑已移动到 `ext_components/cp0_lvgl/src/cp0_app_lora.cpp`。 +- `RadioLib` 依赖已加入 `cp0_lvgl/SConstruct`;当前由于 SCons 静态库链接顺序限制,`APPLaunch/main/SConstruct` 仍保留直接 `RadioLib` 依赖以通过链接,后续应在构建系统层修复 transitive link order 后移除。 +- WiFi profile 的旧别名接口已移除声明和实现,保留 `cp0_wifi_profile_exists()` / `cp0_wifi_profile_forget()` / `cp0_wifi_profile_disconnect_active()`。 + +验证: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +``` + +结果:通过。 + +当前 APPLaunch 平台残留扫描结果: + +- 主要剩余为 POSIX 文件管理/轻量平台 include:`main.cpp`、`UILaunchPage.cpp`、`Launch.cpp`、`ui_global_hint.cpp`、`ui_app_page.hpp`、`ui_app_file.hpp`、`ui_app_camera.hpp`、`ui_app_rec.hpp`、`ui_app_console.hpp`、`ui_app_setup.hpp`。 +- `AppRegistry.cpp` / `Launch.cpp` / `zero_lvgl_os.cpp` 仍有 `__linux__` / `HAL_PLATFORM_SDL` feature gate。 +- `ui_app_lora.hpp` 和 `ui_app_compass.hpp` 已无直接硬件/平台访问。 +- `APPLaunch/main/SConstruct` 仍直接声明 `RadioLib`,原因见上面的链接顺序说明。 + +后续建议: + +1. 把 `cp0_app_lora.cpp`、`cp0_app_compass.cpp`、`cp0_app_bq27220.cpp` 等 Linux/硬件实现按 cp0_lvgl 原风格继续拆到平台目录或提供 SDL stub,根 `src/` 仅保留平台无关 glue。 +2. 建立 `cp0_feature_available()`,替换 APPLaunch 中的 `__linux__` / `HAL_PLATFORM_SDL` app gate。 +3. 将 `ui_app_file.hpp`、`ui_app_camera.hpp`、`ui_app_rec.hpp` 的目录创建/遍历/删除收敛到 `cp0_file_*`。 +4. 修复构建系统 transitive static link order 后,从 APPLaunch `REQUIREMENTS` 移除 `RadioLib`。 + +补充验证: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +``` + +结果:均通过。当前仅保留既有/迁移后的 `snprintf` 截断 warning:`cp0_app_lora.cpp` 诊断字符串和 `ui_app_setup.hpp` WiFi title。 + +## 2026-06-15 下一步执行:平台目录收敛 + +本轮继续完成: + +- 将 CP0 硬件专用实现移入 `ext_components/cp0_lvgl/src/cp0/`: + - `cp0_app_bq27220.cpp` + - `cp0_app_compass.cpp` + - `cp0_app_lora.cpp` +- 新增 SDL stub:`ext_components/cp0_lvgl/src/sdl/cp0_app_hardware_stub_sdl.cpp`。 + - SDL 下 `cp0_compass_read()` 返回 unavailable。 + - SDL 下 `cp0_lora_*()` 返回 unavailable/stub 信息。 + - SDL 下 `cp0_bq27220_calibrate()` 返回失败。 +- 修复本轮能处理的截断 warning: + - `cp0_app_lora.cpp` 的 SPI 候选/诊断字符串增加长度限制。 + - `ui_app_setup.hpp` 的 WiFi title 对 SSID/IP 做长度限制。 + +验证: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed +``` + +结果:均通过。 + +当前边界: + +- `src/` 根目录仍保留平台工具/策略文件:`cp0_app_process_utils.cpp`、`cp0_app_system_info.cpp`、`cp0_app_update.cpp`、`cp0_app_wifi_profile.cpp`。 +- 其中 `update` 和 `wifi_profile` 仍含 Linux 命令策略;后续若继续严格化,应拆到 `src/cp0/` 并在 `src/sdl/` 提供 stub,或者抽为更通用的 package/service/process API。 + +## 2026-06-15 WiFi API 收敛 + +本轮将 cp0 WiFi 相关接口统一到 `cp0_signal_wifi_api` / `cp0_lvgl_wifi.cpp` 风格: + +- 新增 `cp0_signal_wifi_api`,签名与 audio/camera API 保持一致: + - `std::list` 参数 + - `std::function` 回调 +- 新增统一实现:`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_wifi.cpp`。 + - `init_wifi()` 注册 `cp0_signal_wifi_api` handler。 + - `cp0_wifi_get_status()`、`cp0_wifi_scan()`、`cp0_wifi_connect()`、`cp0_wifi_disconnect()` 均通过 signal API 转发。 + - `cp0_wifi_profile_forget()`、`cp0_wifi_profile_exists()`、`cp0_wifi_profile_disconnect_active()` 也统一到同一个 signal API。 + - 内部改用 `cp0_process_run_argv()` / `cp0_process_capture_argv()`,不再在 WiFi 实现里使用 `system()` / `popen()` / shell 拼接。 +- 新增 SDL 转接:`ext_components/cp0_lvgl/src/sdl/cp0_lvgl_wifi.cpp`,与 audio 当前写法一致,直接复用 `../cp0/cp0_lvgl_wifi.cpp`。 +- 删除旧的 WiFi profile 独立实现:`ext_components/cp0_lvgl/src/cp0_app_wifi_profile.cpp`。 +- 从旧位置移除重复 WiFi 实现: + - `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` + - `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` +- `cp0_lvgl_init()` 在 CP0/SDL 初始化路径中增加 `init_wifi()`。 + +验证: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed --config=force +g++ -std=c++17 -Iext_components/cp0_lvgl/include -ISDK/github_source/eventpp/include -c ext_components/cp0_lvgl/src/cp0/cp0_lvgl_wifi.cpp -o /tmp/cp0_lvgl_wifi.o +``` + +结果:均通过。 + +## 2026-06-15 Process API 收敛 + +本轮将 `cp0_process_*` 相关接口统一到 `cp0_signal_process_api` / `cp0_lvgl_process.cpp` 风格: + +- 新增 `cp0_signal_process_api`,签名与 audio/wifi/camera API 保持一致: + - `std::list` 参数 + - `std::function` 回调 +- 新增统一实现:`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp`。 + - `init_process()` 注册 `cp0_signal_process_api` handler。 + - `cp0_process_exec_blocking()`、`cp0_process_spawn()`、`cp0_process_stop()`、`cp0_process_check_lock()`、`cp0_process_kill()` 均通过 signal API 转发。 + - `cp0_process_run_argv()`、`cp0_process_capture_argv()` 也统一到同一个 process API。 + - `cp0_system_shutdown()` / `cp0_system_reboot()` 随 process service 一起收口,避免继续留在旧 process 文件中。 + - `.desktop Exec` 安全检查和 `cp0_file_read_first_line()` 作为 process/file utility 保留在同一统一文件中。 +- 新增 SDL 转接:`ext_components/cp0_lvgl/src/sdl/cp0_lvgl_process.cpp`,与 audio/wifi 写法一致,复用 `../cp0/cp0_lvgl_process.cpp`。 +- 初始化路径接入:CP0/SDL 两条 `cp0_lvgl_init()` 都在 audio 后、wifi 前调用 `init_process()`,确保 WiFi/update/system-info 中的 argv API 可用。 +- 删除旧实现: + - `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` + - `ext_components/cp0_lvgl/src/cp0_app_process_utils.cpp` +- SDL 旧 compat 中的 `cp0_process_*` wrapper 已移除,PTY wrapper 保留,因为它不属于本次 process API 收敛范围。 + +验证: + +```sh +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed +g++ -std=c++17 -DHAL_PLATFORM_SDL=1 -Iext_components/cp0_lvgl/include -ISDK/github_source/eventpp/include -c ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp -o /tmp/cp0_lvgl_process_sdl.o +g++ -std=c++17 -Iext_components/cp0_lvgl/include -ISDK/github_source/eventpp/include -c ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp -o /tmp/cp0_lvgl_process_cp0.o +``` + +结果:均通过。 + +协作说明:本轮期间工作树里已有其他人对 filesystem/screenshot/lora/bq27220 等模块的并行修改;编译过程中一度暴露的 filesystem 重复定义属于并行改动范畴,后续再次构建已通过。 diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index 7dd533b6..eebfb3a9 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -30,17 +30,28 @@ LDFLAGS = [] LINK_SEARCH_PATH = [] STATIC_FILES = [] -# run generate_page_app_includes.py to generate the header file for including app pages in the UI. -def run_python_script(script_rel_path): +# Generate page_app includes into build output so builds do not rewrite source files. +def run_python_script(script_rel_path, args=None): script_path = script_rel_path.get_abspath() script_dir = os.path.dirname(script_path) - subprocess.run( - [sys.executable, script_path], + cmd = [sys.executable, script_path] + if args: + cmd += args + result = subprocess.run( + cmd, cwd=script_dir, capture_output=True, text=True ) -run_python_script(AFile('ui/generate_page_app_includes.py')) + if result.stdout: + print(result.stdout, end='') + if result.stderr: + print(result.stderr, end='', file=sys.stderr) + result.check_returncode() + +_generated_include_dir = Path(os.getcwd()) / 'build' / 'generated' / 'include' +run_python_script(AFile('ui/generate_page_app_includes.py'), + ['--output', str(_generated_include_dir / 'generated' / 'page_app.h')]) # Get git commit short hash for version display try: @@ -62,7 +73,7 @@ print('-- Launcher git commit: %s' % _git_commit) SRCS += Glob('src/*.c*') SRCS += append_srcs_dir(ADir('ui')) # add includes -INCLUDE += [ADir('.'), ADir('include')] +INCLUDE += [ADir('.'), ADir('include'), ADir('ui'), str(_generated_include_dir)] # Inject git commit hash as a compile-time macro diff --git a/projects/APPLaunch/main/include/APPLaunch_api.h b/projects/APPLaunch/main/include/APPLaunch_api.h new file mode 100644 index 00000000..f5290ddb --- /dev/null +++ b/projects/APPLaunch/main/include/APPLaunch_api.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void APPLaunch_audio_play_file(const char *path); +void APPLaunch_audio_play_asset(const char *name); +void APPLaunch_system_play_asset(const char *name); +int APPLaunch_volume_read(void); +int APPLaunch_volume_write(int val); + +#ifdef __cplusplus +} +#endif diff --git a/projects/APPLaunch/main/src/APPLaunch_api.cpp b/projects/APPLaunch/main/src/APPLaunch_api.cpp new file mode 100644 index 00000000..9e34f3d3 --- /dev/null +++ b/projects/APPLaunch/main/src/APPLaunch_api.cpp @@ -0,0 +1,89 @@ +#include "APPLaunch_api.h" + +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#ifdef HAL_PLATFORM_SDL +#include "hal/hal_audio.h" +#include "hal/hal_settings.h" +#else +#include +#endif + +#include +#include +#include + +extern "C" { + +void APPLaunch_audio_play_file(const char *path) +{ + if (!path || !path[0]) { + return; + } + +#ifdef HAL_PLATFORM_SDL + hal_audio_play(path); +#else + cp0_signal_audio_api({"PlayFile", std::string(path)}, nullptr); +#endif +} + +void APPLaunch_audio_play_asset(const char *name) +{ + if (!name || !name[0]) { + return; + } + +#ifdef HAL_PLATFORM_SDL + const char *path = cp0_file_path_c(name); + APPLaunch_audio_play_file(path && path[0] ? path : name); +#else + cp0_signal_audio_api({"Play", std::string(name)}, nullptr); +#endif +} + +void APPLaunch_system_play_asset(const char *name) +{ + if (!name || !name[0]) { + return; + } + +#ifdef HAL_PLATFORM_SDL + APPLaunch_audio_play_asset(name); +#else + cp0_signal_system_play(std::string(name)); +#endif +} + +int APPLaunch_volume_read(void) +{ +#ifdef HAL_PLATFORM_SDL + return hal_volume_read(); +#else + int volume = -1; + cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) { + if (code == 0) { + volume = std::atoi(data.c_str()); + } + }); + return volume; +#endif +} + +int APPLaunch_volume_write(int val) +{ +#ifdef HAL_PLATFORM_SDL + return hal_volume_write(val); +#else + int volume = -1; + cp0_signal_audio_api({"VolumeWrite", std::to_string(val)}, [&](int code, std::string data) { + if (code == 0) { + volume = std::atoi(data.c_str()); + } + }); + return volume; +#endif +} + +} diff --git a/projects/APPLaunch/main/ui/AppRegistry.cpp b/projects/APPLaunch/main/ui/AppRegistry.cpp new file mode 100644 index 00000000..352b2c84 --- /dev/null +++ b/projects/APPLaunch/main/ui/AppRegistry.cpp @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#include "AppRegistry.h" + +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" + +#include +#include + +namespace { + +LauncherAppRegistryChangedCallback g_changed_callback = nullptr; +void *g_changed_user_data = nullptr; + +int config_get_int(const char *key, int default_val) +{ + int val = default_val; + cp0_signal_config_api({"GetInt", key ? std::string(key) : std::string(), std::to_string(default_val)}, + [&](int code, std::string data) { + if (code == 0) val = std::atoi(data.c_str()); + }); + return val; +} + +void config_set_int(const char *key, int val) +{ + cp0_signal_config_api({"SetInt", key ? std::string(key) : std::string(), std::to_string(val)}, nullptr); +} + +} // namespace + +bool launcher_app_registry_is_enabled(const AppDescriptor &desc) +{ + if (desc.always_on || !desc.configurable) + return true; + return config_get_int(desc.config_key, 1) != 0; +} + +void launcher_app_registry_set_enabled(const AppDescriptor &desc, bool enabled) +{ + if (desc.always_on || !desc.configurable) + enabled = true; + config_set_int(desc.config_key, enabled ? 1 : 0); +} + +void launcher_app_registry_set_changed_callback(LauncherAppRegistryChangedCallback callback, + void *user_data) +{ + g_changed_callback = callback; + g_changed_user_data = user_data; +} + +void launcher_app_registry_notify_changed() +{ + if (g_changed_callback) + g_changed_callback(g_changed_user_data); +} diff --git a/projects/APPLaunch/main/ui/AppRegistry.h b/projects/APPLaunch/main/ui/AppRegistry.h new file mode 100644 index 00000000..c98c461e --- /dev/null +++ b/projects/APPLaunch/main/ui/AppRegistry.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +struct AppDescriptor { + const char *label; + const char *icon; + const char *config_key; + bool configurable; + bool always_on; +}; + +const AppDescriptor *launcher_app_registry_entries(std::size_t *count); +bool launcher_app_registry_is_enabled(const AppDescriptor &desc); +void launcher_app_registry_set_enabled(const AppDescriptor &desc, bool enabled); + +typedef void (*LauncherAppRegistryChangedCallback)(void *user_data); +void launcher_app_registry_set_changed_callback(LauncherAppRegistryChangedCallback callback, + void *user_data); +void launcher_app_registry_notify_changed(); diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/Launch.cpp index b288f351..da736290 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/Launch.cpp @@ -6,10 +6,11 @@ #include "Launch.h" +#include "AppRegistry.h" #include "ui.h" #include "UILaunchPage.h" #include "ui_loading.h" -#include "page_app.h" +#include "generated/page_app.h" #include "cp0_lvgl_app.h" #include "cp0_lvgl_file.hpp" #include "sample_log.h" @@ -26,6 +27,85 @@ namespace { constexpr size_t kHomeCarouselSlotCount = 5; constexpr int kHomeCarouselCenterSlot = 2; + +using BuiltinAppAppender = void (*)(std::list &apps, const AppDescriptor &desc); + +struct BuiltinAppRegistration { + AppDescriptor desc; + const char *exec; + bool terminal; + bool sysplause; + bool run_as_root; + BuiltinAppAppender append; +}; + +template +void append_page_app(std::list &apps, const AppDescriptor &desc) +{ + apps.emplace_back(desc.label, cp0_file_path(desc.icon), page_v); +} + +void append_builtin_app(std::list &apps, const BuiltinAppRegistration ®istration) +{ + const AppDescriptor &desc = registration.desc; + if (registration.append) { + registration.append(apps, desc); + return; + } + + apps.emplace_back(desc.label, + cp0_file_path(desc.icon), + registration.exec ? registration.exec : "", + registration.terminal, + registration.sysplause, + registration.run_as_root); +} + +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, + {{"MATH", "math_100.png", "app_Math", true, false}, + "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false, true, false, nullptr}, + {{"Compass", "compass_needle_80.png", "app_Compass", true, false}, + nullptr, false, true, false, append_page_app}, +#if defined(__linux__) && !defined(HAL_PLATFORM_SDL) + {{"IP_PANEL", "ip_panel_100.png", "app_IP_Panel", true, false}, + nullptr, false, true, false, append_page_app}, + {{"FILE", "file_100.png", "app_File", true, false}, + nullptr, false, true, false, append_page_app}, + {{"SSH", "ssh_100.png", "app_SSH", true, false}, + nullptr, false, true, false, append_page_app}, + {{"MESH", "mesh_100.png", "app_Mesh", true, false}, + nullptr, false, true, false, append_page_app}, + {{"REC", "rec_100.png", "app_Rec", true, false}, + nullptr, false, true, false, append_page_app}, + {{"CAMERA", "camera_100.png", "app_Camera", true, false}, + nullptr, false, true, false, append_page_app}, + {{"LORA", "lora_100.png", "app_LoRa", true, false}, + nullptr, false, true, false, append_page_app}, + {{"TANK", "tank_100.png", "app_Tank", true, false}, + nullptr, false, true, false, append_page_app}, +#endif +}; +} + +const AppDescriptor *launcher_app_registry_entries(std::size_t *count) +{ + static AppDescriptor descriptors[sizeof(kBuiltinApps) / sizeof(kBuiltinApps[0])]; + static bool initialized = false; + if (!initialized) { + for (std::size_t i = 0; i < sizeof(kBuiltinApps) / sizeof(kBuiltinApps[0]); ++i) + descriptors[i] = kBuiltinApps[i].desc; + initialized = true; + } + + if (count) + *count = sizeof(kBuiltinApps) / sizeof(kBuiltinApps[0]); + return descriptors; } // ============================================================ @@ -52,60 +132,8 @@ void Launch::bind_ui() } bound_ = true; - // Fixed icon; users cannot modify it - app_list.emplace_back("Python", - cp0_file_path("python_100.png"), "python3", true, false); - app_list.emplace_back("STORE", - cp0_file_path("store_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); - app_list.emplace_back("CLI", - cp0_file_path("cli_100.png"), "bash", true, false); - app_list.emplace_back("GAME", - cp0_file_path("game_100.png"), page_v); - - app_list.emplace_back("SETTING", - cp0_file_path("setting_100.png"), page_v); - - - // Dynamic icons filtered by Settings configuration - #define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - - if (APP_ENABLED("Math")) - app_list.emplace_back("MATH", - cp0_file_path("math_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); - - app_list.emplace_back("Compass", - cp0_file_path("compass_needle_80.png"), page_v); - - -#if defined(__linux__) && !defined(HAL_PLATFORM_SDL) - if (APP_ENABLED("IP_Panel")) - app_list.emplace_back("IP_PANEL", - cp0_file_path("ip_panel_100.png"), page_v); - if (APP_ENABLED("File")) - app_list.emplace_back("FILE", - cp0_file_path("file_100.png"), page_v); - if (APP_ENABLED("SSH")) - app_list.emplace_back("SSH", - cp0_file_path("ssh_100.png"), page_v); - if (APP_ENABLED("Mesh")) - app_list.emplace_back("MESH", - cp0_file_path("mesh_100.png"), page_v); - if (APP_ENABLED("Rec")) - app_list.emplace_back("REC", - cp0_file_path("rec_100.png"), page_v); - if (APP_ENABLED("Camera")) - app_list.emplace_back("CAMERA", - cp0_file_path("camera_100.png"), page_v); - if (APP_ENABLED("LoRa")) - app_list.emplace_back("LORA", cp0_file_path("lora_100.png"), page_v); - if (APP_ENABLED("Tank")) - app_list.emplace_back("TANK", cp0_file_path("tank_100.png"), page_v); -#endif - #undef APP_ENABLED - - fixed_count = app_list.size(); + launcher_app_registry_set_changed_callback(app_registry_changed_cb, this); + rebuild_builtin_apps(); applications_load(); refresh_home_carousel(); @@ -114,7 +142,8 @@ void Launch::bind_ui() inotify_init_watch(); // Create a 3s LVGL timer to periodically check directory changes - watch_timer = lv_timer_create(app_dir_watch_cb, 3000, this); + release_watch_timer(); + watch_timer_ = lv_timer_create(app_dir_watch_cb, 3000, this); } @@ -130,8 +159,8 @@ void Launch::lv_go_back_home(void *arg) auto self = (Launch *)arg; SLOGI("[HOME] lv_go_back_home executing (page=%p)", self->app_Page.get()); lv_timer_enable(true); - if (self->launch_page_) - self->launch_page_->show_home_screen(); + if (auto page = self->launch_page_.lock()) + page->show_home_screen(); lv_refr_now(NULL); if (self->app_Page) self->app_Page.reset(); @@ -183,13 +212,12 @@ void Launch::launch_Exec(const std::string &exec, bool keep_root) int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); SLOGI("App %s exited with code %d", exec.c_str(), ret); + lv_timer_enable(true); if (indev) lv_indev_set_group(indev, UILaunchPage::home_input_group()); - if (launch_page_) - launch_page_->show_home_screen(); - /* Child process has returned; we are back on the launcher home. - * Hide the overlay so it doesn't linger. */ + if (auto page = launch_page_.lock()) + page->show_home_screen(); ui_loading::hide(); lv_obj_invalidate(lv_screen_active()); lv_refr_now(disp); @@ -316,6 +344,13 @@ void Launch::applications_load() fprintf(stderr, "applications_load: skip %s (missing Name or Exec)\n", filepath.c_str()); continue; } + char unsafe_reason[128] = {}; + if (!cp0_desktop_exec_is_safe(app_exec.c_str(), unsafe_reason, sizeof(unsafe_reason))) + { + fprintf(stderr, "applications_load: skip %s (unsafe Exec: %s)\n", + filepath.c_str(), unsafe_reason); + continue; + } bool in_list = false; for (const auto &it : app_list) { @@ -331,7 +366,7 @@ void Launch::applications_load() continue; } - app_list.emplace_back(page_title, app_icon, app_exec, app_terminal, app_sysplause); + app_list.emplace_back(page_title, cp0_file_path(app_icon), app_exec, app_terminal, app_sysplause); } closedir(dir); @@ -343,7 +378,24 @@ void Launch::applications_load() void Launch::inotify_init_watch() { const std::string app_dir_path = cp0_file_path("applications"); - dir_watcher = cp0_dir_watch_start(app_dir_path.c_str()); + release_dir_watcher(); + dir_watcher_ = cp0_dir_watch_start(app_dir_path.c_str()); + } + +void Launch::release_dir_watcher() + { + if (dir_watcher_) { + cp0_dir_watch_stop(dir_watcher_); + dir_watcher_ = NULL; + } + } + +void Launch::release_watch_timer() + { + if (watch_timer_) { + lv_timer_delete(watch_timer_); + watch_timer_ = nullptr; + } } // ============================================================ @@ -355,8 +407,8 @@ void Launch::refresh_home_carousel() if (normalized < 0) return; current_app = normalized; - if (launch_page_) - launch_page_->refresh_carousel(); + if (auto page = launch_page_.lock()) + page->refresh_carousel(); } // ============================================================ @@ -364,12 +416,7 @@ void Launch::refresh_home_carousel() // ============================================================ void Launch::applications_reload() { - int sz = (int)app_list.size(); - if (sz > fixed_count) - { - auto it = std::next(app_list.begin(), fixed_count); - app_list.erase(it, app_list.end()); - } + rebuild_builtin_apps(); applications_load(); refresh_home_carousel(); } @@ -399,10 +446,10 @@ const app *Launch::app_at_index(int index) const void Launch::app_dir_watch_cb(lv_timer_t *timer) { auto *self = static_cast(lv_timer_get_user_data(timer)); - if (!self || !self->dir_watcher) + if (!self || !self->dir_watcher_) return; - if (cp0_dir_watch_poll(self->dir_watcher) > 0) + if (cp0_dir_watch_poll(self->dir_watcher_) > 0) { SLOGI("app_dir_watch_cb: applications dir changed, reloading..."); self->applications_reload(); @@ -418,6 +465,7 @@ inline app::app(std::string name, std::string exec, bool terminal) : Name(std::move(name)), Icon(std::move(icon)){ + Exec = exec; launch = [exec = std::move(exec), terminal](Launch *ctx) { if (terminal) @@ -433,6 +481,7 @@ inline app::app(std::string name, bool terminal, bool sysplause) : Name(std::move(name)), Icon(std::move(icon)){ + Exec = exec; launch = [exec = std::move(exec), terminal, sysplause](Launch *ctx) { if (terminal) @@ -449,6 +498,7 @@ inline app::app(std::string name, bool sysplause, bool run_as_root) : Name(std::move(name)), Icon(std::move(icon)){ + Exec = exec; launch = [exec = std::move(exec), terminal, sysplause, run_as_root](Launch *ctx) { if (terminal) @@ -490,16 +540,9 @@ app::app(std::string name, // ============================================================ Launch::~Launch() { - if (watch_timer) - { - lv_timer_delete(watch_timer); - watch_timer = nullptr; - } - if (dir_watcher) - { - cp0_dir_watch_stop(dir_watcher); - dir_watcher = NULL; - } + launcher_app_registry_set_changed_callback(nullptr, nullptr); + release_watch_timer(); + release_dir_watcher(); } Launch::Launch() = default; @@ -508,3 +551,25 @@ void Launch::set_launch_page(std::shared_ptr launch_page) { launch_page_ = std::move(launch_page); } + + +void Launch::rebuild_builtin_apps() +{ + app_list.clear(); + + for (const auto ®istration : kBuiltinApps) { + if (!launcher_app_registry_is_enabled(registration.desc)) + continue; + append_builtin_app(app_list, registration); + } + + fixed_count = app_list.size(); + current_app = normalized_app_index(current_app); +} + +void Launch::app_registry_changed_cb(void *user_data) +{ + auto *self = static_cast(user_data); + if (self) + self->applications_reload(); +} diff --git a/projects/APPLaunch/main/ui/Launch.h b/projects/APPLaunch/main/ui/Launch.h index 6fff870c..b06c40a4 100644 --- a/projects/APPLaunch/main/ui/Launch.h +++ b/projects/APPLaunch/main/ui/Launch.h @@ -62,18 +62,22 @@ class Launch void launch_Exec(const std::string &exec, bool keep_root = false); void applications_load(); void inotify_init_watch(); + void release_dir_watcher(); + void release_watch_timer(); void refresh_home_carousel(); void applications_reload(); + void rebuild_builtin_apps(); int normalized_app_index(int index) const; const app *app_at_index(int index) const; static void lv_go_back_home(void *arg); static void app_dir_watch_cb(lv_timer_t *timer); + static void app_registry_changed_cb(void *user_data); - std::shared_ptr launch_page_; + std::weak_ptr launch_page_; int current_app = 2; - cp0_watcher_t dir_watcher = NULL; - lv_timer_t *watch_timer = nullptr; + cp0_watcher_t dir_watcher_ = NULL; + lv_timer_t *watch_timer_ = nullptr; int fixed_count = 0; bool bound_ = false; std::list app_list; diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 80f13135..5f81a498 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -6,6 +6,7 @@ #include "UILaunchPage.h" +#include "APPLaunch_api.h" #include "Launch.h" #include "lvgl/src/widgets/gif/lv_gif.h" #include "sample_log.h" @@ -142,7 +143,7 @@ static const CarouselSlot CAROUSEL_SLOTS[] = { static void audio_play_ui_asset(const char *name) { - cp0_signal_system_play_asset(name); + APPLaunch_system_play_asset(name); } static void audio_play_switch(void) @@ -316,7 +317,7 @@ void UILaunchPage::show_home_screen() void UILaunchPage::load_home_screen() { show_home_screen(); - cp0_signal_audio_api_play_asset("startup.mp3"); + APPLaunch_audio_play_asset("startup.mp3"); } void UILaunchPage::start_startup_gif() @@ -330,8 +331,8 @@ void UILaunchPage::start_startup_gif() lv_disp_load_scr(startup_gif_); } -UILaunchPage::UILaunchPage(std::shared_ptr launch) - : home_base(), launch_(std::move(launch)) +UILaunchPage::UILaunchPage(Launch *launch) + : home_base(), launch_(launch) { active_launch_page = this; } diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index f881c14f..b344d5d2 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -15,7 +15,7 @@ class Launch; class UILaunchPage : public home_base { public: - explicit UILaunchPage(std::shared_ptr launch); + explicit UILaunchPage(Launch *launch); ~UILaunchPage(); void show_home_screen(); @@ -83,7 +83,7 @@ class UILaunchPage : public home_base static void on_home_key(lv_event_t *event); static void on_startup_gif_event(lv_event_t *event); - std::shared_ptr launch_; + Launch *launch_ = nullptr; lv_obj_t *startup_gif_ = nullptr; lv_obj_t *left_arrow_button_ = nullptr; lv_obj_t *right_arrow_button_ = nullptr; diff --git a/projects/APPLaunch/main/ui/generate_page_app_includes.py b/projects/APPLaunch/main/ui/generate_page_app_includes.py index 7e1f438e..b3b0e20b 100644 --- a/projects/APPLaunch/main/ui/generate_page_app_includes.py +++ b/projects/APPLaunch/main/ui/generate_page_app_includes.py @@ -3,25 +3,27 @@ # # SPDX-License-Identifier: MIT +import argparse import os +import sys PAGE_APP_DIR = "page_app" -OUTPUT_FILE = "page_app.h" +DEFAULT_OUTPUT_FILE = "page_app.h" -def generate_includes(): + +def generate_includes(output_file=DEFAULT_OUTPUT_FILE): script_dir = os.path.dirname(os.path.abspath(__file__)) page_app_path = os.path.join(script_dir, PAGE_APP_DIR) - output_path = os.path.join(script_dir, OUTPUT_FILE) + output_path = output_file + if not os.path.isabs(output_path): + output_path = os.path.join(script_dir, output_path) if not os.path.isdir(page_app_path): - print(f"Error: Directory '{PAGE_APP_DIR}' not found.") - return + raise FileNotFoundError(f"Directory '{PAGE_APP_DIR}' not found.") hpp_files = sorted([f for f in os.listdir(page_app_path) if f.endswith(".hpp")]) - if not hpp_files: - print(f"No .hpp files found in '{PAGE_APP_DIR}'.") - return + raise RuntimeError(f"No .hpp files found in '{PAGE_APP_DIR}'.") new_content = """/* * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD @@ -35,19 +37,32 @@ def generate_includes(): for hpp_file in hpp_files: new_content += f'#include "{PAGE_APP_DIR}/{hpp_file}"\n' + os.makedirs(os.path.dirname(output_path), exist_ok=True) if os.path.exists(output_path): with open(output_path, "r") as f: old_content = f.read() if old_content == new_content: - print(f"{OUTPUT_FILE} is already up to date. No changes made.") + print(f"{output_path} is already up to date. No changes made.") return with open(output_path, "w") as f: f.write(new_content) - print(f"Successfully updated {OUTPUT_FILE} with {len(hpp_files)} includes:") + print(f"Successfully updated {output_path} with {len(hpp_files)} includes:") for hpp_file in hpp_files: print(f" - {hpp_file}") + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--output", default=DEFAULT_OUTPUT_FILE) + args = parser.parse_args() + generate_includes(args.output) + + if __name__ == "__main__": - generate_includes() + try: + main() + except Exception as exc: + print(f"generate_page_app_includes.py: {exc}", file=sys.stderr) + sys.exit(1) diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp index a8826ca6..2505a90a 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp @@ -11,21 +11,13 @@ #include "compat/input_keys.h" #include "hal_lvgl_bsp.h" #include -#include #include #include #include #include #include -#include #include -#include -#include #include -#include -#include -#include -#include #include /* @@ -77,7 +69,6 @@ class UICompassPage : public AppPageRoot ~UICompassPage() { - stop_sensor_thread(); if (sensor_timer_) { lv_timer_delete(sensor_timer_); sensor_timer_ = nullptr; @@ -86,17 +77,6 @@ class UICompassPage : public AppPageRoot } private: - struct IioDevicePaths { - std::string accel; - std::string magn; - bool hasGyro = false; - - bool ready() const - { - return !accel.empty() && !magn.empty(); - } - }; - struct CompassUiState { std::string statusText = "Compass"; float yaw = 0.0f; @@ -132,11 +112,6 @@ class UICompassPage : public AppPageRoot std::array lbl_bottom_indicators_{}; lv_timer_t* sensor_timer_ = nullptr; - std::thread sensor_thread_; - std::atomic sensor_running_{false}; - std::mutex sensor_mutex_; - CompassUiState sensor_state_{}; - bool sensor_state_dirty_ = false; CompassUiState last_state_{}; static lv_color_t color(uint32_t hex) @@ -162,14 +137,9 @@ class UICompassPage : public AppPageRoot create_bottom_bar(root_screen_); create_sensor_missing_overlay(root_screen_); - CompassUiState initial_state; - IioDevicePaths initial_paths = enumerate_iio_devices(); - initial_state.statusText = initial_paths.ready() ? "Sensor starting" : "IIO sensor missing"; - initial_state.sensorReady = initial_paths.ready(); - update_from_state(initial_state); - start_sensor_thread(); + update_from_state(CompassUiState{}); sensor_timer_ = lv_timer_create(&UICompassPage::sensor_timer_cb, 50, this); - poll_sensor_once(); + request_compass_render(); } void create_status_bar(lv_obj_t* parent) @@ -332,218 +302,49 @@ class UICompassPage : public AppPageRoot lv_label_set_text(lbl_bottom_btns_[idx], text); } - /* - * ============================================================ - * IIO 驱动枚举与数据读取 - * ============================================================ - */ - static bool file_exists(const std::string& path) - { - struct stat st; - return stat(path.c_str(), &st) == 0; - } - - static bool read_text_file(const std::string& path, std::string& out) - { - std::ifstream ifs(path); - if (!ifs.is_open()) return false; - std::getline(ifs, out); - return true; - } - - static bool read_float_file(const std::string& path, float& out) - { - std::ifstream ifs(path); - if (!ifs.is_open()) return false; - ifs >> out; - return !ifs.fail(); - } - - static float read_float_file_or(const std::string& path, float fallback) - { - float v = fallback; - return read_float_file(path, v) ? v : fallback; - } - - static bool has_accel_files(const std::string& dir) - { - return file_exists(dir + "/in_accel_x_raw") && - file_exists(dir + "/in_accel_y_raw") && - file_exists(dir + "/in_accel_z_raw"); - } - - static bool has_magn_files(const std::string& dir) - { - return file_exists(dir + "/in_magn_x_raw") && - file_exists(dir + "/in_magn_y_raw") && - file_exists(dir + "/in_magn_z_raw"); - } - - static bool has_gyro_files(const std::string& dir) - { - return file_exists(dir + "/in_anglvel_x_raw") && - file_exists(dir + "/in_anglvel_y_raw") && - file_exists(dir + "/in_anglvel_z_raw"); - } - - static IioDevicePaths enumerate_iio_devices() - { - static constexpr const char* kIioRoot = "/sys/bus/iio/devices"; - IioDevicePaths paths; - - DIR* dp = opendir(kIioRoot); - if (!dp) return paths; - - while (dirent* ent = readdir(dp)) { - if (std::strncmp(ent->d_name, "iio:device", 10) != 0) continue; - - std::string dir = std::string(kIioRoot) + "/" + ent->d_name; - if (paths.accel.empty() && has_accel_files(dir)) { - paths.accel = dir; - paths.hasGyro = has_gyro_files(dir); - } - if (paths.magn.empty() && has_magn_files(dir)) { - paths.magn = dir; - } - } - - closedir(dp); - return paths; - } - - bool read_axis_triplet(const std::string& dir, const char* prefix, - float scale, float& x, float& y, float& z) const - { - float rx = 0.0f; - float ry = 0.0f; - float rz = 0.0f; - if (!read_float_file(dir + "/" + prefix + "_x_raw", rx)) return false; - if (!read_float_file(dir + "/" + prefix + "_y_raw", ry)) return false; - if (!read_float_file(dir + "/" + prefix + "_z_raw", rz)) return false; - - x = rx * scale; - y = ry * scale; - z = rz * scale; - return true; - } - - bool read_iio_state(IioDevicePaths& paths, CompassUiState& state) + static CompassUiState state_from_compass_info(int code, const cp0_compass_info_t* info) { - if (!paths.ready()) { - paths = enumerate_iio_devices(); - } - - if (!paths.ready()) { - state.statusText = "IIO sensor missing"; - state.sensorReady = false; - return false; - } - - const float acc_scale = read_float_file_or(paths.accel + "/in_accel_scale", 1.0f); - const float gyr_scale = read_float_file_or(paths.accel + "/in_anglvel_scale", 1.0f); - const float mag_scale = read_float_file_or(paths.magn + "/in_magn_scale", 1.0f); - - float acc_x = 0.0f, acc_y = 0.0f, acc_z = 0.0f; - float mag_x = 0.0f, mag_y = 0.0f, mag_z = 0.0f; - float gyr_x = 0.0f, gyr_y = 0.0f, gyr_z = 0.0f; - - if (!read_axis_triplet(paths.accel, "in_accel", acc_scale, acc_x, acc_y, acc_z) || - !read_axis_triplet(paths.magn, "in_magn", mag_scale, mag_x, mag_y, mag_z)) { - state.statusText = "IIO read failed"; + CompassUiState state; + if (!info || code != 0 || !info->sensor_ready) { + state.statusText = (info && info->status[0]) ? info->status : "Compass sensor missing"; state.sensorReady = false; - return false; + return state; } - if (paths.hasGyro) { - read_axis_triplet(paths.accel, "in_anglvel", gyr_scale, gyr_x, gyr_y, gyr_z); - } - - float pitch = std::atan2(-acc_x, std::sqrt(acc_y * acc_y + acc_z * acc_z)); - float roll = std::atan2(acc_y, acc_z); - float sin_p = std::sin(pitch); - float cos_p = std::cos(pitch); - float sin_r = std::sin(roll); - float cos_r = std::cos(roll); - - float mag_x_h = mag_x * cos_p + mag_z * sin_p; - float mag_y_h = mag_x * sin_r * sin_p + mag_y * cos_r - mag_z * sin_r * cos_p; - float yaw = std::atan2(-mag_y_h, mag_x_h) * 180.0f / 3.1415926f; - if (yaw < 0.0f) yaw += 360.0f; - - state.statusText = "Sensor OK"; + state.statusText = info->status[0] ? info->status : "Sensor OK"; state.sensorReady = true; - state.yaw = yaw; - state.pitch = pitch * 180.0f / 3.1415926f; - state.roll = roll * 180.0f / 3.1415926f; - state.accX = acc_x; - state.accY = acc_y; - state.accZ = acc_z; - state.gyrX = gyr_x; - state.gyrY = gyr_y; - state.gyrZ = gyr_z; - state.magX = mag_x; - state.magY = mag_y; - state.magZ = mag_z; - return true; - } - - void start_sensor_thread() - { - if (sensor_running_.load()) return; - sensor_running_ = true; - sensor_thread_ = std::thread(&UICompassPage::sensor_thread_func, this); - } - - void stop_sensor_thread() - { - sensor_running_ = false; - if (sensor_thread_.joinable()) { - sensor_thread_.join(); - } - } - - void publish_sensor_state(const CompassUiState& state) - { - std::lock_guard lock(sensor_mutex_); - sensor_state_ = state; - sensor_state_dirty_ = true; + state.yaw = info->yaw; + state.pitch = info->pitch; + state.roll = info->roll; + state.accX = info->acc_x; + state.accY = info->acc_y; + state.accZ = info->acc_z; + state.gyrX = info->gyr_x; + state.gyrY = info->gyr_y; + state.gyrZ = info->gyr_z; + state.magX = info->mag_x; + state.magY = info->mag_y; + state.magZ = info->mag_z; + return state; } - void sensor_thread_func() + void request_compass_render() { - IioDevicePaths paths = enumerate_iio_devices(); - CompassUiState state; - - while (sensor_running_.load()) { - read_iio_state(paths, state); - publish_sensor_state(state); - usleep(50000); /* 50 ms => 20 Hz, matching the original Compass app */ - } + cp0_compass_read(&UICompassPage::compass_read_cb, this); } - void poll_sensor_once() + static void compass_read_cb(int code, const cp0_compass_info_t* info, void* user) { - CompassUiState state; - bool dirty = false; - { - std::lock_guard lock(sensor_mutex_); - dirty = sensor_state_dirty_; - if (dirty) { - state = sensor_state_; - sensor_state_dirty_ = false; - } - } - - if (dirty) { - update_from_state(state); - } + auto* self = static_cast(user); + if (!self) return; + self->update_from_state(state_from_compass_info(code, info)); } static void sensor_timer_cb(lv_timer_t* t) { auto* self = static_cast(lv_timer_get_user_data(t)); if (!self) return; - self->poll_sensor_once(); + self->request_compass_render(); } /* diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp index ef4a07ea..87d37a30 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "cp0_lvgl_app.h" @@ -106,7 +107,7 @@ class UIConsolePage : public AppPage */ void exec(std::string cmd) { - if (pty_handle != NULL) + if (!pty_handle.empty()) stop_pty(); terminal_active = true; @@ -222,7 +223,7 @@ class UIConsolePage : public AppPage bool vt100_skip_until_st = false; /* ── PTY ──────────────────────────────────────────────── */ - cp0_pty_t pty_handle = NULL; + std::string pty_handle; lv_timer_t *poll_timer = nullptr; lv_timer_t *cursor_timer = nullptr; @@ -339,7 +340,7 @@ class UIConsolePage : public AppPage } else { - if (pty_handle != NULL && terminal_active) + if (!pty_handle.empty() && terminal_active) { if (elm->key_state) { SLOGI("[CONSOLE] -> PTY write (state=%s)", kbd_state_name(elm->key_state)); @@ -686,8 +687,8 @@ class UIConsolePage : public AppPage case 'c': /* Secondary Device Attributes */ /* Reply: VT100 (type 0), firmware v10, no options */ fprintf(stderr, "[VT100-DBG] SDA reply: \\033[>0;10;0c\n"); - if (pty_handle != NULL) - cp0_pty_write(pty_handle, "\033[>0;10;0c", 10); + if (!pty_handle.empty()) + pty_write( "\033[>0;10;0c", 10); break; case 'm': /* xterm set-modifyOtherKeys — ignore */ fprintf(stderr, "[VT100-DBG] SDA: set-modifyOtherKeys ignored\n"); @@ -816,16 +817,16 @@ class UIConsolePage : public AppPage /* ── Device control ────────────────────── */ case 'c': /* DA — Device Attributes: reply with \033[?1;0c (VT100) */ - if (pty_handle != NULL) { + if (!pty_handle.empty()) { const char *reply = "\033[?1;0c"; - cp0_pty_write(pty_handle, reply, strlen(reply)); + pty_write( reply, strlen(reply)); } break; case 'n': /* DSR — Device Status Report */ fprintf(stderr, "[VT100-DBG] DSR query param[0]=%d\n", vt100_params[0]); if (vt100_params[0] == 5) { fprintf(stderr, "[VT100-DBG] DSR 5: reply \\033[0n (OK)\n"); - if (pty_handle != NULL) cp0_pty_write(pty_handle, "\033[0n", 4); + if (!pty_handle.empty()) pty_write( "\033[0n", 4); } else if (vt100_params[0] == 6) { /* Cursor Position Report */ char buf[32]; @@ -833,7 +834,7 @@ class UIConsolePage : public AppPage vt100_cur_row + 1, vt100_cur_col + 1); fprintf(stderr, "[VT100-DBG] DSR 6: cursor=(%d,%d) reply=%s\n", vt100_cur_row + 1, vt100_cur_col + 1, buf); - if (pty_handle != NULL) cp0_pty_write(pty_handle, buf, len); + if (!pty_handle.empty()) pty_write( buf, len); } break; @@ -1059,22 +1060,78 @@ class UIConsolePage : public AppPage /* ================================================================== */ /* PTY management */ /* ================================================================== */ + std::string pty_open(const std::string &cmd, const std::vector &args) + { + int code = -1; + std::string handle; + std::list api_args = { + "Open", + cmd, + std::to_string(COLS), + std::to_string(ROWS), + cmd, + }; + for (const auto &arg : args) + api_args.push_back(arg); + + cp0_signal_pty_api(std::move(api_args), [&](int c, std::string data) { + code = c; + if (code == 0) handle = std::move(data); + }); + return handle; + } + + int pty_read(char *buf, size_t buf_size) + { + int code = -1; + std::string data; + cp0_signal_pty_api({"Read", pty_handle, std::to_string(buf_size)}, [&](int c, std::string d) { + code = c; + data = std::move(d); + }); + if (code < 0) return -1; + + size_t n = data.size() < buf_size ? data.size() : buf_size; + if (n > 0 && buf) + memcpy(buf, data.data(), n); + return (int)n; + } + + int pty_write(const char *buf, size_t len) + { + if (pty_handle.empty() || !buf) return -1; + int code = -1; + cp0_signal_pty_api({"Write", pty_handle, std::string(buf, len)}, [&](int c, std::string) { + code = c; + }); + return code; + } + + int pty_check_child(int *status) + { + int code = -1; + std::string data; + cp0_signal_pty_api({"CheckChild", pty_handle}, [&](int c, std::string d) { + code = c; + data = std::move(d); + }); + if (status) *status = atoi(data.c_str()); + return code; + } + bool start_pty(const std::string &cmd, const std::vector &args = {}) { - std::vector argv; - argv.push_back(cmd.c_str()); - for (const auto &a : args) - argv.push_back(a.c_str()); - argv.push_back(nullptr); - pty_handle = cp0_pty_open(cmd.c_str(), argv.data(), COLS, ROWS); - return pty_handle != NULL; + stop_pty(); + pty_handle = pty_open(cmd, args); + return !pty_handle.empty(); } void stop_pty() { - if (pty_handle) { - cp0_pty_close(pty_handle); - pty_handle = NULL; + if (!pty_handle.empty()) + { + cp0_signal_pty_api({"Close", pty_handle}, nullptr); + pty_handle.clear(); } } @@ -1084,14 +1141,14 @@ class UIConsolePage : public AppPage void vt100_poll_cb(lv_timer_t *t) { (void)t; - if (pty_handle == NULL || !terminal_active) + if (pty_handle.empty() || !terminal_active) return; char buf[1024]; int n; bool changed = false; - while ((n = cp0_pty_read(pty_handle, buf, sizeof(buf))) > 0) + while ((n = pty_read( buf, sizeof(buf))) > 0) { vt100_process_bytes(buf, n); changed = true; @@ -1105,10 +1162,10 @@ class UIConsolePage : public AppPage { child_exited = true; } - else if (pty_handle != NULL) + else if (!pty_handle.empty()) { int status = 0; - if (cp0_pty_check_child(pty_handle, &status) == 1) + if (pty_check_child( &status) == 1) child_exited = true; } @@ -1119,8 +1176,7 @@ class UIConsolePage : public AppPage vt100_process_bytes(hint, (int)strlen(hint)); vt100_render_all(); waiting_key_to_exit = true; - cp0_pty_close(pty_handle); - pty_handle = NULL; + stop_pty(); } } @@ -1160,7 +1216,7 @@ class UIConsolePage : public AppPage */ void write_key_to_pty(uint32_t evdev_key, const char *utf8_str) { - if (!terminal_active || pty_handle == NULL) + if (!terminal_active || pty_handle.empty()) return; char buf[8]; int len = 0; @@ -1217,7 +1273,7 @@ class UIConsolePage : public AppPage for (int ki = 0; ki < len; ki++) fprintf(stderr, "%02X ", (unsigned char)buf[ki]); fprintf(stderr, "\n"); - cp0_pty_write(pty_handle, buf, (size_t)len); + pty_write( buf, (size_t)len); } } diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp index 5d710f12..644f2705 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp @@ -5,186 +5,29 @@ */ #pragma once -#include "sample_log.h" -#if !defined(HAL_PLATFORM_SDL) -#include "ui_app_lora.hpp" + +#include "../ui_app_page.hpp" +#include "compat/input_keys.h" +#include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" +#include "keyboard_input.h" #include "lvgl/lvgl.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include + +#include +#include #include #include -#include -#include "keyboard_input.h" -#if __has_include() -#include -#define HAS_LINUX_GPIO_CDEV 1 -#else -#define HAS_LINUX_GPIO_CDEV 0 -#endif - -#if __has_include() && __has_include() -#include -#include -#else -extern "C" int ioctl(int fd, unsigned long request, ...); -struct spi_ioc_transfer { - unsigned long tx_buf; - unsigned long rx_buf; - uint32_t len; - uint32_t speed_hz; - uint16_t delay_usecs; - uint8_t bits_per_word; - uint8_t cs_change; - uint32_t pad; -}; -#ifndef SPI_MODE_0 -#define SPI_MODE_0 0 -#endif -#ifndef SPI_NO_CS -#define SPI_NO_CS 0x40 -#endif -#ifndef SPI_IOC_WR_MODE -#define SPI_IOC_WR_MODE 0 -#endif -#ifndef SPI_IOC_WR_BITS_PER_WORD -#define SPI_IOC_WR_BITS_PER_WORD 0 -#endif -#ifndef SPI_IOC_WR_MAX_SPEED_HZ -#define SPI_IOC_WR_MAX_SPEED_HZ 0 -#endif -#ifndef SPI_IOC_MESSAGE -#define SPI_IOC_MESSAGE(N) 0 -#endif -#endif - -#if __has_include() -#include -#if __has_include() -#include -#define APPLAUNCH_HAS_LINUX_I2C_RDWR 1 -#else -#define APPLAUNCH_HAS_LINUX_I2C_RDWR 0 -#endif -#define APPLAUNCH_HAS_LINUX_I2CDEV 1 -#else -#define APPLAUNCH_HAS_LINUX_I2CDEV 0 -#define APPLAUNCH_HAS_LINUX_I2C_RDWR 0 -#ifndef I2C_SLAVE -#define I2C_SLAVE 0x0703 -#endif -#endif - -#include "RadioLib.h" -#if __has_include() -#include "hal/RPi/PiHal.h" -#define APPLAUNCH_HAS_PIHAL 1 -#else -#define APPLAUNCH_HAS_PIHAL 0 - - - +#include +#include namespace Lora_APP { -// LoRa app entry function + void ui_app_lora_create(lv_obj_t* parent, lv_obj_t* root); void ui_app_lora_set_go_back(std::function go_back); void ui_app_lora_destroy(void); void lora_app_task(); -class PiHal : public RadioLibHal { - public: - PiHal(uint8_t spiChannel, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) - : RadioLibHal(0, 1, 0, 1, 1, 2), - _gpioDevice(gpioDevice), - _spiDevice(spiDevice), - _spiSpeed(spiSpeed), - _spiChannel(spiChannel) { - } - - protected: - uint8_t _gpioDevice; - uint8_t _spiDevice; - uint32_t _spiSpeed; - uint8_t _spiChannel; -}; -#endif - -// ============================================================ -// Hardware configuration and state -// ============================================================ -static int g_spi_fd = -1; -static bool g_lora_tx_mode = false; -static bool g_lora_selected_tx_mode = false; -static bool g_lora_tx_in_progress = false; -static bool g_lora_pending_rx_after_tx = false; -static uint64_t g_lora_last_auto_tx_ms = 0; -static char g_spi_device[64] = "/dev/spidev0.1"; -static unsigned int g_spi_speed = 1000000; -static int g_lora_sck_gpio = 11; -static int g_lora_mosi_gpio = 10; -static int g_lora_miso_gpio = 9; -static int g_lora_power_gpio = 5; -static int g_lora_nss_gpio = 7; -static bool g_lora_nss_manual = false; -static int g_lora_rst_gpio = 26; -static int g_lora_irq_gpio = 23; -static int g_lora_busy_gpio = 22; -static int g_lora_rst_fd = -1; -static int g_lora_busy_fd = -1; -static int g_lora_irq_fd = -1; -static int g_lora_nss_fd = -1; -static volatile bool g_lora_initialized = false; -static bool g_lora_irq_poll_fallback = true; -static volatile bool g_lora_rx_done = false; -static volatile bool g_lora_tx_done = false; -static uint32_t g_lora_tx_counter = 0; -static uint64_t g_lora_tx_start_ms = 0; -static uint64_t g_lora_sent_popup_until_ms = 0; -static char g_lora_last_rx[128] = {0}; -static char g_lora_last_tx[128] = "Hello from M5 LoRa-1262"; -static char g_lora_tx_input[128] = ""; -static bool g_lora_has_sent_message = false; -static float g_lora_last_rssi = 0.0f; -static float g_lora_last_snr = 0.0f; -static const char *g_lora_cfg_freq = "869.525024MHz"; -static const char *g_lora_cfg_bw = "250kHz"; -static const char *g_lora_cfg_sf = "SF7"; -static const char *g_lora_cfg_cr = "4/5"; -static const char *g_lora_cfg_sync = "0x34"; -static const char *g_lora_cfg_preamble = "20"; -static const char *g_lora_cfg_power = "10dBm"; -static const char *g_lora_cfg_tcxo = "0.0V(disabled)"; -static char g_lora_last_diag[256] = "idle"; -static char g_lora_probe_summary[256] = "probe not started"; -static char g_lora_probe_display[128] = "SPI: probing..."; -static const int g_pi4io_i2c_bus = 1; -static const int g_pi4io_sda_gpio = 2; -static const int g_pi4io_scl_gpio = 3; -static const uint8_t g_pi4io_i2c_addr = 0x43; -static bool g_pi4io_found = false; -static bool g_pi4io_initialized = false; -static char g_pi4io_status[160] = "I2C 0x43 not checked"; -static uint8_t g_pi4io_output_cache = 0x00; -static uint8_t g_pi4io_config_cache = 0xFF; -static uint8_t g_pi4io_polarity_cache = 0x00; -static int g_hat_5vout_fd = -1; -static int g_hat_5vout_offset = 5; -static char g_hat_5vout_chip[64] = ""; -static int g_hat_5vout_last_sysfs_ret = -999; -static int g_hat_5vout_last_value = -1; -static bool g_hat_5vout_last_cdev_ok = false; - -// Back callback static std::function g_go_back_home_fn; void ui_app_lora_set_go_back(std::function go_back) @@ -192,67 +35,33 @@ void ui_app_lora_set_go_back(std::function go_back) g_go_back_home_fn = go_back; } -// App state enum LoraView { LORA_VIEW_MESSAGES = 0, LORA_VIEW_INFO, LORA_VIEW_SEND, }; + static LoraView g_lora_view = LORA_VIEW_MESSAGES; -static bool g_lora_hw_ready = false; static bool g_app_active = false; +static uint32_t g_lora_sent_popup_until_ms = 0; +static char g_lora_tx_input[128] = ""; +static cp0_lora_info_t g_lora_info{}; + +static lv_obj_t *g_ui_parent = nullptr; +static lv_obj_t *g_ui_root = nullptr; +static lv_obj_t *g_title_label = nullptr; +static lv_obj_t *g_content_label = nullptr; +static lv_obj_t *g_info_pins = nullptr; +static lv_obj_t *g_info_device = nullptr; +static lv_obj_t *g_info_mode = nullptr; +static lv_obj_t *g_info_status = nullptr; +static lv_obj_t *g_info_hint = nullptr; +static lv_timer_t *g_lora_timer = nullptr; +static lv_obj_t *g_rx_bubble_bg = nullptr; +static lv_obj_t *g_rx_bubble_lbl = nullptr; +static lv_obj_t *g_tx_bubble_bg = nullptr; +static lv_obj_t *g_tx_bubble_lbl = nullptr; -// UI objects -static lv_obj_t *g_ui_parent = NULL; -static lv_obj_t *g_ui_root = NULL; -static lv_obj_t *g_title_label = NULL; -static lv_obj_t *g_content_label = NULL; -static lv_obj_t *g_info_pins = NULL; -static lv_obj_t *g_info_device = NULL; -static lv_obj_t *g_info_mode = NULL; -static lv_obj_t *g_info_status = NULL; -static lv_obj_t *g_info_hint = NULL; -static lv_timer_t *g_lora_timer = NULL; -static lv_obj_t *g_rx_bubble_bg = NULL; -static lv_obj_t *g_rx_bubble_lbl = NULL; -static lv_obj_t *g_tx_bubble_bg = NULL; -static lv_obj_t *g_tx_bubble_lbl = NULL; - -// Forward declarations -static uint64_t get_monotonic_ms(void); -static bool lora_spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len); -static int gpio_set_value(int gpio, int value); -#if HAS_LINUX_GPIO_CDEV -static bool gpio_open_output_line(const char *chip_path, int offset, int value, int *line_fd); -static bool gpio_set_output_line_value(int line_fd, int value); -#endif -static int gpio_init_output_any(const char *chip_env_name, const char *offset_env_name, int gpio, int value, int *line_fd, const char *line_name); -static int gpio_init_input_any(const char *chip_env_name, const char *offset_env_name, int gpio, int *line_fd, const char *line_name); -static int gpio_get_value_any(int gpio, int line_fd); -static int gpio_set_value_any(int gpio, int line_fd, int value); -static size_t collect_spi_candidates(char out[][64], size_t max_count, const char *preferred); -static void resolve_lora_spi_device(void); -static bool probe_lora_spi_device(void); -static bool hat_5vout_enable(void); -static bool hat_5vout_prepare_line(void); -static void lora_update_power_debug(const char *stage, int sysfs_ret, int gpio_value, bool cdev_ok); -static bool pi4io_scan_and_init_before_lora(void); -static bool pi4io_open_bus(int *fd); -static bool pi4io_select_device(int fd); -static bool pi4io_write_reg(int fd, uint8_t reg, uint8_t value); -static bool pi4io_probe_device(int fd); -static bool pi4io_init_device(int fd); -static void lora_apply_mode(bool tx_mode); -static void lora_start_receive_mode(void); -static void lora_send_demo_packet(void); -static void lora_service_irq_once(void); -static void lora_check_tx_fallback(void); -static void lora_set_diag_step(const char *step, int code, const char *detail); -static void lora_refresh_status(const char *prefix); -static const char *lora_radiolib_status_text(int16_t state); -static bool lora_send_text_packet(const char *payload); -static void lora_poll_irq_and_update_ui(void); -static void lora_init_hardware(void); static void lora_render_current_view(void); static void lora_render_messages_view(void); static void lora_render_info_view(void); @@ -263,1130 +72,28 @@ static bool is_lora_text_key(uint32_t key); static char lora_key_to_char(uint32_t key); static bool handle_app_key(uint32_t key); - -// ============================================================ -// GPIO / SPI / I2C low level (ported from UserDemo) -// ============================================================ - -static int write_text_file(const char *path, const char *value) -{ - int fd = open(path, O_WRONLY); - if (fd < 0) return -1; - ssize_t ret = write(fd, value, strlen(value)); - close(fd); - return ret < 0 ? -1 : 0; -} - -static int gpio_export_if_needed(int gpio) -{ - char path[64]; - snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); - if (access(path, F_OK) == 0) return 0; - char gpio_str[16]; - snprintf(gpio_str, sizeof(gpio_str), "%d", gpio); - if (write_text_file("/sys/class/gpio/export", gpio_str) < 0 && errno != EBUSY) { - return -1; - } - usleep(100000); - return 0; -} - -static int gpio_set_direction(int gpio, const char *direction) -{ - char path[64]; - snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", gpio); - return write_text_file(path, direction); -} - -static int gpio_init_input(int gpio) -{ - return gpio_export_if_needed(gpio) < 0 || gpio_set_direction(gpio, "in") < 0 ? -1 : 0; -} - -static int gpio_open_value_fd(int gpio) -{ - char path[64]; - snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); - return open(path, O_RDONLY | O_NONBLOCK); -} - -static int gpio_init_input_irq_sysfs(int gpio, int *line_fd) -{ - if (line_fd == NULL) return -1; - if (gpio_init_input(gpio) < 0) return -1; - char edge_path[64]; - snprintf(edge_path, sizeof(edge_path), "/sys/class/gpio/gpio%d/edge", gpio); - if (write_text_file(edge_path, "rising") < 0) return -1; - int fd = gpio_open_value_fd(gpio); - if (fd < 0) return -1; - char dummy = 0; - lseek(fd, 0, SEEK_SET); - (void)read(fd, &dummy, 1); - *line_fd = fd; - return 0; -} - -static int gpio_init_output(int gpio, int value) -{ - if (gpio_export_if_needed(gpio) < 0) return -1; - if (value) { - if (gpio_set_direction(gpio, "high") == 0) return 0; - } else { - if (gpio_set_direction(gpio, "low") == 0) return 0; - } - if (gpio_set_direction(gpio, "out") < 0) return -1; - return gpio_set_value(gpio, value); -} - -static int gpio_get_value(int gpio) -{ - char path[64]; - char value = '0'; - snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); - int fd = open(path, O_RDONLY); - if (fd < 0) return -1; - ssize_t ret = read(fd, &value, 1); - close(fd); - if (ret <= 0) return -1; - return value == '0' ? 0 : 1; -} - -static int gpio_set_value(int gpio, int value) -{ - char path[64]; - snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", gpio); - return write_text_file(path, value ? "1" : "0"); -} - -#if HAS_LINUX_GPIO_CDEV -static bool gpio_open_input_line(const char *chip_path, int offset, int *line_fd) -{ - if (chip_path == NULL || line_fd == NULL) return false; - int chip_fd = open(chip_path, O_RDONLY); - if (chip_fd < 0) return false; - struct gpiohandle_request req; - memset(&req, 0, sizeof(req)); - req.lines = 1; - req.lineoffsets[0] = (uint32_t)offset; - req.flags = GPIOHANDLE_REQUEST_INPUT; - snprintf(req.consumer_label, sizeof(req.consumer_label), "applaunch-lora-in"); - if (ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) { - close(chip_fd); - return false; - } - close(chip_fd); - *line_fd = req.fd; - return true; -} - -static bool gpio_get_input_line_value(int line_fd, int *value) -{ - if (line_fd < 0 || value == NULL) return false; - struct gpiohandle_data data; - memset(&data, 0, sizeof(data)); - if (ioctl(line_fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data) < 0) return false; - *value = data.values[0] ? 1 : 0; - return true; -} - -static bool gpio_open_input_event_line(const char *chip_path, int offset, int *line_fd) -{ - if (chip_path == NULL || line_fd == NULL) return false; - int chip_fd = open(chip_path, O_RDONLY); - if (chip_fd < 0) return false; - struct gpioevent_request req; - memset(&req, 0, sizeof(req)); - req.lineoffset = (uint32_t)offset; - req.handleflags = GPIOHANDLE_REQUEST_INPUT; - req.eventflags = GPIOEVENT_REQUEST_RISING_EDGE; - snprintf(req.consumer_label, sizeof(req.consumer_label), "applaunch-lora-irq"); - if (ioctl(chip_fd, GPIO_GET_LINEEVENT_IOCTL, &req) < 0) { - close(chip_fd); - return false; - } - close(chip_fd); - *line_fd = req.fd; - (void)fcntl(*line_fd, F_SETFL, fcntl(*line_fd, F_GETFL, 0) | O_NONBLOCK); - return true; -} - -static bool gpio_line_name_matches(const char *name) -{ - static const char *candidates[] = { - "G5_HAT_5VOUT_EN", "HAT_5VOUT_EN", "PG5", "G5", - }; - if (name == NULL || name[0] == '\0') return false; - for (size_t i = 0; i < sizeof(candidates)/sizeof(candidates[0]); ++i) { - if (strcmp(name, candidates[i]) == 0) return true; - } - return false; -} - -static bool gpio_find_named_line(char *chip_path, size_t chip_path_size, int *offset) -{ - if (chip_path == NULL || chip_path_size == 0 || offset == NULL) return false; - for (int chip_index = 0; chip_index < 8; ++chip_index) { - char path[64]; - snprintf(path, sizeof(path), "/dev/gpiochip%d", chip_index); - int chip_fd = open(path, O_RDONLY); - if (chip_fd < 0) continue; - struct gpiochip_info chip_info; - memset(&chip_info, 0, sizeof(chip_info)); - if (ioctl(chip_fd, GPIO_GET_CHIPINFO_IOCTL, &chip_info) < 0) { - close(chip_fd); continue; - } - for (int line = 0; line < (int)chip_info.lines; ++line) { - struct gpioline_info line_info; - memset(&line_info, 0, sizeof(line_info)); - line_info.line_offset = line; - if (ioctl(chip_fd, GPIO_GET_LINEINFO_IOCTL, &line_info) < 0) continue; - if (gpio_line_name_matches(line_info.name) || gpio_line_name_matches(line_info.consumer)) { - snprintf(chip_path, chip_path_size, "%s", path); - *offset = line; - close(chip_fd); - return true; - } - } - close(chip_fd); - } - return false; -} - -static bool gpio_open_output_line(const char *chip_path, int offset, int value, int *line_fd) -{ - if (chip_path == NULL || line_fd == NULL) return false; - int chip_fd = open(chip_path, O_RDONLY); - if (chip_fd < 0) return false; - struct gpiohandle_request req; - memset(&req, 0, sizeof(req)); - req.lines = 1; - req.lineoffsets[0] = (uint32_t)offset; - req.flags = GPIOHANDLE_REQUEST_OUTPUT; - req.default_values[0] = (uint8_t)(value ? 1 : 0); - snprintf(req.consumer_label, sizeof(req.consumer_label), "applaunch-lora-5v"); - if (ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) { - close(chip_fd); - return false; - } - close(chip_fd); - *line_fd = req.fd; - return true; -} - -static bool gpio_set_output_line_value(int line_fd, int value) -{ - if (line_fd < 0) return false; - struct gpiohandle_data data; - memset(&data, 0, sizeof(data)); - data.values[0] = (uint8_t)(value ? 1 : 0); - return ioctl(line_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data) == 0; -} -#endif - -static int gpio_init_output_any(const char *chip_env_name, const char *offset_env_name, int gpio, int value, int *line_fd, const char *line_name) -{ - if (line_fd && *line_fd >= 0) return 0; -#if HAS_LINUX_GPIO_CDEV - const char *chip_env = getenv(chip_env_name); - const char *offset_env = getenv(offset_env_name); - char chip_path[64] = "/dev/gpiochip0"; - int offset = gpio; - if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); - if (offset_env && offset_env[0]) offset = atoi(offset_env); - if (line_fd && gpio_open_output_line(chip_path, offset, value, line_fd)) { - SLOGI("LoRa GPIO %s via cdev: %s[%d]=%d", line_name ? line_name : "out", chip_path, offset, value); - return 0; - } -#endif - if (gpio_init_output(gpio, value) == 0) return 0; - SLOGI("LoRa GPIO %s init failed: gpio=%d errno=%d", line_name ? line_name : "out", gpio, errno); - return -1; -} - -static int gpio_init_input_any(const char *chip_env_name, const char *offset_env_name, int gpio, int *line_fd, const char *line_name) -{ - if (line_fd && *line_fd >= 0) return 0; -#if HAS_LINUX_GPIO_CDEV - const char *chip_env = getenv(chip_env_name); - const char *offset_env = getenv(offset_env_name); - char chip_path[64] = "/dev/gpiochip0"; - int offset = gpio; - if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); - if (offset_env && offset_env[0]) offset = atoi(offset_env); - if (line_fd && gpio_open_input_line(chip_path, offset, line_fd)) { - SLOGI("LoRa GPIO %s via cdev: %s[%d]", line_name ? line_name : "in", chip_path, offset); - return 0; - } -#endif - if (gpio_init_input(gpio) == 0) return 0; - SLOGI("LoRa GPIO %s input init failed: gpio=%d errno=%d", line_name ? line_name : "in", gpio, errno); - return -1; -} - -static int gpio_init_input_irq_any(const char *chip_env_name, const char *offset_env_name, int gpio, int *line_fd, const char *line_name) -{ - if (line_fd && *line_fd >= 0) return 0; -#if HAS_LINUX_GPIO_CDEV - const char *chip_env = getenv(chip_env_name); - const char *offset_env = getenv(offset_env_name); - char chip_path[64] = "/dev/gpiochip0"; - int offset = gpio; - if (chip_env && chip_env[0]) snprintf(chip_path, sizeof(chip_path), "%s", chip_env); - if (offset_env && offset_env[0]) offset = atoi(offset_env); - if (line_fd && gpio_open_input_event_line(chip_path, offset, line_fd)) { - SLOGI("LoRa GPIO %s irq-event via cdev: %s[%d]", line_name ? line_name : "irq", chip_path, offset); - return 0; - } -#endif - if (line_fd && gpio_init_input_irq_sysfs(gpio, line_fd) == 0) { - SLOGI("LoRa GPIO %s irq-event via sysfs: gpio%d rising", line_name ? line_name : "irq", gpio); - return 0; - } - return -1; -} - -static int gpio_get_value_any(int gpio, int line_fd) -{ -#if HAS_LINUX_GPIO_CDEV - int value = 0; - if (line_fd >= 0 && gpio_get_input_line_value(line_fd, &value)) return value; -#endif - return gpio_get_value(gpio); -} - -static int gpio_set_value_any(int gpio, int line_fd, int value) -{ -#if HAS_LINUX_GPIO_CDEV - if (line_fd >= 0) return gpio_set_output_line_value(line_fd, value) ? 0 : -1; -#endif - return gpio_set_value(gpio, value); -} - -static size_t collect_spi_candidates(char out[][64], size_t max_count, const char *preferred) -{ - if (out == NULL || max_count == 0) return 0; - size_t count = 0; - auto append_candidate = [&](const char *path) { - if (path == NULL || path[0] == '\0') return; - for (size_t i = 0; i < count; ++i) if (strcmp(out[i], path) == 0) return; - if (count < max_count) { snprintf(out[count], 64, "%s", path); ++count; } - }; - append_candidate(preferred); - append_candidate("/dev/spidev0.1"); - append_candidate("/dev/spidev0.0"); - DIR *dir = opendir("/dev"); - if (dir != NULL) { - struct dirent *entry = NULL; - while ((entry = readdir(dir)) != NULL) { - if (strncmp(entry->d_name, "spidev", 6) != 0) continue; - char full_path[64]; - snprintf(full_path, sizeof(full_path), "/dev/%s", entry->d_name); - append_candidate(full_path); - } - closedir(dir); - } - const char *fallbacks[] = { - "/dev/spidev0.1", "/dev/spidev0.0", "/dev/spidev1.0", "/dev/spidev1.1", - "/dev/spidev2.0", "/dev/spidev2.1", "/dev/spidev3.0", "/dev/spidev3.1", - "/dev/spidev4.0", "/dev/spidev4.1", - }; - for (size_t i = 0; i < sizeof(fallbacks)/sizeof(fallbacks[0]); ++i) append_candidate(fallbacks[i]); - return count; -} - -static void lora_update_power_debug(const char *stage, int sysfs_ret, int gpio_value, bool cdev_ok) -{ - char text[256]; - const char *chip_text = g_hat_5vout_chip[0] ? g_hat_5vout_chip : "sysfs"; - const char *value_text = gpio_value < 0 ? "read_fail" : (gpio_value ? "HIGH" : "LOW"); - snprintf(text, sizeof(text), "5VDBG %s cdev=%s chip=%s[%d] sysfs_ret=%d gpio5=%s", - stage ? stage : "?", cdev_ok ? "ok" : "fail", chip_text, g_hat_5vout_offset, sysfs_ret, value_text); - SLOGI("%s", text); -} - -static bool hat_5vout_prepare_line(void) -{ -#if HAS_LINUX_GPIO_CDEV - const char *chip_env = getenv("HAT_5VOUT_CHIP"); - const char *offset_env = getenv("HAT_5VOUT_OFFSET"); - if (chip_env && chip_env[0]) { - snprintf(g_hat_5vout_chip, sizeof(g_hat_5vout_chip), "%s", chip_env); - g_hat_5vout_offset = offset_env && offset_env[0] ? atoi(offset_env) : 5; - } else if (!gpio_find_named_line(g_hat_5vout_chip, sizeof(g_hat_5vout_chip), &g_hat_5vout_offset)) { - snprintf(g_hat_5vout_chip, sizeof(g_hat_5vout_chip), "/dev/gpiochip0"); - g_hat_5vout_offset = 5; - } - if (g_hat_5vout_fd >= 0) { g_hat_5vout_last_cdev_ok = true; return true; } - if (gpio_open_output_line(g_hat_5vout_chip, g_hat_5vout_offset, 1, &g_hat_5vout_fd)) { - g_hat_5vout_last_cdev_ok = true; return true; - } - g_hat_5vout_last_cdev_ok = false; -#endif - return false; -} - -static bool lora_spi_transfer(const uint8_t *tx, uint8_t *rx, size_t len) -{ - if (g_spi_fd < 0) return false; - struct spi_ioc_transfer tr; - memset(&tr, 0, sizeof(tr)); - tr.tx_buf = (unsigned long)tx; - tr.rx_buf = (unsigned long)rx; - tr.len = (uint32_t)len; - tr.speed_hz = g_spi_speed; - tr.bits_per_word = 8; - int ret = ioctl(g_spi_fd, SPI_IOC_MESSAGE(1), &tr); - return ret >= 0; -} - -static bool lora_open_runtime_spi(void) -{ - if (g_spi_fd >= 0) return true; - g_spi_fd = open(g_spi_device, O_RDWR); - if (g_spi_fd < 0) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "runtime SPI open failed on %s", g_spi_device); - return false; - } - uint8_t mode = (uint8_t)SPI_MODE_0; - uint8_t bits = 8; - if (ioctl(g_spi_fd, SPI_IOC_WR_MODE, &mode) < 0 || - ioctl(g_spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 || - ioctl(g_spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &g_spi_speed) < 0) { - close(g_spi_fd); g_spi_fd = -1; - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "runtime SPI config failed on %s", g_spi_device); - return false; - } - return true; -} - -static bool sx1262_wait_while_busy(unsigned int timeout_ms) -{ - const unsigned int sleep_us = 1000; - unsigned int waited_ms = 0; - while (waited_ms < timeout_ms) { - int busy = gpio_get_value_any(g_lora_busy_gpio, g_lora_busy_fd); - if (busy < 0) return false; - if (busy == 0) return true; - usleep(sleep_us); - waited_ms += 1; - } - return false; -} - -static bool sx1262_reset(void) -{ - if (gpio_set_value_any(g_lora_rst_gpio, g_lora_rst_fd, 0) < 0) return false; - usleep(20000); - if (gpio_set_value_any(g_lora_rst_gpio, g_lora_rst_fd, 1) < 0) return false; - usleep(10000); - return sx1262_wait_while_busy(200); -} - -static bool sx1262_get_status_raw(uint8_t *status) -{ - uint8_t tx[2] = {0xC0, 0x00}; - uint8_t rx[2] = {0}; - if (!status) return false; - if (!lora_spi_transfer(tx, rx, sizeof(tx))) return false; - *status = rx[1]; - return true; -} - -static bool hat_5vout_enable(void) -{ - bool cdev_ok = false; -#if HAS_LINUX_GPIO_CDEV - if (hat_5vout_prepare_line()) { - if (gpio_set_output_line_value(g_hat_5vout_fd, 0)) { - cdev_ok = true; - g_hat_5vout_last_sysfs_ret = 0; - g_hat_5vout_last_value = gpio_get_value(g_lora_power_gpio); - lora_update_power_debug("cdev_set", g_hat_5vout_last_sysfs_ret, g_hat_5vout_last_value, cdev_ok); - usleep(50000); - return true; - } - } -#endif - g_hat_5vout_last_sysfs_ret = gpio_init_output(g_lora_power_gpio, 0); - g_hat_5vout_last_value = gpio_get_value(g_lora_power_gpio); - lora_update_power_debug("sysfs_set", g_hat_5vout_last_sysfs_ret, g_hat_5vout_last_value, cdev_ok); - if (g_hat_5vout_last_sysfs_ret == 0) { usleep(50000); return true; } - lora_update_power_debug("enable_fail", g_hat_5vout_last_sysfs_ret, g_hat_5vout_last_value, cdev_ok); - return false; -} - -static bool pi4io_open_bus(int *fd) -{ -#if !APPLAUNCH_HAS_LINUX_I2CDEV - if (fd) *fd = -1; - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C dev header missing, cannot access 0x%02X", g_pi4io_i2c_addr); - return false; -#else - if (fd == NULL) { snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C fd pointer invalid"); return false; } - char dev_path[64]; - snprintf(dev_path, sizeof(dev_path), "/dev/i2c-%d", g_pi4io_i2c_bus); - *fd = open(dev_path, O_RDWR); - if (*fd < 0) { - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "open %s failed, SDA:%d SCL:%d errno=%d", - dev_path, g_pi4io_sda_gpio, g_pi4io_scl_gpio, errno); - return false; - } - return true; -#endif -} - -static bool pi4io_select_device(int fd) -{ - if (fd < 0) { snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C fd invalid for 0x%02X", g_pi4io_i2c_addr); return false; } - if (ioctl(fd, I2C_SLAVE, g_pi4io_i2c_addr) < 0) { - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "select 0x%02X failed on /dev/i2c-%d errno=%d", - g_pi4io_i2c_addr, g_pi4io_i2c_bus, errno); - return false; - } - return true; -} - -static bool pi4io_write_reg(int fd, uint8_t reg, uint8_t value) -{ - uint8_t buf[2] = {reg, value}; - return write(fd, buf, sizeof(buf)) == (ssize_t)sizeof(buf); -} - -static bool pi4io_probe_device(int fd) -{ - uint8_t reg = 0x00; - if (write(fd, ®, 1) != 1) { - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C 0x%02X not found on /dev/i2c-%d (SDA:%d SCL:%d)", - g_pi4io_i2c_addr, g_pi4io_i2c_bus, g_pi4io_sda_gpio, g_pi4io_scl_gpio); - return false; - } - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C 0x%02X found on /dev/i2c-%d (SDA:%d SCL:%d)", - g_pi4io_i2c_addr, g_pi4io_i2c_bus, g_pi4io_sda_gpio, g_pi4io_scl_gpio); - return true; -} - -static bool pi4io_init_device(int fd) -{ - if (fd < 0) { snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO init invalid fd for 0x%02X", g_pi4io_i2c_addr); return false; } - g_pi4io_polarity_cache = 0x00; - g_pi4io_output_cache = 0x01; - g_pi4io_config_cache = 0xFE; - errno = 0; - if (!pi4io_write_reg(fd, 0x02, g_pi4io_polarity_cache)) { - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO write POL failed at 0x%02X errno=%d", g_pi4io_i2c_addr, errno); - return false; - } - errno = 0; - if (!pi4io_write_reg(fd, 0x01, g_pi4io_output_cache)) { - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO write OUT failed at 0x%02X errno=%d", g_pi4io_i2c_addr, errno); - return false; - } - errno = 0; - if (!pi4io_write_reg(fd, 0x03, g_pi4io_config_cache)) { - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO write CFG failed at 0x%02X errno=%d", g_pi4io_i2c_addr, errno); - return false; - } - snprintf(g_pi4io_status, sizeof(g_pi4io_status), "I2C IO init ok OUT=0x%02X POL=0x%02X CFG=0x%02X P0=HIGH", - g_pi4io_output_cache, g_pi4io_polarity_cache, g_pi4io_config_cache); - return true; -} - -static bool pi4io_scan_and_init_before_lora(void) -{ - int fd = -1; - bool ok = false; - g_pi4io_found = false; - g_pi4io_initialized = false; - if (!pi4io_open_bus(&fd)) return false; - do { - if (!pi4io_select_device(fd)) break; - if (!pi4io_probe_device(fd)) break; - g_pi4io_found = true; - if (!pi4io_init_device(fd)) break; - g_pi4io_initialized = true; - ok = true; - } while (0); - if (fd >= 0) close(fd); - return ok; -} - -static bool probe_lora_spi_device(void) -{ - const char *spi_env = getenv("LORA_SPI_DEV"); - char candidates[16][64] = {{0}}; - const size_t candidate_count = collect_spi_candidates(candidates, 16, spi_env); - char summary[256] = {0}; - - if (access("/dev", F_OK) != 0) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "Linux /dev not available; LoRa SPI HAL requires Raspberry Pi Linux runtime"); - snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "no /dev directory visible"); - snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "SPI: /dev unavailable"); - return false; - } - if (candidate_count == 0) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "no /dev/spidev* found; enable SPI on Raspberry Pi OS"); - snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "probe aborted: no spidev nodes"); - snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "SPI: no spidev found"); - return false; - } - SLOGI("LoRa SPI probe policy: prefer SPI0 only, CE1 then CE0"); - summary[0] = '\0'; - for (size_t i = 0; i < candidate_count; ++i) { - const char *dev = candidates[i]; - if (spi_env && spi_env[0] && strcmp(spi_env, dev) == 0) continue; - if (summary[0]) strncat(summary, ", ", sizeof(summary) - strlen(summary) - 1); - strncat(summary, dev, sizeof(summary) - strlen(summary) - 1); - } - if (spi_env && spi_env[0]) { - snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "probe order: %s%s%s", spi_env, summary[0] ? ", " : "", summary); - snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "Try: %s -> 0.1 -> 0.0", spi_env); - } else { - snprintf(g_lora_probe_summary, sizeof(g_lora_probe_summary), "probe order: %s", summary); - snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "Try: /dev/spidev0.1 -> /dev/spidev0.0"); - } - - auto try_probe = [](const char *dev) -> bool { - if (dev == NULL || dev[0] == '\0' || access(dev, F_OK) != 0) return false; - snprintf(g_spi_device, sizeof(g_spi_device), "%s", dev); - g_lora_nss_manual = false; - const char *cs_name = strstr(g_spi_device, "spidev0.1") ? "SPI0-CE1" : (strstr(g_spi_device, "spidev0.0") ? "SPI0-CE0" : "non-SPI0"); - SLOGI("LoRa probe: trying %s [%s] (cs=hw-auto)", g_spi_device, cs_name); - g_lora_initialized = false; - if (g_spi_fd >= 0) { close(g_spi_fd); g_spi_fd = -1; } - if (gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", g_lora_rst_gpio, 1, &g_lora_rst_fd, "RST") < 0) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RST gpio init failed on %s", g_spi_device); - return false; - } - if (!sx1262_reset()) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RST/BUSY handshake failed on %s", g_spi_device); - return false; - } - uint8_t status = 0; - g_spi_fd = open(g_spi_device, O_RDWR); - if (g_spi_fd < 0) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "SPI open failed on %s", g_spi_device); - return false; - } - uint8_t mode = (uint8_t)SPI_MODE_0; - uint8_t bits = 8; - if (ioctl(g_spi_fd, SPI_IOC_WR_MODE, &mode) < 0 || - ioctl(g_spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 || - ioctl(g_spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &g_spi_speed) < 0) { - close(g_spi_fd); g_spi_fd = -1; - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "SPI config failed on %s", g_spi_device); - return false; - } - bool ok = sx1262_get_status_raw(&status); - close(g_spi_fd); g_spi_fd = -1; - if (!ok) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "status read failed on %s", g_spi_device); - return false; - } - SLOGI("LoRa probe: %s [%s] (cs=hw-auto) status=0x%02X", g_spi_device, cs_name, status); - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "probe ok on %s[%s] cs=hw-auto status=0x%02X", g_spi_device, cs_name, status); - snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "FOUND: %s (%s)", g_spi_device, cs_name); - return true; - }; - - if (spi_env && spi_env[0] && try_probe(spi_env)) return true; - for (size_t i = 0; i < candidate_count; ++i) { - if (try_probe(candidates[i])) return true; - } - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "all SPI buses probed, no SX1262 response (%s)", g_lora_probe_summary); - snprintf(g_lora_probe_display, sizeof(g_lora_probe_display), "NOT FOUND: tried 0.1 and 0.0"); - return false; -} - -static void resolve_lora_spi_device(void) -{ - const char *spi_env = getenv("LORA_SPI_DEV"); - char candidates[16][64] = {{0}}; - const size_t candidate_count = collect_spi_candidates(candidates, 16, spi_env); - if (spi_env != NULL && spi_env[0] != '\0' && access(spi_env, F_OK) == 0) { - snprintf(g_spi_device, sizeof(g_spi_device), "%s", spi_env); return; - } - for (size_t i = 0; i < candidate_count; ++i) { - if (access(candidates[i], F_OK) == 0) { - snprintf(g_spi_device, sizeof(g_spi_device), "%s", candidates[i]); return; - } - } - snprintf(g_spi_device, sizeof(g_spi_device), "%s", spi_env && spi_env[0] ? spi_env : "/dev/spidev0.1"); -} - - -// ============================================================ -// RadioLib HAL / Module / TX/RX logic -// ============================================================ - -class LinuxRadioLibHal : public PiHal { - public: - LinuxRadioLibHal() : PiHal(0, 2000000, 0, 0) {} - - void pinMode(uint32_t pin, uint32_t mode) override { - if (pin == RADIOLIB_NC) return; - if (mode == GpioModeOutput) { - if (pin == (uint32_t)g_lora_rst_gpio) { - (void)gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", (int)pin, 1, &g_lora_rst_fd, "RST"); - } - } else { - if (pin == (uint32_t)g_lora_busy_gpio) { - (void)gpio_init_input_any("LORA_BUSY_CHIP", "LORA_BUSY_OFFSET", (int)pin, &g_lora_busy_fd, "BUSY"); - } - } - } - - void digitalWrite(uint32_t pin, uint32_t value) override { - if (pin == RADIOLIB_NC) return; - int line_fd = -1; - if (pin == (uint32_t)g_lora_rst_gpio) line_fd = g_lora_rst_fd; - (void)gpio_set_value_any((int)pin, line_fd, value ? 1 : 0); - } - - uint32_t digitalRead(uint32_t pin) override { - if (pin == RADIOLIB_NC) return 0; - int line_fd = -1; - if (pin == (uint32_t)g_lora_busy_gpio) line_fd = g_lora_busy_fd; - int value = gpio_get_value_any((int)pin, line_fd); - return value > 0 ? 1U : 0U; - } - - void attachInterrupt(uint32_t, void (*)(void), uint32_t) override {} - void detachInterrupt(uint32_t) override {} - void delay(RadioLibTime_t ms) override { usleep((useconds_t)(ms * 1000)); } - void delayMicroseconds(RadioLibTime_t us) override { usleep((useconds_t)us); } - RadioLibTime_t millis() override { return (RadioLibTime_t)get_monotonic_ms(); } - RadioLibTime_t micros() override { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (RadioLibTime_t)ts.tv_sec * 1000000ULL + (RadioLibTime_t)ts.tv_nsec / 1000ULL; - } - long pulseIn(uint32_t pin, uint32_t state, RadioLibTime_t timeout) override { - RadioLibTime_t start = micros(); - while (micros() - start < timeout) { - if (digitalRead(pin) == state) { - RadioLibTime_t pulse_start = micros(); - while (micros() - start < timeout && digitalRead(pin) == state) {} - return (long)(micros() - pulse_start); - } - } - return 0; - } - void spiBegin() override {} - void spiBeginTransaction() override {} - void spiTransfer(uint8_t *out, size_t len, uint8_t *in) override { - uint8_t dummy[512] = {0}; - uint8_t *tx = out ? out : dummy; - uint8_t *rx = in ? in : dummy; - if (len > sizeof(dummy)) len = sizeof(dummy); - (void)lora_spi_transfer(tx, rx, len); - } - void spiEndTransaction() override {} - void spiEnd() override {} -}; - -static LinuxRadioLibHal g_lora_radio_hal; -static Module *g_lora_radio_module = NULL; -static SX1262 *g_lora_radio = NULL; - -static uint64_t get_monotonic_ms(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL; -} - -static void lora_set_diag_step(const char *step, int code, const char *detail) -{ - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "%s%s%s | rc=%d", - step ? step : "diag", (detail && detail[0]) ? " | " : "", (detail && detail[0]) ? detail : "", code); - SLOGI("LoRa diag: %s", g_lora_last_diag); -} - -static const char *lora_radiolib_status_text(int16_t state) -{ - switch (state) { - case RADIOLIB_ERR_NONE: return "ok"; - case RADIOLIB_ERR_CHIP_NOT_FOUND: return "chip_not_found"; - case RADIOLIB_ERR_TX_TIMEOUT: return "tx_timeout"; - case RADIOLIB_ERR_RX_TIMEOUT: return "rx_timeout"; - case RADIOLIB_ERR_CRC_MISMATCH: return "crc_mismatch"; - case RADIOLIB_ERR_SPI_WRITE_FAILED: return "spi_write_failed"; - case RADIOLIB_ERR_SPI_CMD_TIMEOUT: return "spi_cmd_timeout"; - case RADIOLIB_ERR_SPI_CMD_INVALID: return "spi_cmd_invalid"; - case RADIOLIB_ERR_SPI_CMD_FAILED: return "spi_cmd_failed"; - default: return "radiolib_err"; - } -} - -static void lora_capture_device_errors(const char *stage, uint16_t irq_status) -{ - if (!g_lora_initialized || g_lora_radio == NULL) return; - SLOGI("LoRa error: %s irq=0x%04X", stage ? stage : "radio_err", irq_status); - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "%s irq=0x%04X", stage ? stage : "radio_err", irq_status); -} - -static bool lora_send_text_packet(const char *payload) -{ - if (!g_lora_initialized || g_lora_radio == NULL) { - SLOGI("LoRa TX: not initialized"); - return false; - } - if (payload == NULL || payload[0] == '\0') return false; - if (g_lora_tx_in_progress) return false; - snprintf(g_lora_last_tx, sizeof(g_lora_last_tx), "%s", payload); - g_lora_has_sent_message = true; - g_lora_tx_done = false; - g_lora_rx_done = false; - g_lora_pending_rx_after_tx = true; - g_lora_tx_mode = false; - g_lora_selected_tx_mode = false; - (void)g_lora_radio->standby(); - int16_t state = g_lora_radio->startTransmit((uint8_t *)g_lora_last_tx, strlen(g_lora_last_tx)); - if (state != RADIOLIB_ERR_NONE) { - g_lora_tx_in_progress = false; - g_lora_pending_rx_after_tx = false; - SLOGI("LoRa TX: startTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - return false; - } - g_lora_tx_in_progress = true; - g_lora_tx_start_ms = g_lora_last_auto_tx_ms = get_monotonic_ms(); - SLOGI("LoRa TX: sending '%s'", g_lora_last_tx); - return true; -} - -static void lora_send_demo_packet(void) -{ - if (!g_lora_initialized || g_lora_radio == NULL) return; - if (!g_lora_tx_mode) return; - snprintf(g_lora_last_tx, sizeof(g_lora_last_tx), "Hello from M5 LoRa-1262 #%lu", (unsigned long)g_lora_tx_counter); - g_lora_has_sent_message = true; - g_lora_pending_rx_after_tx = false; - g_lora_tx_done = false; - g_lora_rx_done = false; - int16_t state = g_lora_radio->startTransmit((uint8_t *)g_lora_last_tx, strlen(g_lora_last_tx)); - if (state != RADIOLIB_ERR_NONE) { - g_lora_tx_in_progress = false; - SLOGI("LoRa TX: demo startTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - return; - } - g_lora_tx_in_progress = true; - g_lora_tx_start_ms = g_lora_last_auto_tx_ms = get_monotonic_ms(); - SLOGI("LoRa TX: demo sending '%s'", g_lora_last_tx); - ++g_lora_tx_counter; -} - -static void lora_start_receive_mode(void) -{ - if (!g_lora_initialized || g_lora_radio == NULL) { - SLOGI("LoRa RX: startReceive skipped, not initialized"); - return; - } - if (g_lora_tx_in_progress) { - SLOGI("LoRa RX: startReceive skipped, TX in progress"); - g_lora_pending_rx_after_tx = true; - return; - } - g_lora_tx_mode = false; - g_lora_selected_tx_mode = false; - g_lora_pending_rx_after_tx = false; - SLOGI("LoRa RX: startReceive()"); - int16_t state = g_lora_radio->startReceive(); - SLOGI("LoRa RX: startReceive rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - if (state != RADIOLIB_ERR_NONE) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "startReceive rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - } -} - -static void lora_apply_mode(bool tx_mode) -{ - g_lora_selected_tx_mode = tx_mode; - if (!g_lora_initialized || g_lora_radio == NULL) { - SLOGI("LoRa mode: not initialized"); - return; - } - if (tx_mode) { - g_lora_pending_rx_after_tx = false; - g_lora_tx_mode = true; - g_lora_last_auto_tx_ms = get_monotonic_ms(); - if (g_lora_tx_in_progress) { - SLOGI("LoRa mode: TX already in progress"); - return; - } - int16_t state = g_lora_radio->standby(); - if (state == RADIOLIB_ERR_NONE) { - SLOGI("LoRa mode: TX ready"); - } else { - SLOGI("LoRa mode: set TX failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - } - } else { - if (g_lora_tx_in_progress) { - g_lora_pending_rx_after_tx = true; - SLOGI("LoRa mode: TX in progress, will RX after done"); - return; - } - g_lora_pending_rx_after_tx = false; - g_lora_tx_mode = false; - g_lora_last_auto_tx_ms = get_monotonic_ms(); - lora_start_receive_mode(); - } -} - -static void lora_service_irq_once(void) -{ - if (!g_lora_initialized || g_lora_radio == NULL) return; - - bool irq_event = false; - if (!g_lora_irq_poll_fallback && g_lora_irq_fd >= 0) { - struct pollfd pfd; - memset(&pfd, 0, sizeof(pfd)); - pfd.fd = g_lora_irq_fd; - pfd.events = POLLIN | POLLPRI; - if (poll(&pfd, 1, 0) > 0 && (pfd.revents & (POLLIN | POLLPRI))) { - irq_event = true; -#if HAS_LINUX_GPIO_CDEV - struct gpioevent_data event_data; - while (read(g_lora_irq_fd, &event_data, sizeof(event_data)) == (ssize_t)sizeof(event_data)) {} -#else - char value_buf[8]; - lseek(g_lora_irq_fd, 0, SEEK_SET); - while (read(g_lora_irq_fd, value_buf, sizeof(value_buf)) > 0) { lseek(g_lora_irq_fd, 0, SEEK_SET); break; } -#endif - } - } - - uint32_t irq_flags = g_lora_radio->getIrqFlags(); - if (irq_flags != RADIOLIB_SX126X_IRQ_NONE || irq_event) { - SLOGI("LoRa IRQ: event=%d flags=0x%08lX tx_in_progress=%d tx_mode=%d", - irq_event ? 1 : 0, (unsigned long)irq_flags, g_lora_tx_in_progress ? 1 : 0, g_lora_tx_mode ? 1 : 0); - } - if (!irq_event && irq_flags == RADIOLIB_SX126X_IRQ_NONE) return; - - if (g_lora_tx_in_progress) { - if (irq_flags & RADIOLIB_SX126X_IRQ_TX_DONE) { - int16_t state = g_lora_radio->finishTransmit(); - if (state == RADIOLIB_ERR_NONE) { - g_lora_tx_done = true; - } else { - g_lora_tx_in_progress = false; - SLOGI("LoRa TX: finishTransmit failed rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - } - } else if (irq_flags & RADIOLIB_SX126X_IRQ_TIMEOUT) { - g_lora_tx_in_progress = false; - g_lora_tx_start_ms = 0; - lora_capture_device_errors("TX irq timeout", 0); - if (g_lora_pending_rx_after_tx || !g_lora_tx_mode) lora_start_receive_mode(); - } - return; - } - - if (irq_flags & RADIOLIB_SX126X_IRQ_RX_DONE) { - uint8_t rx_buf[sizeof(g_lora_last_rx)] = {0}; - int16_t state = g_lora_radio->readData(rx_buf, sizeof(g_lora_last_rx) - 1); - SLOGI("LoRa RX: readData rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - if (state == RADIOLIB_ERR_NONE) { - memcpy(g_lora_last_rx, rx_buf, sizeof(g_lora_last_rx)); - g_lora_last_rx[sizeof(g_lora_last_rx) - 1] = '\0'; - g_lora_last_rssi = g_lora_radio->getRSSI(); - g_lora_last_snr = g_lora_radio->getSNR(); - g_lora_rx_done = true; - SLOGI("LoRa RX OK: '%s' RSSI=%.1f SNR=%.1f", g_lora_last_rx, g_lora_last_rssi, g_lora_last_snr); - } else if (state != RADIOLIB_ERR_CRC_MISMATCH) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "readData rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - } - if (!g_lora_tx_mode) lora_start_receive_mode(); - } else if (irq_flags & (RADIOLIB_SX126X_IRQ_CRC_ERR | RADIOLIB_SX126X_IRQ_HEADER_ERR)) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RX crc/header error irq=0x%04lX", (unsigned long)irq_flags); - SLOGI("LoRa RX error: %s", g_lora_last_diag); - if (!g_lora_tx_mode) lora_start_receive_mode(); - } else if (irq_flags & RADIOLIB_SX126X_IRQ_TIMEOUT) { - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RX timeout irq=0x%04lX", (unsigned long)irq_flags); - SLOGI("LoRa RX timeout: %s", g_lora_last_diag); - } -} - -static void lora_check_tx_fallback(void) +static const char *safe_text(const char *text, const char *fallback = "") { - if (!g_lora_initialized || !g_lora_tx_in_progress || g_lora_radio == NULL) return; - uint64_t now_ms = get_monotonic_ms(); - if (g_lora_tx_start_ms != 0 && now_ms - g_lora_tx_start_ms >= 4000ULL) { - g_lora_tx_in_progress = false; - g_lora_tx_start_ms = 0; - g_lora_last_auto_tx_ms = now_ms; - lora_capture_device_errors("TX timeout", 0); - (void)g_lora_radio->standby(); - if (g_lora_pending_rx_after_tx || !g_lora_tx_mode) lora_start_receive_mode(); - } + return text && text[0] ? text : fallback; } -static void lora_poll_irq_and_update_ui(void) +static int lora_api_call(const std::list& args, cp0_lora_info_t *info = nullptr) { - if (!g_lora_initialized) return; - lora_service_irq_once(); - lora_check_tx_fallback(); - - if (g_lora_tx_done) { - g_lora_tx_done = false; - g_lora_tx_in_progress = false; - g_lora_tx_start_ms = 0; - if (g_lora_pending_rx_after_tx || !g_lora_tx_mode) { - lora_start_receive_mode(); - } - } - - if (g_lora_rx_done) { - g_lora_rx_done = false; - if (g_app_active) { - g_lora_view = LORA_VIEW_MESSAGES; - g_lora_sent_popup_until_ms = get_monotonic_ms() + 2000ULL; - lora_render_current_view(); - } - } - - if (g_app_active && g_lora_sent_popup_until_ms != 0 && get_monotonic_ms() >= g_lora_sent_popup_until_ms) { - g_lora_sent_popup_until_ms = 0; - g_lora_view = LORA_VIEW_MESSAGES; - lora_render_current_view(); - } - - if (g_lora_initialized && g_lora_tx_mode && !g_lora_tx_in_progress) { - uint64_t now_ms = get_monotonic_ms(); - if (now_ms - g_lora_last_auto_tx_ms >= 2000ULL) { - lora_send_demo_packet(); + int result = -1; + cp0_signal_lora_api(args, [&](int code, std::string data) { + result = code; + if (info && data.size() == sizeof(*info)) { + std::memcpy(info, data.data(), sizeof(*info)); } - } + }); + return result; } - -// ============================================================ -// Hardware initialization -// ============================================================ - -static void lora_init_hardware(void) +static void refresh_lora_info(bool poll) { - delete g_lora_radio; g_lora_radio = NULL; - delete g_lora_radio_module; g_lora_radio_module = NULL; - - lora_set_diag_step("i2c_scan", 0, "scan 0x43 before LoRa init"); - if (pi4io_scan_and_init_before_lora()) { - lora_set_diag_step("i2c_scan", 0, g_pi4io_status); - } else { - lora_set_diag_step("i2c_scan", 1, g_pi4io_status); - } - - lora_set_diag_step("power_enable", 0, "start"); - if (!hat_5vout_enable()) { - SLOGI("Status: GPIO5 low set failed"); - lora_set_diag_step("power_enable", 1, "GPIO5 low set failed"); - } - usleep(100000); - - lora_set_diag_step("reset_gpio_init", 0, "prepare rst pin"); - if (gpio_init_output_any("LORA_RST_CHIP", "LORA_RST_OFFSET", g_lora_rst_gpio, 1, &g_lora_rst_fd, "RST") < 0) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("reset_gpio_init", 1, "rst gpio init failed"); - return; - } - - if (gpio_init_input_any("LORA_BUSY_CHIP", "LORA_BUSY_OFFSET", g_lora_busy_gpio, &g_lora_busy_fd, "BUSY") < 0) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("busy_gpio_init", 1, "busy gpio init failed"); - return; - } - - lora_set_diag_step("hard_reset", 0, "toggle rst before probe"); - if (!sx1262_reset()) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("hard_reset", 1, "rst/busy handshake failed"); - return; - } - - lora_set_diag_step("resolve_spi", 0, "detect device"); - resolve_lora_spi_device(); - - if (!probe_lora_spi_device()) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("probe_spi", 1, g_lora_last_diag); - return; - } - - lora_set_diag_step("pre_begin_prepare", 0, "reset again before RadioLib begin"); - if (!sx1262_reset()) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("pre_begin_prepare", 1, "rst/busy handshake failed before RadioLib begin"); - return; - } - - lora_set_diag_step("prepare_irq", 0, "init irq pin"); - if (gpio_init_input_irq_any("LORA_IRQ_CHIP", "LORA_IRQ_OFFSET", g_lora_irq_gpio, &g_lora_irq_fd, "IRQ") < 0) { - g_lora_irq_poll_fallback = true; - lora_set_diag_step("prepare_irq", 1, "irq gpio init failed, fallback=poll"); - } else { - g_lora_irq_poll_fallback = false; - lora_set_diag_step("prepare_irq", 0, "irq gpio ok"); - } - - lora_set_diag_step("runtime_spi", 0, "open SPI for RadioLib runtime"); - if (!lora_open_runtime_spi()) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("runtime_spi", 1, g_lora_last_diag); - return; - } - - lora_set_diag_step("radiolib_setup", 0, "create module"); - g_lora_nss_manual = false; - g_lora_radio_module = new Module(&g_lora_radio_hal, RADIOLIB_NC, - (uint32_t)g_lora_irq_gpio, (uint32_t)g_lora_rst_gpio, (uint32_t)g_lora_busy_gpio); - g_lora_radio = new SX1262(g_lora_radio_module); - - if (g_lora_radio_module == NULL || g_lora_radio == NULL) { - g_lora_initialized = false; g_lora_hw_ready = false; - lora_set_diag_step("radiolib_setup", 1, "allocation failed"); - return; - } - - lora_set_diag_step("radiolib_begin", 0, "configure sx1262 via RadioLib"); - int16_t state = g_lora_radio->begin( - 868.0f, // frequency MHz - 125.0f, // bandwidth kHz - 12, // spreading factor - 5, // coding rate 4/5 - 0x34, // sync word - 22, // output power dBm - 20, // preamble length - 3.0f, // TCXO voltage - false - ); - - if (state != RADIOLIB_ERR_NONE) { - g_lora_initialized = false; g_lora_hw_ready = false; - snprintf(g_lora_last_diag, sizeof(g_lora_last_diag), "RadioLib begin rc=%d(%s)", (int)state, lora_radiolib_status_text(state)); - SLOGI("LoRa init failed: rc=%d (%s)", (int)state, lora_radiolib_status_text(state)); - lora_set_diag_step("radiolib_begin", state, g_lora_last_diag); - return; - } - - (void)g_lora_radio->setCurrentLimit(140); - (void)g_lora_radio->setDio2AsRfSwitch(true); - - g_lora_initialized = true; - g_lora_hw_ready = true; - g_lora_tx_mode = false; - g_lora_selected_tx_mode = false; - g_lora_tx_in_progress = false; - g_lora_pending_rx_after_tx = false; - g_lora_tx_start_ms = 0; - g_lora_last_auto_tx_ms = get_monotonic_ms(); - - lora_set_diag_step("ready", 0, "LoRa init finished"); - SLOGI("LoRa: init done, auto enter RX"); - lora_start_receive_mode(); + (void)lora_api_call({poll ? "Poll" : "Info"}, &g_lora_info); } - -// ============================================================ -// UI rendering (adapted for the APPLaunch 320x150 container) -// ============================================================ - static void lora_ui_clear(void) { if (g_title_label) lv_obj_add_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); @@ -1461,20 +168,21 @@ static void lora_render_boot_diag(void) if (g_content_label) { lv_obj_clear_flag(g_content_label, LV_OBJ_FLAG_HIDDEN); char text[256]; - snprintf(text, sizeof(text), "SPI:%s RST:%d BUSY:%d IRQ:%d\n%s\n%s", - g_spi_device, g_lora_rst_gpio, g_lora_busy_gpio, g_lora_irq_gpio, - g_pi4io_status, g_lora_probe_summary); + snprintf(text, sizeof(text), "SPI:%s\n%s\n%s", + safe_text(g_lora_info.spi_device, "n/a"), + safe_text(g_lora_info.pi4io_status, "I2C status unavailable"), + safe_text(g_lora_info.probe_summary, "probe not started")); lv_label_set_text(g_content_label, text); lv_obj_set_style_text_color(g_content_label, lv_color_hex(0xFF4D4F), LV_PART_MAIN | LV_STATE_DEFAULT); } if (g_info_status) { lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_status, g_lora_last_diag); + lv_label_set_text(g_info_status, safe_text(g_lora_info.diag, "LoRa not ready")); lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFF4D4F), LV_PART_MAIN | LV_STATE_DEFAULT); } if (g_info_hint) { lv_obj_clear_flag(g_info_hint, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_hint, "Boot diag for CE0/CE1 check"); + lv_label_set_text(g_info_hint, safe_text(g_lora_info.probe_display, "Boot diag for SPI check")); lv_obj_set_style_text_color(g_info_hint, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); } } @@ -1482,14 +190,14 @@ static void lora_render_boot_diag(void) static void lora_render_page(void) { if (!g_ui_parent) return; - if (!g_lora_hw_ready) { + if (!g_lora_info.hw_ready) { lora_render_boot_diag(); return; } g_lora_view = LORA_VIEW_MESSAGES; lora_render_current_view(); - if (!g_lora_tx_mode && !g_lora_tx_in_progress) { - lora_start_receive_mode(); + if (!g_lora_info.tx_mode && !g_lora_info.tx_in_progress) { + (void)lora_api_call({"StartReceive"}); } } @@ -1501,15 +209,14 @@ static void lora_render_messages_view(void) lv_label_set_text(g_title_label, "Messages"); } - // Received message bubble (left, blue) if (g_rx_bubble_bg && g_rx_bubble_lbl) { lv_obj_clear_flag(g_rx_bubble_bg, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(g_rx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); lv_obj_set_pos(g_rx_bubble_bg, 4, 20); lv_obj_set_size(g_rx_bubble_bg, 250, 44); lv_obj_set_width(g_rx_bubble_lbl, 234); - if (g_lora_last_rx[0]) { - lv_label_set_text(g_rx_bubble_lbl, g_lora_last_rx); + if (g_lora_info.last_rx[0]) { + lv_label_set_text(g_rx_bubble_lbl, g_lora_info.last_rx); lv_obj_set_style_text_color(g_rx_bubble_lbl, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); } else { lv_label_set_text(g_rx_bubble_lbl, "Waiting for message..."); @@ -1517,15 +224,14 @@ static void lora_render_messages_view(void) } } - // Sent message bubble (right, green) if (g_tx_bubble_bg && g_tx_bubble_lbl) { - if (g_lora_has_sent_message) { + if (g_lora_info.has_sent_message) { lv_obj_clear_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(g_tx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); lv_obj_set_pos(g_tx_bubble_bg, 66, 68); lv_obj_set_size(g_tx_bubble_bg, 250, 44); lv_obj_set_width(g_tx_bubble_lbl, 234); - lv_label_set_text(g_tx_bubble_lbl, g_lora_last_tx[0] ? g_lora_last_tx : ""); + lv_label_set_text(g_tx_bubble_lbl, safe_text(g_lora_info.last_tx)); } else { lv_obj_add_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(g_tx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); @@ -1534,8 +240,8 @@ static void lora_render_messages_view(void) if (g_info_status) { lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - char text[256]; - snprintf(text, sizeof(text), "RSSI: %.1fdBm | SNR: %.1fdB", g_lora_last_rssi, g_lora_last_snr); + char text[128]; + snprintf(text, sizeof(text), "RSSI: %.1fdBm | SNR: %.1fdB", g_lora_info.rssi, g_lora_info.snr); lv_label_set_text(g_info_status, text); lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); } @@ -1561,20 +267,21 @@ static void lora_render_info_view(void) if (g_info_device) { lv_obj_clear_flag(g_info_device, LV_OBJ_FLAG_HIDDEN); char text[192]; - snprintf(text, sizeof(text), "Channel: %s | BW:%s SF:%s CR:%s", g_lora_cfg_freq, g_lora_cfg_bw, g_lora_cfg_sf, g_lora_cfg_cr); + snprintf(text, sizeof(text), "Device: %s | RX:%s TX:%s", + safe_text(g_lora_info.spi_device, "n/a"), + g_lora_info.hw_ready ? "ready" : "off", + g_lora_info.tx_in_progress ? "busy" : "idle"); lv_label_set_text(g_info_device, text); lv_obj_set_style_text_color(g_info_device, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); } if (g_info_mode) { lv_obj_clear_flag(g_info_mode, LV_OBJ_FLAG_HIDDEN); - char text[192]; - snprintf(text, sizeof(text), "Sync:%s Preamble:%s Power:%s TCXO:%s", g_lora_cfg_sync, g_lora_cfg_preamble, g_lora_cfg_power, g_lora_cfg_tcxo); - lv_label_set_text(g_info_mode, text); + lv_label_set_text(g_info_mode, safe_text(g_lora_info.probe_display, "Channel: 868MHz | BW:125kHz SF12")); lv_obj_set_style_text_color(g_info_mode, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); } if (g_info_status) { lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_status, g_lora_last_diag); + lv_label_set_text(g_info_status, safe_text(g_lora_info.diag, "LoRa ready")); lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); } if (g_info_hint) { @@ -1618,13 +325,13 @@ static void lora_render_sent_popup(void) lv_obj_set_pos(g_rx_bubble_bg, 4, 22); lv_obj_set_size(g_rx_bubble_bg, 312, 86); lv_obj_set_width(g_rx_bubble_lbl, 296); - lv_label_set_text(g_rx_bubble_lbl, g_lora_last_rx[0] ? g_lora_last_rx : ""); + lv_label_set_text(g_rx_bubble_lbl, safe_text(g_lora_info.last_rx, "")); lv_obj_set_style_text_color(g_rx_bubble_lbl, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); } if (g_info_status) { lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); char text[96]; - snprintf(text, sizeof(text), "SNR %.1f RSSI %.0f", g_lora_last_snr, g_lora_last_rssi); + snprintf(text, sizeof(text), "SNR %.1f RSSI %.0f", g_lora_info.snr, g_lora_info.rssi); lv_label_set_text(g_info_status, text); lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); } @@ -1632,7 +339,7 @@ static void lora_render_sent_popup(void) static void lora_render_current_view(void) { - if (g_lora_sent_popup_until_ms != 0 && get_monotonic_ms() < g_lora_sent_popup_until_ms) { + if (g_lora_sent_popup_until_ms != 0 && lv_tick_elaps(g_lora_sent_popup_until_ms) < 2000) { lora_render_sent_popup(); return; } @@ -1658,11 +365,6 @@ static void lora_open_send_view(uint32_t first_key) lora_render_send_view(); } - -// ============================================================ -// Keyboard input handling -// ============================================================ - static bool is_lora_text_key(uint32_t key) { return (key >= 'A' && key <= 'Z') || @@ -1674,11 +376,11 @@ static bool is_lora_text_key(uint32_t key) static char lora_key_to_char(uint32_t key) { - if (key >= 'A' && key <= 'Z') return (char)key; - if (key >= 'a' && key <= 'z') return (char)key; + if (key >= 'A' && key <= 'Z') return static_cast(key); + if (key >= 'a' && key <= 'z') return static_cast(key); if ((key >= '0' && key <= '9') || key == ' ' || key == '-' || key == '_' || key == '.' || key == ',' || key == '!' || key == '?' || key == '#') { - return (char)key; + return static_cast(key); } return '\0'; } @@ -1715,7 +417,8 @@ static bool handle_app_key(uint32_t key) return true; } if (key == LV_KEY_ENTER) { - if (lora_send_text_packet(g_lora_tx_input)) { + if (lora_api_call({"SendText", g_lora_tx_input}) == 0) { + refresh_lora_info(false); g_lora_view = LORA_VIEW_MESSAGES; g_lora_sent_popup_until_ms = 0; lora_render_current_view(); @@ -1763,20 +466,19 @@ static bool handle_app_key(uint32_t key) static void lora_key_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) != (lv_event_code_t)LV_EVENT_KEYBOARD) return; - struct key_item *elm = (struct key_item *)lv_event_get_param(e); - if (!elm || elm->key_state == 0) return; // Ignore release events + if (lv_event_get_code(e) != static_cast(LV_EVENT_KEYBOARD)) return; + auto *elm = static_cast(lv_event_get_param(e)); + if (!elm || elm->key_state == 0) return; uint32_t key = elm->key_code; uint32_t cp = elm->codepoint; - // For letters/digits/symbols, prefer the Unicode code point converted by xkbcommon - if (cp >= 'a' && cp <= 'z') key = cp; - else if (cp >= 'A' && cp <= 'Z') key = cp; - else if (cp >= '0' && cp <= '9') key = cp; - else if (cp == ' ' || cp == '-' || cp == '_' || cp == '.' || cp == ',' || cp == '!' || cp == '?' || cp == '#') key = cp; + if ((cp >= 'a' && cp <= 'z') || (cp >= 'A' && cp <= 'Z') || + (cp >= '0' && cp <= '9') || cp == ' ' || cp == '-' || cp == '_' || + cp == '.' || cp == ',' || cp == '!' || cp == '?' || cp == '#') { + key = cp; + } - // Map arrow/function keys to LV_KEY_* if (key == KEY_UP) key = LV_KEY_UP; else if (key == KEY_DOWN) key = LV_KEY_DOWN; else if (key == KEY_LEFT) key = LV_KEY_LEFT; @@ -1786,32 +488,28 @@ static void lora_key_event_cb(lv_event_t *e) else if (key == KEY_BACKSPACE) key = LV_KEY_BACKSPACE; else if (key == KEY_DELETE) key = LV_KEY_DEL; - SLOGI("[LoRa] raw=%u cp=%u key=0x%X view=%d", elm->key_code, cp, key, (int)g_lora_view); (void)handle_app_key(key); } -// ============================================================ -// LVGL timer -// ============================================================ - static void lora_timer_cb(lv_timer_t *timer) { (void)timer; - lora_poll_irq_and_update_ui(); + if (!g_app_active) return; + refresh_lora_info(true); + if (g_lora_info.rx_event) { + g_lora_view = LORA_VIEW_MESSAGES; + g_lora_sent_popup_until_ms = lv_tick_get(); + } + lora_render_current_view(); } -// ============================================================ -// Public API -// ============================================================ - void ui_app_lora_create(lv_obj_t* parent, lv_obj_t* root) { if (!parent || !root) return; - // Clean up old state (if previously created) if (g_lora_timer) { lv_timer_delete(g_lora_timer); - g_lora_timer = NULL; + g_lora_timer = nullptr; } g_ui_parent = parent; @@ -1820,88 +518,66 @@ void ui_app_lora_create(lv_obj_t* parent, lv_obj_t* root) g_lora_view = LORA_VIEW_MESSAGES; g_lora_sent_popup_until_ms = 0; - // Create UI labels - // Title: centered at the top g_title_label = lora_make_label(parent, "LoRa-1262", 0, 0, 320, 18, &lv_font_montserrat_14, lv_color_hex(0x8D44FF), LV_TEXT_ALIGN_CENTER); - - // Content: main content area g_content_label = lora_make_label(parent, "", 8, 22, 304, 90, &lv_font_montserrat_12, lv_color_hex(0xFFFFFF), LV_TEXT_ALIGN_LEFT); lv_label_set_long_mode(g_content_label, LV_LABEL_LONG_WRAP); - - // Info lines (for info view) g_info_pins = lora_make_label(parent, "", 8, 22, 304, 18, &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); g_info_device = lora_make_label(parent, "", 8, 42, 304, 18, &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); g_info_mode = lora_make_label(parent, "", 8, 62, 304, 18, &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); - - // Status: bottom status g_info_status = lora_make_label(parent, "", 8, 114, 304, 16, &lv_font_montserrat_12, lv_color_hex(0xFFD24A), LV_TEXT_ALIGN_LEFT); - - // Hint: bottommost g_info_hint = lora_make_label(parent, "", 8, 132, 304, 14, &lv_font_montserrat_10, lv_color_hex(0x8AA8FF), LV_TEXT_ALIGN_LEFT); - // Chat bubbles (Messages view) g_rx_bubble_bg = lora_make_bubble(parent, 4, 20, 250, 44, lv_color_hex(0x3A7DFF)); g_rx_bubble_lbl = lora_make_bubble_label(g_rx_bubble_bg, 234); lv_obj_add_flag(g_rx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - g_tx_bubble_bg = lora_make_bubble(parent, 66, 68, 250, 44, lv_color_hex(0x00A854)); g_tx_bubble_lbl = lora_make_bubble_label(g_tx_bubble_bg, 234); lv_obj_add_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - // Bind keyboard events to root - lv_obj_add_event_cb(root, lora_key_event_cb, (lv_event_code_t)LV_EVENT_KEYBOARD, NULL); + lv_obj_add_event_cb(root, lora_key_event_cb, static_cast(LV_EVENT_KEYBOARD), nullptr); - // Initialize hardware (if not initialized yet) - if (!g_lora_initialized && !g_lora_hw_ready) { - lora_init_hardware(); - } - - // Render the page + (void)lora_api_call({"Init"}); + refresh_lora_info(false); lora_render_page(); - - // Start timer (100ms polling interval) - g_lora_timer = lv_timer_create(lora_timer_cb, 100, NULL); + g_lora_timer = lv_timer_create(lora_timer_cb, 100, nullptr); } void lora_app_task() { - lora_poll_irq_and_update_ui(); + refresh_lora_info(true); } - void ui_app_lora_destroy(void) { if (g_lora_timer) { lv_timer_delete(g_lora_timer); - g_lora_timer = NULL; + g_lora_timer = nullptr; } g_app_active = false; - g_ui_parent = NULL; - g_ui_root = NULL; - g_title_label = NULL; - g_content_label = NULL; - g_info_pins = NULL; - g_info_device = NULL; - g_info_mode = NULL; - g_info_status = NULL; - g_info_hint = NULL; - g_rx_bubble_bg = NULL; - g_rx_bubble_lbl = NULL; - g_tx_bubble_bg = NULL; - g_tx_bubble_lbl = NULL; + g_ui_parent = nullptr; + g_ui_root = nullptr; + g_title_label = nullptr; + g_content_label = nullptr; + g_info_pins = nullptr; + g_info_device = nullptr; + g_info_mode = nullptr; + g_info_status = nullptr; + g_info_hint = nullptr; + g_rx_bubble_bg = nullptr; + g_rx_bubble_lbl = nullptr; + g_tx_bubble_bg = nullptr; + g_tx_bubble_lbl = nullptr; } } // namespace Lora_APP - - class UILoraPage : public AppPage { public: @@ -1918,7 +594,4 @@ class UILoraPage : public AppPage Lora_APP::ui_app_lora_set_go_back(nullptr); Lora_APP::ui_app_lora_destroy(); } - }; - -#endif // !HAL_PLATFORM_SDL diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp index a56246ba..71592714 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp @@ -5,13 +5,8 @@ */ #pragma once -// Note: this file used to be wrapped in `#if !defined(HAL_PLATFORM_SDL)` to -// exclude it from the emulator build, but ui_app_launch.cpp references -// UISetupPage unconditionally. The class body is cp0_lvgl-clean (uses cp0_wifi_*, -// cp0_battery_*, cp0_volume_*); residual raw syscalls (i2c ioctl, popen for -// IP/whoami, sudo date) are either already inside #ifdef __linux__ or only -// triggered by user actions that the emulator never performs. Keeping the -// class compiled on every platform lets the emulator open the SETTING page. +// Keep this page platform-neutral: system and hardware operations go through +// cp0_lvgl's cp0_* service interfaces so the SDL build can compile the page. #define _STRINGIFY(x) #x #define STRINGIFY(x) _STRINGIFY(x) #ifndef LAUNCHER_GIT_COMMIT_RAW @@ -32,13 +27,10 @@ #include #endif #include -#ifdef __linux__ -#include -#include -#include -#endif +#include "APPLaunch_api.h" #include "cp0_lvgl_app.h" #include "hal_lvgl_bsp.h" +#include "../AppRegistry.h" // ============================================================ // System settings screen UISetupPage (Carousel Design) @@ -127,6 +119,26 @@ class UISetupPage : public AppPage void play_enter() { play_audio_file(snd_enter_); } void play_back() { play_audio_file(snd_back_); } + static int config_get_int(const char *key, int default_val) + { + int val = default_val; + cp0_signal_config_api({"GetInt", key ? std::string(key) : std::string(), std::to_string(default_val)}, + [&](int code, std::string data) { + if (code == 0) val = std::atoi(data.c_str()); + }); + return val; + } + + static void config_set_int(const char *key, int val) + { + cp0_signal_config_api({"SetInt", key ? std::string(key) : std::string(), std::to_string(val)}, nullptr); + } + + static void config_save() + { + cp0_signal_config_api({"Save"}, nullptr); + } + void play_audio_file(const std::string &path) { if (!path.empty()) cp0_signal_audio_api({"PlayFile", path}, nullptr); @@ -150,29 +162,15 @@ class UISetupPage : public AppPage { MenuItem m; m.label = "Launcher"; - static const char *app_keys[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", - "IP_Panel", "File", - "SSH", "Mesh", "Rec", "Camera", - "LoRa", "Tank" - }; - static const char *app_labels[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", - "IP Panel", "File", - "SSH", "Mesh", "Rec", "Camera", - "LoRa", "Tank" - }; - // Always-on apps (cannot be disabled) - static const char *always_on[] = {"Store", "CLI", "Game", "Setting"}; - - for (int i = 0; i < (int)(sizeof(app_keys) / sizeof(app_keys[0])); ++i) { - char cfg_key[64]; - snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[i]); - bool enabled = cp0_config_get_int(cfg_key, 1) != 0; - m.sub_items.push_back({app_labels[i], true, enabled, - [this, i]() { save_app_toggle(i); }}); + std::size_t app_count = 0; + const AppDescriptor *apps = launcher_app_registry_entries(&app_count); + for (std::size_t i = 0; i < app_count; ++i) { + const AppDescriptor &desc = apps[i]; + if (!desc.configurable) + continue; + bool enabled = launcher_app_registry_is_enabled(desc); + m.sub_items.push_back({desc.label, true, enabled, + [this, key = std::string(desc.config_key)]() { save_app_toggle(key); }}); } menu_items_.push_back(m); } @@ -263,18 +261,18 @@ class UISetupPage : public AppPage { MenuItem m; m.label = "ExtPort"; - bool usb_en = cp0_config_get_int("extport_usb", 1) != 0; - bool vout_en = cp0_config_get_int("extport_5vout", 1) != 0; + bool usb_en = config_get_int("extport_usb", 1) != 0; + bool vout_en = config_get_int("extport_5vout", 1) != 0; m.sub_items = { {"USB", true, usb_en, [this]() { bool en = menu_items_[7].sub_items[0].toggle_state; - cp0_config_set_int("extport_usb", en ? 1 : 0); - cp0_config_save(); + config_set_int("extport_usb", en ? 1 : 0); + config_save(); }}, {"5VOUT", true, vout_en, [this]() { bool en = menu_items_[7].sub_items[1].toggle_state; - cp0_config_set_int("extport_5vout", en ? 1 : 0); - cp0_config_save(); + config_set_int("extport_5vout", en ? 1 : 0); + config_save(); }}, }; menu_items_.push_back(m); @@ -366,7 +364,7 @@ class UISetupPage : public AppPage { val_title_ = "Volume"; val_options_ = {"100%", "75%", "50%", "25%", "0%"}; - vol_val_ = cp0_config_get_int("volume", cp0_volume_read()); + vol_val_ = config_get_int("volume", APPLaunch_volume_read()); int pct = vol_val_ * 100 / 63; if (pct >= 87) val_sel_idx_ = 0; else if (pct >= 62) val_sel_idx_ = 1; @@ -390,7 +388,7 @@ class UISetupPage : public AppPage { val_title_ = "Startup"; val_options_ = {"Launcher", "CLI"}; - val_sel_idx_ = cp0_config_get_int("startup_mode", 0); + val_sel_idx_ = config_get_int("startup_mode", 0); view_state_ = ViewState::VALUE_SELECT; transition_enter_level(); } @@ -413,10 +411,10 @@ class UISetupPage : public AppPage // Title + current connection status lv_obj_t *title = lv_label_create(cont); { - cp0_wifi_status_t ws = cp0_wifi_get_status(); + cp0_wifi_status_t ws = launcher_wifi::get_status(); static char title_buf[128]; if (ws.connected) - snprintf(title_buf, sizeof(title_buf), "Connected WiFi: %s %s", ws.ssid, ws.ip); + snprintf(title_buf, sizeof(title_buf), "Connected WiFi: %.64s %.40s", ws.ssid, ws.ip); else snprintf(title_buf, sizeof(title_buf), "WiFi: Not connected"); lv_label_set_text(title, title_buf); @@ -626,37 +624,31 @@ class UISetupPage : public AppPage { int new_val = atoi(val_options_[val_sel_idx_].c_str()); rtc_values_[rtc_field_] = new_val; - // Apply to system via date command - char cmd[128]; - snprintf(cmd, sizeof(cmd), "sudo date -s '%04d-%02d-%02d %02d:%02d:%02d' >/dev/null 2>&1", + char timestamp[32]; + snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d", rtc_values_[0], rtc_values_[1], rtc_values_[2], rtc_values_[3], rtc_values_[4], rtc_values_[5]); - system(cmd); - } - - void save_app_toggle(int idx) - { - static const char *app_keys[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", - "IP_Panel", "File", - "SSH", "Mesh", "Rec", "Camera", - "LoRa", "Tank" - }; - // Enforce always-on apps - static const char *always_on[] = {"Store", "CLI", "Game", "Setting"}; - for (auto *ao : always_on) { - if (strcmp(app_keys[idx], ao) == 0) { - // Force back to enabled - menu_items_[0].sub_items[idx].toggle_state = true; + cp0_time_set(timestamp); + } + + void save_app_toggle(const std::string &config_key) + { + std::size_t app_count = 0; + const AppDescriptor *apps = launcher_app_registry_entries(&app_count); + int visible_idx = 0; + for (std::size_t i = 0; i < app_count; ++i) { + const AppDescriptor &desc = apps[i]; + if (!desc.configurable) + continue; + if (config_key == desc.config_key) { + bool enabled = menu_items_[0].sub_items[visible_idx].toggle_state; + launcher_app_registry_set_enabled(desc, enabled); + config_save(); + launcher_app_registry_notify_changed(); return; } + ++visible_idx; } - char cfg_key[64]; - snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); - bool enabled = menu_items_[0].sub_items[idx].toggle_state; - cp0_config_set_int(cfg_key, enabled ? 1 : 0); - cp0_config_save(); } // ==================== Bluetooth ==================== @@ -692,42 +684,11 @@ class UISetupPage : public AppPage { for (auto &m : menu_items_) { if (m.label != "Ethernet") continue; - // Read IP info via system commands - char buf[128]; - FILE *fp; - // IP address - fp = popen("ip -4 addr show eth0 2>/dev/null | grep inet | awk '{print $2}' | head -1", "r"); - if (fp) { - if (fgets(buf, sizeof(buf), fp)) { - buf[strcspn(buf, "\n")] = 0; - m.sub_items[0].label = std::string("IP: ") + (buf[0] ? buf : "N/A"); - } else { - m.sub_items[0].label = "IP: N/A"; - } - pclose(fp); - } - // Gateway - fp = popen("ip route | grep default | grep eth0 | awk '{print $3}' | head -1", "r"); - if (fp) { - if (fgets(buf, sizeof(buf), fp)) { - buf[strcspn(buf, "\n")] = 0; - m.sub_items[1].label = std::string("GW: ") + (buf[0] ? buf : "N/A"); - } else { - m.sub_items[1].label = "GW: N/A"; - } - pclose(fp); - } - // MAC - fp = popen("cat /sys/class/net/eth0/address 2>/dev/null", "r"); - if (fp) { - if (fgets(buf, sizeof(buf), fp)) { - buf[strcspn(buf, "\n")] = 0; - m.sub_items[2].label = std::string("MAC: ") + (buf[0] ? buf : "N/A"); - } else { - m.sub_items[2].label = "MAC: N/A"; - } - pclose(fp); - } + cp0_eth_info_t info; + cp0_network_default_info_read(&info); + m.sub_items[0].label = std::string("IP: ") + info.ipv4; + m.sub_items[1].label = std::string("GW: ") + info.gateway; + m.sub_items[2].label = std::string("MAC: ") + info.mac; break; } } @@ -737,24 +698,11 @@ class UISetupPage : public AppPage { for (auto &m : menu_items_) { if (m.label != "Account") continue; - char buf[128]; - FILE *fp = popen("whoami", "r"); - if (fp) { - if (fgets(buf, sizeof(buf), fp)) { - buf[strcspn(buf, "\n")] = 0; - m.sub_items[0].label = std::string("User: ") + buf; - } - pclose(fp); - } + cp0_account_info_t info; + cp0_account_info_read(&info); + m.sub_items[0].label = std::string("User: ") + info.user; m.sub_items[1].label = "Password: ****"; - fp = popen("hostname", "r"); - if (fp) { - if (fgets(buf, sizeof(buf), fp)) { - buf[strcspn(buf, "\n")] = 0; - m.sub_items[2].label = std::string("Host: ") + buf; - } - pclose(fp); - } + m.sub_items[2].label = std::string("Host: ") + info.hostname; break; } } @@ -772,17 +720,13 @@ class UISetupPage : public AppPage void check_system_update() { // Run apt update check in background - system("apt update >/dev/null 2>&1 &"); + cp0_system_apt_update_background(); // TODO: show result in UI } void update_launcher() { - // Pull latest from GitHub and rebuild - system("cd /usr/share/APPLaunch && " - "wget -q https://github.com/CardputerZero/M5CardputerZero-Launcher/releases/latest/download/applaunch_*.deb -O /tmp/launcher_update.deb 2>/dev/null && " - "dpkg -i /tmp/launcher_update.deb >/dev/null 2>&1 && " - "systemctl restart APPLaunch &"); + cp0_system_update_launcher_background(); } // ==================== Confirm action (Reboot/Shutdown) ==================== @@ -827,21 +771,7 @@ class UISetupPage : public AppPage void apply_bq_calibrate() { -#ifdef __linux__ - static constexpr const char *BQ_DEV = "/dev/i2c-1"; - static constexpr int BQ_ADDR = 0x55; - int cmds[] = {0x0081, 0x000A, 0x0009, 0x0080}; // Enter/CC/Board/Exit - int fd = open(BQ_DEV, O_RDWR); - if (fd < 0) return; - struct i2c_msg msg; - struct i2c_rdwr_ioctl_data data; - unsigned char buf[3] = {0x00, (unsigned char)(cmds[val_sel_idx_] & 0xFF), - (unsigned char)((cmds[val_sel_idx_] >> 8) & 0xFF)}; - msg.addr = BQ_ADDR; msg.flags = 0; msg.len = 3; msg.buf = buf; - data.msgs = &msg; data.nmsgs = 1; - ioctl(fd, I2C_RDWR, &data); - close(fd); -#endif + cp0_bq27220_calibrate(val_sel_idx_); } // ==================== About ==================== @@ -914,7 +844,7 @@ class UISetupPage : public AppPage // ==================== WiFi functions ==================== void wifi_do_scan() { - wifi_ap_count_ = cp0_wifi_scan(wifi_aps_, CP0_WIFI_AP_MAX); + wifi_ap_count_ = launcher_wifi::scan(wifi_aps_, CP0_WIFI_AP_MAX); } void wifi_toggle_enable() @@ -938,10 +868,10 @@ class UISetupPage : public AppPage int ret = -1; if (strcmp(ap->security, "Open") == 0 || ap->security[0] == 0) { wifi_show_connecting(ap->ssid); - ret = cp0_wifi_connect(ap->ssid, NULL); + ret = launcher_wifi::connect(ap->ssid, NULL); } else if (wifi_has_saved_profile(ap->ssid)) { wifi_show_connecting(ap->ssid); - ret = cp0_wifi_connect(ap->ssid, NULL); + ret = launcher_wifi::connect(ap->ssid, NULL); } else { needs_password = true; wifi_pw_ssid_ = ap->ssid; @@ -1021,13 +951,11 @@ class UISetupPage : public AppPage // Override: next KEY_ENTER in pw handler will do the delete // Actually — simpler: just do it immediately (user already pressed F // which is intentional). Delete + refresh. - char del_cmd[256]; - snprintf(del_cmd, sizeof(del_cmd), "nmcli con delete id '%s' 2>/dev/null", ap->ssid); - system(del_cmd); + launcher_wifi::profile_forget(ap->ssid); // If currently connected to this SSID, disconnect if (ap->in_use) { - system("nmcli con down id 'active' 2>/dev/null"); + launcher_wifi::profile_disconnect_active(); } // Show success briefly @@ -1048,9 +976,7 @@ class UISetupPage : public AppPage bool wifi_has_saved_profile(const char *ssid) { - char cmd[256]; - snprintf(cmd, sizeof(cmd), "nmcli -t -f NAME con show 2>/dev/null | grep -qxF '%s'", ssid); - return system(cmd) == 0; + return launcher_wifi::profile_exists(ssid) != 0; } void show_wifi_pw_input() @@ -1098,14 +1024,11 @@ class UISetupPage : public AppPage if (key == KEY_ENTER) { if (pw_hint_lbl_) lv_label_set_text(pw_hint_lbl_, "Connecting..."); lv_refr_now(NULL); - int ret = cp0_wifi_connect(wifi_pw_ssid_.c_str(), wifi_pw_buf_.c_str()); + int ret = launcher_wifi::connect(wifi_pw_ssid_.c_str(), wifi_pw_buf_.c_str()); if (ret != 0) { // Connection failed — delete the broken profile that nmcli just // saved with the wrong password, so next attempt won't reuse it. - char del_cmd[256]; - snprintf(del_cmd, sizeof(del_cmd), - "nmcli con delete id '%s' 2>/dev/null", wifi_pw_ssid_.c_str()); - system(del_cmd); + launcher_wifi::profile_forget(wifi_pw_ssid_.c_str()); if (pw_hint_lbl_) { lv_label_set_text(pw_hint_lbl_, "Failed! Wrong password? Try again."); @@ -1143,18 +1066,36 @@ class UISetupPage : public AppPage { int pcts[] = {100, 75, 50, 25, 0}; int new_val = 63 * pcts[val_sel_idx_] / 100; - cp0_volume_write(new_val); - cp0_config_set_int("volume", new_val); - cp0_config_save(); + APPLaunch_volume_write(new_val); + config_set_int("volume", new_val); + config_save(); } // ==================== Brightness ==================== + int settings_backlight_read() + { + int value = -1; + cp0_signal_settings_api({"BacklightRead"}, [&](int code, std::string data) { + if (code == 0) value = std::atoi(data.c_str()); + }); + return value; + } + + int settings_backlight_max() + { + int value = 100; + cp0_signal_settings_api({"BacklightMax"}, [&](int code, std::string data) { + if (code == 0) value = std::atoi(data.c_str()); + }); + return value; + } + void enter_brightness_adjust() { val_title_ = "Brightness"; val_options_ = {"100%", "75%", "50%", "25%"}; - bright_val_ = cp0_backlight_read(); - int mx = cp0_backlight_max(); + bright_val_ = settings_backlight_read(); + int mx = settings_backlight_max(); int pct = mx > 0 ? bright_val_ * 100 / mx : 100; if (pct >= 87) val_sel_idx_ = 0; else if (pct >= 62) val_sel_idx_ = 1; @@ -1167,26 +1108,26 @@ class UISetupPage : public AppPage void apply_value_selection() { if (val_title_ == "Brightness") { - int mx = cp0_backlight_max(); + int mx = settings_backlight_max(); int pcts[] = {100, 75, 50, 25}; int new_val = mx * pcts[val_sel_idx_] / 100; if (new_val < 1) new_val = 1; cp0_backlight_write(new_val); - cp0_config_set_int("brightness", new_val); - cp0_config_save(); + config_set_int("brightness", new_val); + config_save(); } else if (val_title_ == "Volume") { apply_volume(); } else if (val_title_ == "DarkTime") { // TODO: save dark time setting int times[] = {0, 10, 30, 60, 300}; - cp0_config_set_int("dark_time", times[val_sel_idx_]); - cp0_config_save(); + config_set_int("dark_time", times[val_sel_idx_]); + config_save(); } else if (val_title_ == "Resolution") { - cp0_config_set_int("cam_resolution", val_sel_idx_); - cp0_config_save(); + config_set_int("cam_resolution", val_sel_idx_); + config_save(); } else if (val_title_ == "Startup") { - cp0_config_set_int("startup_mode", val_sel_idx_); - cp0_config_save(); + config_set_int("startup_mode", val_sel_idx_); + config_save(); } else if (val_title_ == "Year" || val_title_ == "Month" || val_title_ == "Day" || val_title_ == "Hour" || val_title_ == "Minute" || val_title_ == "Second") { apply_rtc_value(); diff --git a/projects/APPLaunch/main/ui/ui_app_page.hpp b/projects/APPLaunch/main/ui/ui_app_page.hpp index 547a4a0b..b469bc6e 100644 --- a/projects/APPLaunch/main/ui/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/ui_app_page.hpp @@ -19,12 +19,116 @@ #include #include #include +#include #include #include #include "cp0_lvgl_app.h" +#include "hal_lvgl_bsp.h" #include "cp0_lvgl_file.hpp" #define APP_CONSOLE_EXIT_EVENT (lv_event_code_t)(LV_EVENT_LAST + 1) +namespace launcher_wifi { + +inline std::vector split_colon(const std::string &line) +{ + std::vector cols; + std::string current; + for (char ch : line) { + if (ch == ':') { + cols.push_back(current); + current.clear(); + } else { + current.push_back(ch); + } + } + cols.push_back(current); + return cols; +} + +inline void copy_string(char *dst, size_t dst_size, const std::string &src) +{ + if (!dst || dst_size == 0) + return; + std::snprintf(dst, dst_size, "%s", src.c_str()); +} + +inline cp0_wifi_status_t get_status() +{ + cp0_wifi_status_t st{}; + cp0_signal_wifi_api({"Status"}, [&](int code, std::string data) { + if (code != 0) + return; + auto cols = split_colon(data); + if (cols.size() < 4) + return; + st.connected = std::atoi(cols[0].c_str()); + copy_string(st.ssid, sizeof(st.ssid), cols[1]); + copy_string(st.ip, sizeof(st.ip), cols[2]); + st.signal = std::atoi(cols[3].c_str()); + }); + return st; +} + +inline int scan(cp0_wifi_ap_t *out, int max_aps) +{ + int count = 0; + cp0_signal_wifi_api({"Scan", std::to_string(max_aps)}, [&](int code, std::string data) { + if (!out || max_aps <= 0) { + count = code; + return; + } + std::istringstream lines(data); + std::string line; + while (count < max_aps && std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + auto cols = split_colon(line); + if (cols.size() < 4 || cols[0].empty()) + continue; + cp0_wifi_ap_t ap{}; + copy_string(ap.ssid, sizeof(ap.ssid), cols[0]); + ap.signal = std::atoi(cols[1].c_str()); + copy_string(ap.security, sizeof(ap.security), cols[2]); + ap.in_use = std::atoi(cols[3].c_str()); + out[count++] = ap; + } + }); + return count; +} + +inline int simple(const std::list &args) +{ + int result = -1; + cp0_signal_wifi_api(args, [&](int code, std::string) { result = code; }); + return result; +} + +inline int connect(const char *ssid, const char *password) +{ + if (!ssid || !ssid[0]) + return -1; + if (password && password[0]) + return simple({"Connect", ssid, password}); + return simple({"Connect", ssid}); +} + +inline int profile_forget(const char *ssid) +{ + return (!ssid || !ssid[0]) ? -1 : simple({"ProfileForget", ssid}); +} + +inline int profile_exists(const char *ssid) +{ + return (!ssid || !ssid[0]) ? 0 : simple({"ProfileExists", ssid}); +} + +inline int profile_disconnect_active() +{ + return simple({"ProfileDisconnectActive"}); +} + +} // namespace launcher_wifi + class UIAppTopBar { public: @@ -87,7 +191,7 @@ class UIAppTopBar void update_wifi() { - cp0_wifi_status_t ws = cp0_wifi_get_status(); + cp0_wifi_status_t ws = launcher_wifi::get_status(); set_wifi_signal(ws.connected ? ws.signal : 0); if (!wifi_panel_) return; @@ -734,7 +838,7 @@ class home_base : public AppPageRoot if (!ui_TOP_wifiPanel) return; - cp0_wifi_status_t ws = cp0_wifi_get_status(); + cp0_wifi_status_t ws = launcher_wifi::get_status(); static const int thresholds[4] = {1, 30, 60, 80}; for (int i = 0; i < 4; ++i) diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index 0503fe3c..a5600532 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -31,6 +31,7 @@ #include "ui.h" #include "keyboard_input.h" #include "lvgl/lvgl.h" +#include "hal_lvgl_bsp.h" #include "cp0_lvgl_app.h" #include "compat/input_keys.h" @@ -38,6 +39,7 @@ #include #include #include +#include #include #include @@ -260,7 +262,10 @@ void on_key(const struct key_item *elm) if (sudo_gid) gid = (gid_t)atoi(sudo_gid); chown(scr_dir, uid, gid); } - int ret = cp0_screenshot_save(scr_dir); + int ret = -1; + cp0_signal_screenshot_api({"Save", scr_dir}, [&](int code, std::string) { + ret = code; + }); show_hint(ret == 0 ? "Saved to ~/Screenshots" : "Screenshot failed"); return; } diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp index 1777167e..f23ebb9d 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.cpp @@ -11,7 +11,7 @@ #include -void zero_lvgl_os::creat_display() +void zero_lvgl_os::create_display() { fonts_ = std::make_shared(); @@ -21,12 +21,15 @@ void zero_lvgl_os::creat_display() lv_disp_set_theme(dispp_, theme_); } -void zero_lvgl_os::create_launcher_home() +void zero_lvgl_os::build_launcher_home() { launch_page_->create_screen(); launch_->bind_ui(); launch_page_->init_input_group(); +} +void zero_lvgl_os::show_initial_screen() +{ #ifndef APPLAUNCH_STARTUP_ANIMATION launch_page_->load_home_screen(); #else @@ -47,10 +50,10 @@ void zero_lvgl_os::create_launcher_home() zero_lvgl_os::zero_lvgl_os() { - creat_display(); + create_display(); - launch_ = std::make_shared(); - launch_page_ = std::make_shared(launch_); + launch_ = std::make_unique(); + launch_page_ = std::make_shared(launch_.get()); launch_->set_launch_page(launch_page_); } @@ -58,5 +61,6 @@ zero_lvgl_os::~zero_lvgl_os() = default; void zero_lvgl_os::start() { - create_launcher_home(); + build_launcher_home(); + show_initial_screen(); } diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.h b/projects/APPLaunch/main/ui/zero_lvgl_os.h index 9f9949f5..a7a4b7fa 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.h +++ b/projects/APPLaunch/main/ui/zero_lvgl_os.h @@ -23,12 +23,13 @@ class zero_lvgl_os private: friend LauncherFonts &launcher_fonts(); - void creat_display(); - void create_launcher_home(); + void create_display(); + void build_launcher_home(); + void show_initial_screen(); lv_disp_t *dispp_ = nullptr; lv_theme_t *theme_ = nullptr; std::shared_ptr launch_page_; std::shared_ptr fonts_; - std::shared_ptr launch_; + std::unique_ptr launch_; }; From 4152149b82a7c33cddf34e149ccf5c31cdf7224f Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 16:29:21 +0800 Subject: [PATCH 54/70] feat: preload configurable system sounds Initialize the CP0 system sound cache when the audio service starts instead of lazily loading on first playback. Cache all entries from system_sound_names_, play SystemSoundPlay by index, and reload cached sounds immediately when SetSystemSoundNames updates the names. Switch CP0 volume control to pactl default-sink percentages and keep VolumeRead/VolumeWrite on a direct 0-100 contract. Mirror the SystemSoundPlay, SetSystemSoundNames, and SystemSoundEnable commands in the SDL audio backend for API parity. --- .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 205 +++++++++++++----- .../cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp | 102 ++++++--- 2 files changed, 220 insertions(+), 87 deletions(-) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index d250867b..f42200f0 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -30,7 +30,7 @@ class AudioSystem AudioSystem() { - // initialize(); + init_system_sounds(); } ~AudioSystem() @@ -49,11 +49,13 @@ class AudioSystem ma_context system_play_context_{}; ma_engine system_play_engine_{}; - ma_sound system_sound_switch_{}; - ma_sound system_sound_enter_{}; + std::array system_sounds_{}; + std::array system_sound_loaded_slots_{}; bool system_play_inited_ = false; bool system_sounds_loaded_ = false; std::mutex system_play_mutex_; + std::array system_sound_names_ = {"startup.mp3", "key_back.wav", "key_back.wav"}; + bool system_sound_enabled_ = true; static constexpr int kRecWaveformSize = 128; std::array rec_waveform_{}; @@ -208,45 +210,48 @@ class AudioSystem int ret = init_system_play_locked(); if(ret != 0) return ret; - std::string switch_path = resolve_play_file("switch.wav", true); - std::string enter_path = resolve_play_file("enter.wav", true); - - ma_result result = ma_sound_init_from_file( - &system_play_engine_, - switch_path.c_str(), - MA_SOUND_FLAG_DECODE, - NULL, - NULL, - &system_sound_switch_ - ); - if(result != MA_SUCCESS) return -3; - - result = ma_sound_init_from_file( - &system_play_engine_, - enter_path.c_str(), - MA_SOUND_FLAG_DECODE, - NULL, - NULL, - &system_sound_enter_ - ); - if(result != MA_SUCCESS) + for(size_t i = 0; i < system_sound_names_.size(); ++i) { - ma_sound_uninit(&system_sound_switch_); - return -4; + std::string path = resolve_play_file(system_sound_names_[i], true); + if(path.empty()) continue; + + ma_result result = ma_sound_init_from_file( + &system_play_engine_, + path.c_str(), + MA_SOUND_FLAG_DECODE, + NULL, + NULL, + &system_sounds_[i] + ); + if(result != MA_SUCCESS) + { + unload_system_sounds_locked(); + return -3; + } + system_sound_loaded_slots_[i] = true; } system_sounds_loaded_ = true; return 0; } - ma_sound* system_sound_for_name(const std::string& name) + void unload_system_sounds_locked() { - size_t pos = name.find_last_of("/\\"); - std::string base = (pos == std::string::npos) ? name : name.substr(pos + 1); + for(size_t i = 0; i < system_sounds_.size(); ++i) + { + if(system_sound_loaded_slots_[i]) + { + ma_sound_uninit(&system_sounds_[i]); + system_sound_loaded_slots_[i] = false; + } + } + system_sounds_loaded_ = false; + } - if(base == "switch.wav") return &system_sound_switch_; - if(base == "enter.wav") return &system_sound_enter_; - return nullptr; + int reload_system_sounds_locked() + { + unload_system_sounds_locked(); + return load_system_sounds_locked(); } void uninit_system_play() @@ -255,9 +260,7 @@ class AudioSystem if(system_sounds_loaded_) { - ma_sound_uninit(&system_sound_switch_); - ma_sound_uninit(&system_sound_enter_); - system_sounds_loaded_ = false; + unload_system_sounds_locked(); } if(system_play_inited_) @@ -467,20 +470,103 @@ class AudioSystem } } public: - void system_play(std::string name) + int init_system_sounds() { std::lock_guard lock(system_play_mutex_); + return load_system_sounds_locked(); + } + + void system_play(std::string name) + { + if(!system_sound_enabled_) return; + + { + std::lock_guard lock(system_play_mutex_); + if(!system_sounds_loaded_) return; + + for(size_t i = 0; i < system_sound_names_.size(); ++i) + { + if(system_sound_names_[i] != name) continue; + if(!system_sound_loaded_slots_[i]) return; - if(load_system_sounds_locked() != 0) return; + ma_sound* sound = &system_sounds_[i]; + if(ma_sound_is_playing(sound)) return; + ma_sound_seek_to_pcm_frame(sound, 0); + ma_sound_start(sound); + return; + } + } - ma_sound* sound = system_sound_for_name(name); - if(sound == nullptr) return; + std::string file = resolve_play_file(name, true); + if(!file.empty()) play(file); + } - ma_sound_stop(sound); + void system_play_index(size_t index) + { + if(!system_sound_enabled_) return; + + std::lock_guard lock(system_play_mutex_); + if(!system_sounds_loaded_ || index >= system_sounds_.size() || !system_sound_loaded_slots_[index]) + { + return; + } + + ma_sound* sound = &system_sounds_[index]; + if(ma_sound_is_playing(sound)) return; ma_sound_seek_to_pcm_frame(sound, 0); ma_sound_start(sound); } + void SetSystemSoundNames(arg_t arg, callback_t callback) + { + { + std::lock_guard lock(system_play_mutex_); + auto it = arg.begin(); + if(it != arg.end()) ++it; + for(size_t i = 0; i < system_sound_names_.size() && it != arg.end(); ++i, ++it) + { + if(!it->empty()) system_sound_names_[i] = *it; + } + + int ret = reload_system_sounds_locked(); + if(ret != 0) + { + report(callback, ret, "system sound reload failed\n"); + return; + } + } + report(callback, 0, "ok"); + } + + void SystemSoundPlay(arg_t arg, callback_t callback) + { + int index = std::atoi(first_arg_after_command(arg).c_str()); + if(index < 0 || index >= static_cast(system_sound_names_.size())) + { + report(callback, -1, "invalid system sound index\n"); + return; + } + if(!system_sound_enabled_) + { + report(callback, 0, "system sound disabled\n"); + return; + } + system_play_index(static_cast(index)); + report(callback, 0, "system sound play\n"); + } + + void SystemSoundEnable(arg_t arg, callback_t callback) + { + std::string value = first_arg_after_command(arg); + if(arg_is_disable(value)) + system_sound_enabled_ = false; + else if(value.empty() || arg_is_enable(value)) + system_sound_enabled_ = true; + else + system_sound_enabled_ = std::atoi(value.c_str()) != 0; + report(callback, 0, system_sound_enabled_ ? "1" : "0"); + } + void cap(bool enable) { if(enable) @@ -675,7 +761,10 @@ class AudioSystem map_fun(CapFileSave), map_fun(SetCallback), map_fun(VolumeRead), - map_fun(VolumeWrite) + map_fun(VolumeWrite), + map_fun(SetSystemSoundNames), + map_fun(SystemSoundPlay), + map_fun(SystemSoundEnable) }; #undef map_fun @@ -693,14 +782,18 @@ class AudioSystem static int read_system_volume() { - FILE *p = popen("amixer -c1 sget 'Headphone Playback Volume' 2>/dev/null", "r"); + FILE *p = popen("pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null", "r"); if (!p) return -1; char buf[256]; int val = -1; while (fgets(buf, sizeof(buf), p)) { - char *s = strstr(buf, ": values="); - if (s) { - val = atoi(s + 9); + char *pct = strchr(buf, '%'); + if (pct) { + char *start = pct; + while (start > buf && start[-1] >= '0' && start[-1] <= '9') { + --start; + } + val = clamp_percent(atoi(start)); break; } } @@ -710,24 +803,23 @@ class AudioSystem static int write_system_volume(int val) { - if (val < 0) val = 0; - if (val > 63) val = 63; + val = clamp_percent(val); char cmd[128]; - snprintf(cmd, sizeof(cmd), "amixer -c1 sset 'Headphone Playback Volume' %d 2>/dev/null", val); - FILE *p = popen(cmd, "r"); - if (!p) return -1; - char buf[128]; - while (fgets(buf, sizeof(buf), p)) {} - pclose(p); - return val; + snprintf(cmd, sizeof(cmd), "pactl set-sink-volume @DEFAULT_SINK@ %d%%", val); + return system(cmd) == 0 ? val : -1; } static int parse_volume_arg(const arg_t& arg) { std::string value = first_arg_after_command(arg); if (value.empty()) return 0; - return std::atoi(value.c_str()); + return clamp_percent(std::atoi(value.c_str())); + } + + static int clamp_percent(int pct) + { + return std::max(0, std::min(100, pct)); } }; @@ -750,4 +842,3 @@ extern "C" void init_audio(void) { audio->system_play(name); }); } - diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp index d41dabf5..7a1d3869 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp @@ -1,6 +1,7 @@ #include "hal_lvgl_bsp.h" #include +#include #include #include #include @@ -55,7 +56,9 @@ class AudioSystem void system_play(const std::string &name) { - (void)name; + if (!system_sound_enabled_) + return; + play(name); } void api_call(arg_t arg, callback_t callback) @@ -65,36 +68,34 @@ class AudioSystem return; } - const std::string &cmd = arg.front(); - if (cmd == "PlayFile") { - PlayFile(arg, callback); - } else if (cmd == "Play") { - Play(arg, callback); - } else if (cmd == "PlayPause") { - PlayPause(arg, callback); - } else if (cmd == "PlayContinue") { - PlayContinue(arg, callback); - } else if (cmd == "PlayEnd") { - PlayEnd(arg, callback); - } else if (cmd == "Cap") { - Cap(arg, callback); - } else if (cmd == "CapPause") { - CapPause(arg, callback); - } else if (cmd == "CapContinue") { - CapContinue(arg, callback); - } else if (cmd == "CapEnd") { - CapEnd(arg, callback); - } else if (cmd == "CapFileSave") { - CapFileSave(arg, callback); - } else if (cmd == "SetCallback") { - SetCallback(arg, callback); - } else if (cmd == "VolumeRead") { - VolumeRead(arg, callback); - } else if (cmd == "VolumeWrite") { - VolumeWrite(arg, callback); - } else { - report(callback, -1, "unknown audio api\n"); +#define map_fun(name) {#name, std::bind(&AudioSystem::name, this, std::placeholders::_1, std::placeholders::_2)} + std::list>> cmd_map = { + map_fun(PlayFile), + map_fun(Play), + map_fun(PlayPause), + map_fun(PlayContinue), + map_fun(PlayEnd), + map_fun(Cap), + map_fun(CapPause), + map_fun(CapContinue), + map_fun(CapEnd), + map_fun(CapFileSave), + map_fun(SetCallback), + map_fun(VolumeRead), + map_fun(VolumeWrite), + map_fun(SetSystemSoundNames), + map_fun(SystemSoundPlay), + map_fun(SystemSoundEnable), + }; +#undef map_fun + + for (const auto &it : cmd_map) { + if (it.first == arg.front()) { + it.second(arg, callback); + return; + } } + report(callback, -1, "unknown audio api\n"); } private: @@ -109,6 +110,8 @@ class AudioSystem bool cap_paused_ = false; bool waveform_enabled_ = false; int volume_ = kDefaultVolume; + std::array system_sound_names_ = {"startup.mp3", "switch.wav", "enter.wav"}; + bool system_sound_enabled_ = true; void report(callback_t callback, int code, const std::string &data) { @@ -314,6 +317,45 @@ class AudioSystem volume_ = std::max(0, std::min(kMaxVolume, parse_volume_arg(arg))); report(callback, 0, std::to_string(volume_)); } + + void SetSystemSoundNames(arg_t arg, callback_t callback) + { + auto it = arg.begin(); + if (it != arg.end()) + ++it; + for (size_t i = 0; i < system_sound_names_.size() && it != arg.end(); ++i, ++it) { + if (!it->empty()) + system_sound_names_[i] = *it; + } + report(callback, 0, "ok"); + } + + void SystemSoundPlay(arg_t arg, callback_t callback) + { + int index = std::atoi(first_arg_after_command(arg).c_str()); + if (index < 0 || index >= static_cast(system_sound_names_.size())) { + report(callback, -1, "invalid system sound index\n"); + return; + } + if (!system_sound_enabled_) { + report(callback, 0, "system sound disabled\n"); + return; + } + system_play(system_sound_names_[static_cast(index)]); + report(callback, 0, "system sound play\n"); + } + + void SystemSoundEnable(arg_t arg, callback_t callback) + { + std::string value = first_arg_after_command(arg); + if (arg_is_disable(value)) + system_sound_enabled_ = false; + else if (value.empty() || arg_is_enable(value)) + system_sound_enabled_ = true; + else + system_sound_enabled_ = std::atoi(value.c_str()) != 0; + report(callback, 0, system_sound_enabled_ ? "1" : "0"); + } }; std::shared_ptr g_audio; From 37bfa631c8dad8b3c0be61f02f1c339ac3edda64 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 16:29:43 +0800 Subject: [PATCH 55/70] refactor: call cp0 audio services from APPLaunch Remove the temporary APPLaunch_api wrapper now that pages can call the cp0 service API directly. Route launcher startup, switch, and enter sounds through SystemSoundPlay indices so they use the preloaded system sound cache. Update setup volume handling to pass 0-100 percentages directly to VolumeRead and VolumeWrite. Drop queued carousel switch handling in favor of ignoring input while animations are active, and refresh battery status when app pages initialize. --- .../APPLaunch/main/include/APPLaunch_api.h | 15 ---- projects/APPLaunch/main/src/APPLaunch_api.cpp | 89 ------------------- projects/APPLaunch/main/ui/UILaunchPage.cpp | 43 ++------- projects/APPLaunch/main/ui/UILaunchPage.h | 8 -- .../main/ui/page_app/ui_app_setup.hpp | 31 +++++-- projects/APPLaunch/main/ui/ui_app_page.hpp | 1 + 6 files changed, 34 insertions(+), 153 deletions(-) delete mode 100644 projects/APPLaunch/main/include/APPLaunch_api.h delete mode 100644 projects/APPLaunch/main/src/APPLaunch_api.cpp diff --git a/projects/APPLaunch/main/include/APPLaunch_api.h b/projects/APPLaunch/main/include/APPLaunch_api.h deleted file mode 100644 index f5290ddb..00000000 --- a/projects/APPLaunch/main/include/APPLaunch_api.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -void APPLaunch_audio_play_file(const char *path); -void APPLaunch_audio_play_asset(const char *name); -void APPLaunch_system_play_asset(const char *name); -int APPLaunch_volume_read(void); -int APPLaunch_volume_write(int val); - -#ifdef __cplusplus -} -#endif diff --git a/projects/APPLaunch/main/src/APPLaunch_api.cpp b/projects/APPLaunch/main/src/APPLaunch_api.cpp deleted file mode 100644 index 9e34f3d3..00000000 --- a/projects/APPLaunch/main/src/APPLaunch_api.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "APPLaunch_api.h" - -#include "cp0_lvgl_app.h" -#include "hal_lvgl_bsp.h" - -#ifdef HAL_PLATFORM_SDL -#include "hal/hal_audio.h" -#include "hal/hal_settings.h" -#else -#include -#endif - -#include -#include -#include - -extern "C" { - -void APPLaunch_audio_play_file(const char *path) -{ - if (!path || !path[0]) { - return; - } - -#ifdef HAL_PLATFORM_SDL - hal_audio_play(path); -#else - cp0_signal_audio_api({"PlayFile", std::string(path)}, nullptr); -#endif -} - -void APPLaunch_audio_play_asset(const char *name) -{ - if (!name || !name[0]) { - return; - } - -#ifdef HAL_PLATFORM_SDL - const char *path = cp0_file_path_c(name); - APPLaunch_audio_play_file(path && path[0] ? path : name); -#else - cp0_signal_audio_api({"Play", std::string(name)}, nullptr); -#endif -} - -void APPLaunch_system_play_asset(const char *name) -{ - if (!name || !name[0]) { - return; - } - -#ifdef HAL_PLATFORM_SDL - APPLaunch_audio_play_asset(name); -#else - cp0_signal_system_play(std::string(name)); -#endif -} - -int APPLaunch_volume_read(void) -{ -#ifdef HAL_PLATFORM_SDL - return hal_volume_read(); -#else - int volume = -1; - cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) { - if (code == 0) { - volume = std::atoi(data.c_str()); - } - }); - return volume; -#endif -} - -int APPLaunch_volume_write(int val) -{ -#ifdef HAL_PLATFORM_SDL - return hal_volume_write(val); -#else - int volume = -1; - cp0_signal_audio_api({"VolumeWrite", std::to_string(val)}, [&](int code, std::string data) { - if (code == 0) { - volume = std::atoi(data.c_str()); - } - }); - return volume; -#endif -} - -} diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 5f81a498..8d8c9dc5 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -6,11 +6,13 @@ #include "UILaunchPage.h" -#include "APPLaunch_api.h" #include "Launch.h" +#include "hal_lvgl_bsp.h" #include "lvgl/src/widgets/gif/lv_gif.h" #include "sample_log.h" #include "compat/input_keys.h" +#include +#include #include #include "Animation/ui_launcher_animation.h" @@ -141,19 +143,14 @@ static const CarouselSlot CAROUSEL_SLOTS[] = { // audio // ============================================================ -static void audio_play_ui_asset(const char *name) -{ - APPLaunch_system_play_asset(name); -} - static void audio_play_switch(void) { - audio_play_ui_asset("switch.wav"); + cp0_signal_audio_api({"SystemSoundPlay", "1"}, nullptr); } static void audio_play_enter(void) { - audio_play_ui_asset("enter.wav"); + cp0_signal_audio_api({"SystemSoundPlay", "2"}, nullptr); } // ============================================================ @@ -317,7 +314,7 @@ void UILaunchPage::show_home_screen() void UILaunchPage::load_home_screen() { show_home_screen(); - APPLaunch_audio_play_asset("startup.mp3"); + cp0_signal_audio_api({"SystemSoundPlay", "0"}, nullptr); } void UILaunchPage::start_startup_gif() @@ -419,33 +416,12 @@ void UILaunchPage::finish_switch_animation() lv_obj_set_style_text_font(carousel_elements_[i], launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD), LV_PART_MAIN | LV_STATE_DEFAULT); } - run_pending_switch(); -} - -void UILaunchPage::run_pending_switch() -{ - PendingSwitch pending = pending_switch_; - pending_switch_ = PendingSwitch::None; - - switch (pending) { - case PendingSwitch::Left: - switch_left(); - break; - case PendingSwitch::Right: - switch_right(); - break; - case PendingSwitch::None: - break; - } } void UILaunchPage::switch_right() { if (is_animating_) - { - pending_switch_ = PendingSwitch::Right; return; - } is_animating_ = true; @@ -477,10 +453,7 @@ void UILaunchPage::switch_right() void UILaunchPage::switch_left() { if (is_animating_) - { - pending_switch_ = PendingSwitch::Left; return; - } is_animating_ = true; @@ -538,7 +511,7 @@ void UILaunchPage::handle_home_key(lv_event_t *event) case KEY_LEFT: { - if (!lvping_lock_) + if (!lvping_lock_ && !is_animating_) { audio_play_switch(); switch_right(); @@ -548,7 +521,7 @@ void UILaunchPage::handle_home_key(lv_event_t *event) case KEY_RIGHT: { - if (!lvping_lock_) + if (!lvping_lock_ && !is_animating_) { audio_play_switch(); switch_left(); diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index b344d5d2..afdec8fe 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -56,19 +56,12 @@ class UILaunchPage : public home_base std::array carousel_elements_ = {}; private: - enum class PendingSwitch { - None, - Left, - Right, - }; - void create_app_container(lv_obj_t *parent); void switch_left(); void switch_right(); void fill_left_entering_slot(lv_obj_t *panel, lv_obj_t *label); void fill_right_entering_slot(lv_obj_t *panel, lv_obj_t *label); void finish_switch_animation(); - void run_pending_switch(); void handle_home_key(lv_event_t *event); void handle_startup_gif_event(lv_event_t *event); @@ -92,6 +85,5 @@ class UILaunchPage : public home_base bool is_animating_ = false; bool startup_gif_done_ = false; int lvping_lock_ = 0; - PendingSwitch pending_switch_ = PendingSwitch::None; int switch_current_pos_ = kPageDot2; }; diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp index 71592714..d2105ad5 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp @@ -27,7 +27,6 @@ #include #endif #include -#include "APPLaunch_api.h" #include "cp0_lvgl_app.h" #include "hal_lvgl_bsp.h" #include "../AppRegistry.h" @@ -116,7 +115,7 @@ class UISetupPage : public AppPage std::string snd_enter_; std::string snd_back_; - void play_enter() { play_audio_file(snd_enter_); } + void play_enter() { cp0_signal_audio_api({"SystemSoundPlay", "2"}, nullptr); } void play_back() { play_audio_file(snd_back_); } static int config_get_int(const char *key, int default_val) @@ -139,6 +138,26 @@ class UISetupPage : public AppPage cp0_signal_config_api({"Save"}, nullptr); } + static int audio_volume_read() + { + int volume = -1; + cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) { + if (code == 0) + volume = std::atoi(data.c_str()); + }); + return volume; + } + + static int audio_volume_write(int val) + { + int volume = -1; + cp0_signal_audio_api({"VolumeWrite", std::to_string(val)}, [&](int code, std::string data) { + if (code == 0) + volume = std::atoi(data.c_str()); + }); + return volume; + } + void play_audio_file(const std::string &path) { if (!path.empty()) cp0_signal_audio_api({"PlayFile", path}, nullptr); @@ -364,8 +383,8 @@ class UISetupPage : public AppPage { val_title_ = "Volume"; val_options_ = {"100%", "75%", "50%", "25%", "0%"}; - vol_val_ = config_get_int("volume", APPLaunch_volume_read()); - int pct = vol_val_ * 100 / 63; + vol_val_ = config_get_int("volume", audio_volume_read()); + int pct = vol_val_; if (pct >= 87) val_sel_idx_ = 0; else if (pct >= 62) val_sel_idx_ = 1; else if (pct >= 37) val_sel_idx_ = 2; @@ -1065,8 +1084,8 @@ class UISetupPage : public AppPage void apply_volume() { int pcts[] = {100, 75, 50, 25, 0}; - int new_val = 63 * pcts[val_sel_idx_] / 100; - APPLaunch_volume_write(new_val); + int new_val = pcts[val_sel_idx_]; + audio_volume_write(new_val); config_set_int("volume", new_val); config_save(); } diff --git a/projects/APPLaunch/main/ui/ui_app_page.hpp b/projects/APPLaunch/main/ui/ui_app_page.hpp index b469bc6e..6aa75549 100644 --- a/projects/APPLaunch/main/ui/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/ui_app_page.hpp @@ -455,6 +455,7 @@ class AppTopBarRegion : virtual public AppPageRoot UI_bind_event(); update_datetime_status(); update_status_bar(); + update_battery_status(cp0_battery_read()); status_timer_ = lv_timer_create(app_status_timer_cb, 5000, this); } From dd7ad57614515ba743b3cbb4c093b0970be23ea7 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 16:30:01 +0800 Subject: [PATCH 56/70] fix: block cp0 event queue thread while idle Use an atomic run flag for the detached LVGL event thread and wait on the event queue before processing. This avoids spinning the queue loop when there are no pending events and makes CP0_C_EVENT_END stop signalling thread-safe. --- ext_components/cp0_lvgl/src/commount.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ext_components/cp0_lvgl/src/commount.cpp b/ext_components/cp0_lvgl/src/commount.cpp index 054bd194..63a18b85 100644 --- a/ext_components/cp0_lvgl/src/commount.cpp +++ b/ext_components/cp0_lvgl/src/commount.cpp @@ -3,6 +3,7 @@ #include "cp0_lvgl_app.h" #include "lvgl/lvgl.h" #include +#include #include #include @@ -11,15 +12,17 @@ #undef def_hal_fun eventpp::EventQueue)> cp0_task_queue; -int queue_run_flage = 1; +static std::atomic_bool queue_run_flage{true}; extern "C" void init_lvgl_event_cpp() { cp0_task_queue.appendListener(CP0_C_EVENT_END, [](const std::list args) - { queue_run_flage = 0; }); + { queue_run_flage = false; }); std::thread t([]() { - while (queue_run_flage) - cp0_task_queue.process(); }); + while (queue_run_flage.load()) { + cp0_task_queue.wait(); + cp0_task_queue.process(); + } }); t.detach(); } From 1f7d66c766303e7416e84aea0486a262c7e8da65 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 16:30:12 +0800 Subject: [PATCH 57/70] chore: update APPLaunch deploy target Point setup.ini at the current remote host and enable the post-deploy service restart command. --- projects/APPLaunch/setup.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/APPLaunch/setup.ini b/projects/APPLaunch/setup.ini index 5d0cae7b..f17392c5 100644 --- a/projects/APPLaunch/setup.ini +++ b/projects/APPLaunch/setup.ini @@ -1,9 +1,9 @@ [ssh] local_file_path = dist remote_file_path = /home/pi/dist -remote_host = 192.168.28.177 +remote_host = 192.168.28.181 remote_port = 22 username = pi password = pi ; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' -; after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' From d1c1c4f9717e2e8a19524acd6102b4be4ef22a23 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 16:32:19 +0800 Subject: [PATCH 58/70] fix: recover system sound cache after boot Align the CP0 default system sound table with the launcher indices: startup, switch, and enter. Allow individual sound files to fail without discarding successfully loaded slots, and retry cache initialization on playback if boot-time preload failed before PulseAudio was ready. --- .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index f42200f0..f3e46b7f 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -54,7 +54,7 @@ class AudioSystem bool system_play_inited_ = false; bool system_sounds_loaded_ = false; std::mutex system_play_mutex_; - std::array system_sound_names_ = {"startup.mp3", "key_back.wav", "key_back.wav"}; + std::array system_sound_names_ = {"startup.mp3", "switch.wav", "enter.wav"}; bool system_sound_enabled_ = true; static constexpr int kRecWaveformSize = 128; @@ -225,13 +225,18 @@ class AudioSystem ); if(result != MA_SUCCESS) { - unload_system_sounds_locked(); - return -3; + printf("load system sound failed: %s\n", path.c_str()); + continue; } system_sound_loaded_slots_[i] = true; } - system_sounds_loaded_ = true; + system_sounds_loaded_ = std::any_of( + system_sound_loaded_slots_.begin(), + system_sound_loaded_slots_.end(), + [](bool loaded) { return loaded; } + ); + if(!system_sounds_loaded_) return -3; return 0; } @@ -482,7 +487,7 @@ class AudioSystem { std::lock_guard lock(system_play_mutex_); - if(!system_sounds_loaded_) return; + if(!system_sounds_loaded_ && load_system_sounds_locked() != 0) return; for(size_t i = 0; i < system_sound_names_.size(); ++i) { @@ -506,7 +511,11 @@ class AudioSystem if(!system_sound_enabled_) return; std::lock_guard lock(system_play_mutex_); - if(!system_sounds_loaded_ || index >= system_sounds_.size() || !system_sound_loaded_slots_[index]) + if(!system_sounds_loaded_ && load_system_sounds_locked() != 0) + { + return; + } + if(index >= system_sounds_.size() || !system_sound_loaded_slots_[index]) { return; } From c70a52086afb40ca1fb27e6a1d410fde9327f95c Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 17:08:55 +0800 Subject: [PATCH 59/70] fix: retry launcher startup sound Make CP0 SystemSoundPlay report whether ma_sound_start actually succeeded instead of always returning success after a request. Add a launcher-side startup sound retry timer so first boot can recover when PulseAudio or the preloaded sound cache is not ready at the moment the home screen first loads. Clean up the startup retry timer in the launcher page destructor to avoid callbacks after the page is destroyed. Keep the home battery percentage label on the same default font path used by sub-app top bars, avoiding the previous 100-percent-only font switch. --- .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 20 ++++---- projects/APPLaunch/main/ui/UILaunchPage.cpp | 48 ++++++++++++++++++- projects/APPLaunch/main/ui/UILaunchPage.h | 5 ++ projects/APPLaunch/main/ui/ui_app_page.hpp | 5 +- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index f3e46b7f..5119cd5d 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -54,7 +54,7 @@ class AudioSystem bool system_play_inited_ = false; bool system_sounds_loaded_ = false; std::mutex system_play_mutex_; - std::array system_sound_names_ = {"startup.mp3", "switch.wav", "enter.wav"}; + std::array system_sound_names_ = {"startup.mp3", "key_back.wav", "key_back.wav"}; bool system_sound_enabled_ = true; static constexpr int kRecWaveformSize = 128; @@ -506,24 +506,24 @@ class AudioSystem if(!file.empty()) play(file); } - void system_play_index(size_t index) + bool system_play_index(size_t index) { - if(!system_sound_enabled_) return; + if(!system_sound_enabled_) return false; std::lock_guard lock(system_play_mutex_); if(!system_sounds_loaded_ && load_system_sounds_locked() != 0) { - return; + return false; } if(index >= system_sounds_.size() || !system_sound_loaded_slots_[index]) { - return; + return false; } ma_sound* sound = &system_sounds_[index]; - if(ma_sound_is_playing(sound)) return; - ma_sound_seek_to_pcm_frame(sound, 0); - ma_sound_start(sound); + if(ma_sound_is_playing(sound)) return true; + if(ma_sound_seek_to_pcm_frame(sound, 0) != MA_SUCCESS) return false; + return ma_sound_start(sound) == MA_SUCCESS; } void SetSystemSoundNames(arg_t arg, callback_t callback) @@ -560,8 +560,8 @@ class AudioSystem report(callback, 0, "system sound disabled\n"); return; } - system_play_index(static_cast(index)); - report(callback, 0, "system sound play\n"); + bool played = system_play_index(static_cast(index)); + report(callback, played ? 0 : -2, played ? "system sound play\n" : "system sound play failed\n"); } void SystemSoundEnable(arg_t arg, callback_t callback) diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/UILaunchPage.cpp index 8d8c9dc5..99a21daf 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/UILaunchPage.cpp @@ -115,6 +115,8 @@ namespace { UILaunchPage *active_launch_page = nullptr; lv_group_t *home_input_group = nullptr; +constexpr int kStartupSoundRetryMax = 10; +constexpr uint32_t kStartupSoundRetryMs = 500; // ==================== standard layout for carousel slots ==================== @@ -314,7 +316,35 @@ void UILaunchPage::show_home_screen() void UILaunchPage::load_home_screen() { show_home_screen(); - cp0_signal_audio_api({"SystemSoundPlay", "0"}, nullptr); + play_startup_sound_with_retry(); +} + +void UILaunchPage::play_startup_sound_with_retry() +{ + int play_result = -1; + cp0_signal_audio_api({"SystemSoundPlay", "0"}, [&](int code, std::string) { + play_result = code; + }); + + if (play_result == 0) { + stop_startup_sound_timer(); + startup_sound_retry_count_ = 0; + return; + } + + if (startup_sound_timer_) + return; + + startup_sound_retry_count_ = 0; + startup_sound_timer_ = lv_timer_create(startup_sound_timer_cb, kStartupSoundRetryMs, this); +} + +void UILaunchPage::stop_startup_sound_timer() +{ + if (startup_sound_timer_) { + lv_timer_delete(startup_sound_timer_); + startup_sound_timer_ = nullptr; + } } void UILaunchPage::start_startup_gif() @@ -336,6 +366,7 @@ UILaunchPage::UILaunchPage(Launch *launch) UILaunchPage::~UILaunchPage() { + stop_startup_sound_timer(); if (green_bg_) { lv_obj_del(green_bg_); green_bg_ = nullptr; @@ -609,6 +640,21 @@ void UILaunchPage::on_startup_gif_event(lv_event_t *event) self->handle_startup_gif_event(event); } +void UILaunchPage::startup_sound_timer_cb(lv_timer_t *timer) +{ + UILaunchPage *self = static_cast(lv_timer_get_user_data(timer)); + if (!self) + return; + + ++self->startup_sound_retry_count_; + if (self->startup_sound_retry_count_ > kStartupSoundRetryMax) { + self->stop_startup_sound_timer(); + return; + } + + self->play_startup_sound_with_retry(); +} + void UILaunchPage::create_screen() { if (carousel_elements_[kCardCenter]) diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/UILaunchPage.h index afdec8fe..ef3edf79 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.h +++ b/projects/APPLaunch/main/ui/UILaunchPage.h @@ -64,6 +64,8 @@ class UILaunchPage : public home_base void finish_switch_animation(); void handle_home_key(lv_event_t *event); void handle_startup_gif_event(lv_event_t *event); + void play_startup_sound_with_retry(); + void stop_startup_sound_timer(); void rotate_carousel_left(size_t start, size_t end); void rotate_carousel_right(size_t start, size_t end); @@ -75,15 +77,18 @@ class UILaunchPage : public home_base static void on_app_clicked(lv_event_t *event); static void on_home_key(lv_event_t *event); static void on_startup_gif_event(lv_event_t *event); + static void startup_sound_timer_cb(lv_timer_t *timer); Launch *launch_ = nullptr; lv_obj_t *startup_gif_ = nullptr; lv_obj_t *left_arrow_button_ = nullptr; lv_obj_t *right_arrow_button_ = nullptr; lv_obj_t *green_bg_ = nullptr; + lv_timer_t *startup_sound_timer_ = nullptr; std::array startup_gif_path_ = {}; bool is_animating_ = false; bool startup_gif_done_ = false; + int startup_sound_retry_count_ = 0; int lvping_lock_ = 0; int switch_current_pos_ = kPageDot2; }; diff --git a/projects/APPLaunch/main/ui/ui_app_page.hpp b/projects/APPLaunch/main/ui/ui_app_page.hpp index 6aa75549..a507da53 100644 --- a/projects/APPLaunch/main/ui/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/ui_app_page.hpp @@ -675,10 +675,7 @@ class home_base : public AppPageRoot char pwr_buf[16]; snprintf(pwr_buf, sizeof(pwr_buf), "%d%%", soc); lv_label_set_text(ui_TOP_power_Label, pwr_buf); - if (soc == 100) - lv_obj_set_style_text_font(ui_TOP_power_Label, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - else - lv_obj_set_style_text_font(ui_TOP_power_Label, LV_FONT_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(ui_TOP_power_Label, LV_FONT_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); uint32_t color = 0x66CC33; if (soc <= 20) From 8c0f8da2e0cfb8b4830526dbfd51516594116f7e Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 17:40:49 +0800 Subject: [PATCH 60/70] Fix screenshot hint directory creation on Windows Port the effective master screenshot hint fix to the current APPLaunch framework without merging master directly. Use _mkdir() behind _WIN32 while keeping the existing cp0_signal_screenshot_api screenshot backend and the Linux ownership fixup path. --- projects/APPLaunch/main/ui/ui_global_hint.cpp | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index a5600532..3d832a7e 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -41,7 +41,11 @@ #include #include #include +#ifdef _WIN32 +#include +#else #include +#endif /* KEY_RIGHTSHIFT / KEY_COMPOSE exist in but the * project's non-Linux compat/input_keys.h does not define them. @@ -196,6 +200,26 @@ static void show_hint(const char *text) } } +static void ensure_screenshot_dir(const char *scr_dir) +{ +#ifdef _WIN32 + _mkdir(scr_dir); +#else + /* Ensure dir exists with correct ownership (real uid/gid, not root). */ + mkdir(scr_dir, 0755); + if (getuid() == 0) { + /* Running as root via systemd — chown to the login user. */ + uid_t uid = 1000; + gid_t gid = 1000; + const char *sudo_uid = getenv("SUDO_UID"); + const char *sudo_gid = getenv("SUDO_GID"); + if (sudo_uid) uid = (uid_t)atoi(sudo_uid); + if (sudo_gid) gid = (gid_t)atoi(sudo_gid); + chown(scr_dir, uid, gid); + } +#endif +} + namespace ui_global_hint { void on_key(const struct key_item *elm) @@ -251,17 +275,7 @@ void on_key(const struct key_item *elm) const char *home = getenv("HOME"); char scr_dir[256]; snprintf(scr_dir, sizeof(scr_dir), "%s/Screenshots", home ? home : "/tmp"); - /* Ensure dir exists with correct ownership (real uid/gid, not root) */ - mkdir(scr_dir, 0755); - if (getuid() == 0) { - /* Running as root via systemd — chown to the login user */ - uid_t uid = 1000; gid_t gid = 1000; - const char *sudo_uid = getenv("SUDO_UID"); - const char *sudo_gid = getenv("SUDO_GID"); - if (sudo_uid) uid = (uid_t)atoi(sudo_uid); - if (sudo_gid) gid = (gid_t)atoi(sudo_gid); - chown(scr_dir, uid, gid); - } + ensure_screenshot_dir(scr_dir); int ret = -1; cp0_signal_screenshot_api({"Save", scr_dir}, [&](int code, std::string) { ret = code; From 92934794f221acc4a1a675641dd8de35784991db Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 17:53:30 +0800 Subject: [PATCH 61/70] Normalize APPLaunch UI source naming and comments Rename APPLaunch UI source files and animation paths to snake_case names that match their responsibilities. Translate remaining project source comments to English and remove stale commented-out debug/demo code from APPLaunch, cp0_lvgl, and UserDemo. --- .../cp0_lvgl/include/hal_lvgl_bsp.h | 1 - .../cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp | 4 ++-- .../cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c | 1 - .../Animation.hpp => animation/animation.hpp} | 0 .../ui_launcher_animation.cpp | 2 +- .../ui_launcher_animation.h | 0 .../ui/{AppRegistry.cpp => app_registry.cpp} | 2 +- .../main/ui/{AppRegistry.h => app_registry.h} | 0 .../main/ui/{Launch.cpp => launch.cpp} | 6 +++--- .../APPLaunch/main/ui/{Launch.h => launch.h} | 0 ...ro_lvgl_os.cpp => launcher_ui_runtime.cpp} | 18 ++++++++--------- .../{zero_lvgl_os.h => launcher_ui_runtime.h} | 6 +++--- .../main/ui/page_app/ui_app_compass.hpp | 14 ++++++------- ...ui_app_IpPanel.hpp => ui_app_ip_panel.hpp} | 0 .../APPLaunch/main/ui/page_app/ui_app_rec.hpp | 6 ------ .../main/ui/page_app/ui_app_setup.hpp | 2 +- projects/APPLaunch/main/ui/ui.cpp | 4 ++-- projects/APPLaunch/main/ui/ui.h | 6 +++--- projects/APPLaunch/main/ui/ui_app_page.hpp | 1 - .../{UILaunchPage.cpp => ui_launch_page.cpp} | 6 +++--- .../ui/{UILaunchPage.h => ui_launch_page.h} | 0 projects/UserDemo/main/SConstruct | 4 ++-- projects/UserDemo/main/src/main.cpp | 20 +------------------ 23 files changed, 38 insertions(+), 65 deletions(-) rename projects/APPLaunch/main/ui/{Animation/Animation.hpp => animation/animation.hpp} (100%) rename projects/APPLaunch/main/ui/{Animation => animation}/ui_launcher_animation.cpp (99%) rename projects/APPLaunch/main/ui/{Animation => animation}/ui_launcher_animation.h (100%) rename projects/APPLaunch/main/ui/{AppRegistry.cpp => app_registry.cpp} (98%) rename projects/APPLaunch/main/ui/{AppRegistry.h => app_registry.h} (100%) rename projects/APPLaunch/main/ui/{Launch.cpp => launch.cpp} (99%) rename projects/APPLaunch/main/ui/{Launch.h => launch.h} (100%) rename projects/APPLaunch/main/ui/{zero_lvgl_os.cpp => launcher_ui_runtime.cpp} (77%) rename projects/APPLaunch/main/ui/{zero_lvgl_os.h => launcher_ui_runtime.h} (88%) rename projects/APPLaunch/main/ui/page_app/{ui_app_IpPanel.hpp => ui_app_ip_panel.hpp} (100%) rename projects/APPLaunch/main/ui/{UILaunchPage.cpp => ui_launch_page.cpp} (99%) rename projects/APPLaunch/main/ui/{UILaunchPage.h => ui_launch_page.h} (100%) diff --git a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h index 850f26a4..be8a37b4 100644 --- a/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h +++ b/ext_components/cp0_lvgl/include/hal_lvgl_bsp.h @@ -30,7 +30,6 @@ void cp0_lvgl_init(void); const char *hal_path_audio_dir(void); #ifdef __cplusplus } -// #include #include "eventpp/callbacklist.h" #include "eventpp/eventqueue.h" #include diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp index 5119cd5d..85c466ad 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp @@ -617,8 +617,8 @@ class AudioSystem stop_play_device(false); } } - // 录音的过程控制:开始,暂停,恢复播放,结束保存。 - // 播放的过程控制:开始,暂停,恢复播放,播放结束。 + // Recording control: start, pause, resume, and stop with save. + // Playback control: start, pause, resume, and stop. void PlayFile(arg_t arg, callback_t callback) { std::string file = resolve_play_file(first_arg_after_command(arg), false); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c index 6fcf8137..b8e29e57 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c @@ -1,4 +1,3 @@ -// cp0_lvgl_keyboard.c #define _GNU_SOURCE #include #include diff --git a/projects/APPLaunch/main/ui/Animation/Animation.hpp b/projects/APPLaunch/main/ui/animation/animation.hpp similarity index 100% rename from projects/APPLaunch/main/ui/Animation/Animation.hpp rename to projects/APPLaunch/main/ui/animation/animation.hpp diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp b/projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp similarity index 99% rename from projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp rename to projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp index c6d34ef8..a177acfa 100644 --- a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp +++ b/projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp @@ -6,7 +6,7 @@ #include "ui_launcher_animation.h" -#include "Animation.hpp" +#include "animation.hpp" #include diff --git a/projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h b/projects/APPLaunch/main/ui/animation/ui_launcher_animation.h similarity index 100% rename from projects/APPLaunch/main/ui/Animation/ui_launcher_animation.h rename to projects/APPLaunch/main/ui/animation/ui_launcher_animation.h diff --git a/projects/APPLaunch/main/ui/AppRegistry.cpp b/projects/APPLaunch/main/ui/app_registry.cpp similarity index 98% rename from projects/APPLaunch/main/ui/AppRegistry.cpp rename to projects/APPLaunch/main/ui/app_registry.cpp index 352b2c84..496aca01 100644 --- a/projects/APPLaunch/main/ui/AppRegistry.cpp +++ b/projects/APPLaunch/main/ui/app_registry.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -#include "AppRegistry.h" +#include "app_registry.h" #include "cp0_lvgl_app.h" #include "hal_lvgl_bsp.h" diff --git a/projects/APPLaunch/main/ui/AppRegistry.h b/projects/APPLaunch/main/ui/app_registry.h similarity index 100% rename from projects/APPLaunch/main/ui/AppRegistry.h rename to projects/APPLaunch/main/ui/app_registry.h diff --git a/projects/APPLaunch/main/ui/Launch.cpp b/projects/APPLaunch/main/ui/launch.cpp similarity index 99% rename from projects/APPLaunch/main/ui/Launch.cpp rename to projects/APPLaunch/main/ui/launch.cpp index da736290..a6f76f10 100644 --- a/projects/APPLaunch/main/ui/Launch.cpp +++ b/projects/APPLaunch/main/ui/launch.cpp @@ -4,11 +4,11 @@ * SPDX-License-Identifier: MIT */ -#include "Launch.h" +#include "launch.h" -#include "AppRegistry.h" +#include "app_registry.h" #include "ui.h" -#include "UILaunchPage.h" +#include "ui_launch_page.h" #include "ui_loading.h" #include "generated/page_app.h" #include "cp0_lvgl_app.h" diff --git a/projects/APPLaunch/main/ui/Launch.h b/projects/APPLaunch/main/ui/launch.h similarity index 100% rename from projects/APPLaunch/main/ui/Launch.h rename to projects/APPLaunch/main/ui/launch.h diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp b/projects/APPLaunch/main/ui/launcher_ui_runtime.cpp similarity index 77% rename from projects/APPLaunch/main/ui/zero_lvgl_os.cpp rename to projects/APPLaunch/main/ui/launcher_ui_runtime.cpp index f23ebb9d..4c0aea42 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.cpp +++ b/projects/APPLaunch/main/ui/launcher_ui_runtime.cpp @@ -4,14 +4,14 @@ * SPDX-License-Identifier: MIT */ -#include "zero_lvgl_os.h" +#include "launcher_ui_runtime.h" -#include "Launch.h" -#include "UILaunchPage.h" +#include "launch.h" +#include "ui_launch_page.h" #include -void zero_lvgl_os::create_display() +void LauncherUiRuntime::create_display() { fonts_ = std::make_shared(); @@ -21,14 +21,14 @@ void zero_lvgl_os::create_display() lv_disp_set_theme(dispp_, theme_); } -void zero_lvgl_os::build_launcher_home() +void LauncherUiRuntime::build_launcher_home() { launch_page_->create_screen(); launch_->bind_ui(); launch_page_->init_input_group(); } -void zero_lvgl_os::show_initial_screen() +void LauncherUiRuntime::show_initial_screen() { #ifndef APPLAUNCH_STARTUP_ANIMATION launch_page_->load_home_screen(); @@ -48,7 +48,7 @@ void zero_lvgl_os::show_initial_screen() #endif } -zero_lvgl_os::zero_lvgl_os() +LauncherUiRuntime::LauncherUiRuntime() { create_display(); @@ -57,9 +57,9 @@ zero_lvgl_os::zero_lvgl_os() launch_->set_launch_page(launch_page_); } -zero_lvgl_os::~zero_lvgl_os() = default; +LauncherUiRuntime::~LauncherUiRuntime() = default; -void zero_lvgl_os::start() +void LauncherUiRuntime::start() { build_launcher_home(); show_initial_screen(); diff --git a/projects/APPLaunch/main/ui/zero_lvgl_os.h b/projects/APPLaunch/main/ui/launcher_ui_runtime.h similarity index 88% rename from projects/APPLaunch/main/ui/zero_lvgl_os.h rename to projects/APPLaunch/main/ui/launcher_ui_runtime.h index a7a4b7fa..34a57cc6 100644 --- a/projects/APPLaunch/main/ui/zero_lvgl_os.h +++ b/projects/APPLaunch/main/ui/launcher_ui_runtime.h @@ -13,11 +13,11 @@ class Launch; class UILaunchPage; class LauncherFonts; -class zero_lvgl_os +class LauncherUiRuntime { public: - zero_lvgl_os(); - ~zero_lvgl_os(); + LauncherUiRuntime(); + ~LauncherUiRuntime(); void start(); diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp index 2505a90a..87ab70cf 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp @@ -27,9 +27,9 @@ * Compass + IMU dashboard * Screen: 320 x 170 * - * 按键: - * F4 预留:校准接口 - * F6/ESC 返回主页 + * Keys: + * F4 Reserved for the calibration interface + * F6/ESC Return home * ============================================================ */ class UICompassPage : public AppPageRoot @@ -121,7 +121,7 @@ class UICompassPage : public AppPageRoot /* * ============================================================ - * UI 构建 + * UI construction * ============================================================ */ void creat_UI() @@ -349,7 +349,7 @@ class UICompassPage : public AppPageRoot /* * ============================================================ - * UI 状态刷新 + * UI state refresh * ============================================================ */ void update_from_state(const CompassUiState& state) @@ -437,7 +437,7 @@ class UICompassPage : public AppPageRoot private: /* * ============================================================ - * 按键事件 + * Key events * ============================================================ */ void event_handler_init() @@ -465,7 +465,7 @@ class UICompassPage : public AppPageRoot { switch (key) { case KEY_F4: - // TODO(compass): 接入接口后触发磁力计/IMU 校准。 + // TODO(compass): Trigger magnetometer/IMU calibration after the API is available. break; case KEY_F6: diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_ip_panel.hpp similarity index 100% rename from projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp rename to projects/APPLaunch/main/ui/page_app/ui_app_ip_panel.hpp diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp index 14333d13..4b9c9f61 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp @@ -297,12 +297,6 @@ class rec_page : public AppPage { lv_obj_add_flag(but[i], LV_OBJ_FLAG_CLICKABLE); } - // lvgl_add_call(but[0], [](lv_event_code_t c, void *d){ - // if(c == LV_EVENT_CLICKED) - // { - // SLOGI("butt will be clicked"); - // } - // }, NULL); } }; diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp index d2105ad5..7f18347f 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp @@ -29,7 +29,7 @@ #include #include "cp0_lvgl_app.h" #include "hal_lvgl_bsp.h" -#include "../AppRegistry.h" +#include "../app_registry.h" // ============================================================ // System settings screen UISetupPage (Carousel Design) diff --git a/projects/APPLaunch/main/ui/ui.cpp b/projects/APPLaunch/main/ui/ui.cpp index 357f20a9..c7875f2f 100644 --- a/projects/APPLaunch/main/ui/ui.cpp +++ b/projects/APPLaunch/main/ui/ui.cpp @@ -25,13 +25,13 @@ #error "LV_COLOR_16_SWAP should be 0 to match SquareLine Studio's settings" #endif -std::unique_ptr home; +std::unique_ptr home; namespace launcher_ui { void init() { - home = std::make_unique(); + home = std::make_unique(); home->start(); } diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 9bb2a9d4..90985c93 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -98,9 +98,9 @@ class LauncherFonts }; LauncherFonts &launcher_fonts(); -#include "Launch.h" -#include "UILaunchPage.h" -#include "zero_lvgl_os.h" +#include "launch.h" +#include "ui_launch_page.h" +#include "launcher_ui_runtime.h" #endif #endif diff --git a/projects/APPLaunch/main/ui/ui_app_page.hpp b/projects/APPLaunch/main/ui/ui_app_page.hpp index a507da53..cf18066b 100644 --- a/projects/APPLaunch/main/ui/ui_app_page.hpp +++ b/projects/APPLaunch/main/ui/ui_app_page.hpp @@ -440,7 +440,6 @@ class AppPageRoot { input_group_ = lv_group_create(); lv_group_add_obj(input_group_, root_screen_); - // lv_group_focus_obj(root_screen_); } }; diff --git a/projects/APPLaunch/main/ui/UILaunchPage.cpp b/projects/APPLaunch/main/ui/ui_launch_page.cpp similarity index 99% rename from projects/APPLaunch/main/ui/UILaunchPage.cpp rename to projects/APPLaunch/main/ui/ui_launch_page.cpp index 99a21daf..43a4e476 100644 --- a/projects/APPLaunch/main/ui/UILaunchPage.cpp +++ b/projects/APPLaunch/main/ui/ui_launch_page.cpp @@ -4,9 +4,9 @@ * SPDX-License-Identifier: MIT */ -#include "UILaunchPage.h" +#include "ui_launch_page.h" -#include "Launch.h" +#include "launch.h" #include "hal_lvgl_bsp.h" #include "lvgl/src/widgets/gif/lv_gif.h" #include "sample_log.h" @@ -15,7 +15,7 @@ #include #include -#include "Animation/ui_launcher_animation.h" +#include "animation/ui_launcher_animation.h" #include #include diff --git a/projects/APPLaunch/main/ui/UILaunchPage.h b/projects/APPLaunch/main/ui/ui_launch_page.h similarity index 100% rename from projects/APPLaunch/main/ui/UILaunchPage.h rename to projects/APPLaunch/main/ui/ui_launch_page.h diff --git a/projects/UserDemo/main/SConstruct b/projects/UserDemo/main/SConstruct index ac746d66..6afa1723 100644 --- a/projects/UserDemo/main/SConstruct +++ b/projects/UserDemo/main/SConstruct @@ -17,12 +17,12 @@ LDFLAGS = [] LINK_SEARCH_PATH = [] STATIC_FILES = [] -# 添加ui目录 +# Add the UI directory. SRCS += append_srcs_dir(ADir("ui")) INCLUDE += ["ui"] -# 为 lvgl_component 添加 SDL2 依赖 +# Add the SDL2 dependency to lvgl_component. lvgl_component = list( filter(lambda x: x["target"] == "lvgl_component", env["COMPONENTS"]) )[0] diff --git a/projects/UserDemo/main/src/main.cpp b/projects/UserDemo/main/src/main.cpp index 7fcfbf4e..5b84ad76 100644 --- a/projects/UserDemo/main/src/main.cpp +++ b/projects/UserDemo/main/src/main.cpp @@ -30,7 +30,7 @@ int get_st7789v_fbdev(char *dev_path, size_t buf_size) char line[256]; int fb_num = -1; - /* 逐行读取,查找包含 fb_st7789v 的行,格式如:0 fb_st7789v */ + /* Read /proc/fb line by line and find the fb_st7789v entry, e.g. "0 fb_st7789v". */ while (fgets(line, sizeof(line), fp) != NULL) { if (strstr(line, "fb_st7789v") != NULL) @@ -60,27 +60,16 @@ int get_st7789v_fbdev(char *dev_path, size_t buf_size) static void lv_linux_indev_init(void) { const char *mouse_device = getenv_default("LV_LINUX_MOUSE_DEVICE", NULL); - const char *keyboard_device = getenv_default("LV_LINUX_KEYBOARD_DEVICE", "/dev/input/by-path/platform-3f804000.i2c-event"); - const char *keyboard_map = getenv_default("LV_LINUX_KEYBOARD_MAP", "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"); - // /home/nihao/w2T/github/m5stack-linux-dtoverlays/modules/tca8418-1.0/tca8418_keypad_m5stack_keymap.map - lv_indev_t *touch = NULL; if (mouse_device) touch = lv_evdev_create(LV_INDEV_TYPE_POINTER, mouse_device); - - lv_indev_t *keyboard = NULL; - // if (keyboard_device) - // keyboard = tca8418_keypad_init(keyboard_device, keyboard_map); - // if (keyboard_device) - // keyboard = lv_evdev_create(LV_INDEV_TYPE_KEYPAD, keyboard_device); } #endif #if LV_USE_LINUX_FBDEV static void lv_linux_disp_init(void) { - // export LV_LINUX_FBDEV_DEVICE="/dev/fb$(grep 'fb_st7789v' /proc/fb | awk '{print $1}')" const char *device = NULL; char fbdev[64] = {0}; device = getenv_default("LV_LINUX_FBDEV_DEVICE", NULL); @@ -98,7 +87,6 @@ static void lv_linux_disp_init(void) lv_linux_fbdev_set_file(disp, device); - // 打印获取到的分辨率 lv_coord_t w = lv_display_get_horizontal_resolution(disp); lv_coord_t h = lv_display_get_vertical_resolution(disp); printf("Framebuffer resolution: %dx%d\n", w, h); @@ -145,13 +133,7 @@ int main(void) lv_linux_disp_init(); lv_linux_indev_init(); - /*Create a Demo*/ - // lv_demo_widgets(); - // lv_demo_widgets_start_slideshow(); - // lv_demo_music(); - ui_init(); - // lv_demo_widgets(); // 用LVGL自带demo测试 /*Handle LVGL tasks*/ printf("Entering main loop...\n"); while (1) From e5dab61413768be02f7e13bafbc1e6349d180662 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 17:54:50 +0800 Subject: [PATCH 62/70] Install APPLaunch as a user systemd service Move the packaged service into the user systemd unit directory and manage it through systemctl --user for the UID 1000 account. Enable linger, start user@1000.service, and wire postinst/prerm to enable, restart, stop, and disable the user service with PipeWire Pulse ordering. --- scripts/debian_packager.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/debian_packager.py b/scripts/debian_packager.py index 1792b78a..7d9ba387 100755 --- a/scripts/debian_packager.py +++ b/scripts/debian_packager.py @@ -30,7 +30,7 @@ DEFAULT_ARCHITECTURE = "arm64" INSTALL_PARENT = PurePosixPath("usr/share") -SERVICE_PATH = PurePosixPath("lib/systemd/system") +SERVICE_PATH = PurePosixPath("usr/lib/systemd/user") OPTIONAL_BINARIES = ( "M5CardputerZero-AppStore", @@ -199,25 +199,39 @@ def _control_text(config: PackageConfig) -> str: def _postinst_text(config: PackageConfig) -> str: + service_file = f"/{_posix_path(config.service_path / f'{config.app_name}.service')}" return f"""#!/bin/sh set -e mkdir -p /var/cache/{config.app_name} ln -sfn /var/cache/{config.app_name} /usr/share/{config.app_name}/cache -if command -v systemctl >/dev/null 2>&1 && [ -f "/lib/systemd/system/{config.app_name}.service" ]; then +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +if command -v systemctl >/dev/null 2>&1 && [ -n "$APP_USER" ] && [ -f "{service_file}" ]; then + if command -v loginctl >/dev/null 2>&1; then + loginctl enable-linger "$APP_USER" || true + fi systemctl daemon-reload || true - systemctl enable {config.app_name}.service || true - systemctl restart {config.app_name}.service || systemctl start {config.app_name}.service || true + systemctl start "user@$APP_UID.service" || true + runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" systemctl --user daemon-reload || true + runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" systemctl --user enable {config.app_name}.service || true + runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" systemctl --user restart {config.app_name}.service || \ + runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" systemctl --user start {config.app_name}.service || true +else + echo "{config.app_name}: UID 1000 user not found or service file missing; skip user service enable/start" >&2 fi exit 0 """ def _prerm_text(config: PackageConfig) -> str: + service_file = f"/{_posix_path(config.service_path / f'{config.app_name}.service')}" return f"""#!/bin/sh set -e -if command -v systemctl >/dev/null 2>&1 && [ -f "/lib/systemd/system/{config.app_name}.service" ]; then - systemctl stop {config.app_name}.service || true - systemctl disable {config.app_name}.service || true +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +if command -v systemctl >/dev/null 2>&1 && [ -n "$APP_USER" ] && [ -f "{service_file}" ]; then + runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" systemctl --user stop {config.app_name}.service || true + runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" systemctl --user disable {config.app_name}.service || true fi rm -rf /var/cache/{config.app_name} exit 0 @@ -227,7 +241,8 @@ def _prerm_text(config: PackageConfig) -> str: def _service_text(config: PackageConfig) -> str: return f"""[Unit] Description={config.app_name} Service -After=multi-user.target +After=pipewire-pulse.service +Wants=pipewire-pulse.service [Service] ExecStart=/{_posix_path(config.bin_path / config.bin_name)} @@ -237,7 +252,7 @@ def _service_text(config: PackageConfig) -> str: StartLimitInterval=0 [Install] -WantedBy=multi-user.target +WantedBy=default.target """ From 7349fd97342af4d98c7690228d52439e01533143 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 17:55:46 +0800 Subject: [PATCH 63/70] Remove stale APPLaunch architecture notes Delete outdated APPLaunch architecture and cp0_lvgl platform decoupling notes that no longer match the current launcher structure. --- .../APPLaunch/docs/architecture_review.md | 397 ------------------ .../docs/cp0_lvgl_platform_decoupling_plan.md | 242 ----------- 2 files changed, 639 deletions(-) delete mode 100644 projects/APPLaunch/docs/architecture_review.md delete mode 100644 projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md diff --git a/projects/APPLaunch/docs/architecture_review.md b/projects/APPLaunch/docs/architecture_review.md deleted file mode 100644 index 1f87282a..00000000 --- a/projects/APPLaunch/docs/architecture_review.md +++ /dev/null @@ -1,397 +0,0 @@ -# APPLaunch 架构审查报告 - -日期:2026-06-15 -范围:`projects/APPLaunch` -方式:主线程梳理 + 3 个并行 agent 分别审查 UI/应用层、平台/构建层、质量/生命周期。 - -## 1. 总体判断 - -APPLaunch 当前更像一个快速演进后的产品型单体:功能能跑起来,但应用注册、平台能力、页面 UI、硬件访问、构建适配、资源路径之间互相穿透。 - -最主要的不和谐不是单点代码风格问题,而是三条架构边界不清: - -1. **应用模型边界不清**:Launcher 列表、Settings 开关、动态 `.desktop` 应用、页面工厂、资源图标各自维护状态。 -2. **平台能力边界不清**:UI 页面中直接出现 `system()`、`popen()`、`/dev`、`/sys`、GPIO/SPI/I2C、平台宏和硬编码 Linux 路径。 -3. **生命周期边界不清**:LVGL 对象、timer、watcher、PTY、动画、页面对象多处裸持有,部分清理依赖路径或进程退出。 - -建议优先修复 P0/P1 的一致性、安全和生命周期问题,再做大规模模块拆分。 - -## 2. 代码结构概览 - -- `projects/APPLaunch/SConstruct`:项目级构建入口,选择默认配置、SDK 路径、交叉编译 static lib/sysroot 等。 -- `projects/APPLaunch/main/SConstruct`:组件构建入口,运行 `main/ui/generate_page_app_includes.py`,编译 `src/*.c*` 和 `ui` 目录。 -- `projects/APPLaunch/main/src/main.cpp`:运行时入口,初始化 LVGL/CP0,注册键盘事件,进入 `APPLaunch_lock()` + `lv_timer_handler()` 主循环。 -- `projects/APPLaunch/main/ui/ui.cpp`:全局 UI 根对象 `std::unique_ptr home`,`launcher_ui::init()` 创建并启动。 -- `projects/APPLaunch/main/ui/zero_lvgl_os.cpp`:应用层组装器,创建主题、字体缓存、`Launch`、`UILaunchPage`,决定启动 GIF 或首页。 -- `projects/APPLaunch/main/ui/Launch.*`:应用列表、应用启动、`.desktop` 扫描、目录 watch、回主页逻辑。 -- `projects/APPLaunch/main/ui/UILaunchPage.*`:首页 UI、轮播控件、键盘/点击事件、启动动画、页面切换动画。 -- `projects/APPLaunch/main/ui/ui_app_page.hpp`:应用页基类和通用 top/content/bottom layout。 -- `projects/APPLaunch/main/ui/page_app/*.hpp`:内置页面,多数是 header-only 千行级实现。 -- `projects/APPLaunch/APPLaunch/share`:运行时资源目录,包含 images/audio/font。 -- `projects/APPLaunch/dist.bak`:备份资源/产物副本,体积和资源来源容易造成歧义。 - -## 3. P0 高优先级问题 - -### 3.1 应用模型不一致 - -- `app::Exec` 字段在 `projects/APPLaunch/main/ui/Launch.h` 中声明,但构造函数没有赋值。 -- `projects/APPLaunch/main/ui/Launch.cpp` 中动态应用去重依赖 `it.Exec == app_exec`,但 `Exec` 实际为空或默认值,导致去重逻辑基本失效。 -- `Exec` 被移动进 lambda 捕获后没有保存在模型对象中,后续无法用于诊断、去重、刷新、权限检查。 - -建议:建立明确的 `AppDescriptor`,至少包含: - -```cpp -name -icon -exec -launch_type // InternalPage / Terminal / ExternalProcess -register_app // appending function/factory for built-in apps -source // Builtin / DesktopFile / Store -config_key -default_enabled -always_on -required_features -``` - -### 3.2 Settings 与 Launcher 列表不一致 - -- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` 维护一份 `app_keys/app_labels/always_on`。 -- `projects/APPLaunch/main/ui/Launch.cpp` 维护另一份实际 Launcher app 列表。 -- Settings 中存在 `Music`,但 Launcher 中没有对应入口。 -- `Compass` 在 Launcher 中存在,但没有统一受配置控制。 -- `Python/Store/CLI/Game/Setting` 的固定/可配置语义与 Settings 展示不完全一致。 - -建议:Settings 不再维护独立 app 清单,只从 `AppRegistry` 读取 `configurable` 项。 - -### 3.3 配置变更不会刷新主页模型 - -- Settings 保存 `app_*` 后没有通知 `Launch`。 -- `Launch::applications_reload()` 当前只保留固定段并重扫 `.desktop`,不会重新应用内置 app enable/disable。 -- 用户在设置页切换 app 后,主页行为可能不变或需要重启。 - -建议:保存配置后发布事件或回调,`Launch` 执行: - -1. 重建内置 app 列表。 -2. 重扫 `.desktop` app。 -3. 规范化当前 selection。 -4. 刷新 carousel。 - -### 3.4 外部命令执行边界弱 - -- `.desktop` 的 `Exec` 从应用目录读取后直接用于终端或外部进程执行。 -- Setup 页面大量使用 `system()` / `popen()` 拼接 shell。 -- SSID、配置字符串、更新命令等可能进入 shell 命令。 -- 阻塞式外部命令会卡住 UI 线程或让恢复路径依赖顺序执行。 - -相关文件: - -- `projects/APPLaunch/main/ui/Launch.cpp` -- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` -- `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` - -建议: - -- 引入统一进程 API,使用 argv 形式执行,避免 shell 拼接。 -- 对 `.desktop Exec` 做 allowlist、owner/permission 校验、路径规范化、最大文件大小/条目数量限制。 -- 对 Setup 的网络、时间、更新等动作抽到 platform service。 -- 外部命令统一返回结构化错误,并显示到 UI hint/诊断页面。 - -### 3.5 对象生命周期存在环和裸资源 - -- `zero_lvgl_os` 持有 `Launch` 和 `UILaunchPage`。 -- `Launch` 持有 `std::shared_ptr`。 -- `UILaunchPage` 持有 `std::shared_ptr`。 -- 这形成 `shared_ptr` 环,`Launch::~Launch()` 中 watcher/timer 清理理论上不可达。 - -相关文件: - -- `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` -- `projects/APPLaunch/main/ui/Launch.h` -- `projects/APPLaunch/main/ui/UILaunchPage.h` -- `projects/APPLaunch/main/ui/Launch.cpp` - -建议: - -- 由 `zero_lvgl_os` 唯一拥有 model/controller/view。 -- 子对象之间使用裸观察指针或 `weak_ptr`。 -- 明确析构顺序:先停 timer/watch/async,再释放页面,再释放 model。 - -## 4. P1 中高优先级问题 - -### 4.1 `Launch` 职责过宽 - -`Launch` 当前同时承担: - -- 内置 app 注册。 -- `.desktop` 解析。 -- 动态 app 去重。 -- 外部进程启动。 -- 内部页面启动。 -- 当前 selection 状态。 -- 目录 watch。 -- 回主页和 UI 刷新协调。 - -建议拆分: - -- `AppRegistry`:内置/动态 app 加载、过滤、排序、去重。 -- `AppLauncher`:外部进程、终端、内部页面启动。 -- `LauncherController`:selection、reload、home navigation。 -- `UILaunchPage`:纯 view + input adapter。 - -### 4.2 平台模型不统一 - -- 构建层使用 `CONFIG_APPLAUNCH_*`。 -- 源码层同时使用 `__linux__`、`HAL_PLATFORM_SDL`、`_WIN32`。 -- 交叉编译时宿主 OS 宏、目标平台宏、后端宏容易混淆。 - -建议建立语义平台宏: - -```cpp -APPLAUNCH_TARGET_CP0 -APPLAUNCH_BACKEND_SDL -APPLAUNCH_HAS_LORA -APPLAUNCH_HAS_CAMERA -APPLAUNCH_HAS_SYSTEM_UPDATE -APPLAUNCH_HAS_WIFI_CONFIG -``` - -源码只消费这些产品语义宏,不直接用宿主 OS 判断功能可用性。 - -### 4.3 构建时生成源码文件 - -- `projects/APPLaunch/main/SConstruct` 每次构建运行 `generate_page_app_includes.py`。 -- 脚本写回 `projects/APPLaunch/main/ui/page_app.h`。 -- 构建副作用会造成脏工作区、IDE/静态分析不稳定、并行构建竞争。 -- 调用端没有检查 return code/stderr,失败时可能继续使用旧文件。 - -建议二选一: - -1. 将 `page_app.h` 作为源码手动维护,禁止构建改写。 -2. 生成到 `build/generated/include`,加入 include path,并使用 `subprocess.run(..., check=True)`。 - -### 4.4 构建脚本承担依赖下载 - -- `projects/APPLaunch/SConstruct` 在交叉编译时检查并下载 static BSP 包。 -- 构建和依赖获取耦合,影响离线构建、CI 可重复性、供应链校验。 - -建议: - -- 新增 `tools/fetch_sysroot.py` 或 SDK bootstrap 步骤。 -- 下载逻辑带 checksum、版本文件、离线缓存。 -- SCons 只校验 sysroot 是否存在和版本是否匹配。 - -### 4.5 平台配置重复严重 - -多个 `*config_defaults.mk` 中 LVGL、font、freetype、thread、asset path 配置大段重复,仅少量后端/toolchain/NEON 差异。 - -建议改成: - -- `config/base_lvgl.mk` -- `config/base_cp0.mk` -- `config/backend_sdl.mk` -- `config/backend_fbdev.mk` -- `config/host_linux.mk` -- `config/host_windows.mk` -- `config/target_aarch64_cp0.mk` - -再由目标配置组合 overlay。 - -### 4.6 SDK 组件内部被应用构建脚本修改 - -`main/SConstruct` 直接过滤 `lv_sdl_keyboard.c`,并向 `lvgl_component` 追加 include/definitions。这让 APPLaunch 知道 SDK 组件内部结构,SDK 升级容易破。 - -建议: - -- SDK 暴露正式配置项或 component hook。 -- APPLaunch 不直接修改 SDK component 内部 `SRCS/DEFINITIONS`。 - -## 5. P2 中优先级问题 - -### 5.1 页面实现 header-only 且体量过大 - -典型文件: - -- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` -- `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` -- `projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp` -- `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` - -问题: - -- UI、状态机、硬件访问、命令执行混在一个文件。 -- 编译边界大,改一个页面牵动 `page_app.h` 聚合 include。 -- 难以 mock 硬件服务,难以写单元测试。 - -建议: - -- `.hpp` 只保留类声明和小型 inline。 -- 复杂实现迁到 `.cpp`。 -- 硬件访问迁到 service/platform 层。 -- UI 只订阅状态和派发 intent。 - -### 5.2 首页轮播状态和 LVGL 对象数组强耦合 - -`UILaunchPage` 使用一个 enum 同时索引 card/title/dot,并在动画中旋转 LVGL 对象数组。UI 对象顺序就是状态机,维护难度高。 - -建议: - -- 抽象 `CarouselModel`:维护当前 index、visible slots、pending switch。 -- `UILaunchPage` 只根据 model 渲染 slots。 -- 动画层只处理 view transition,不修改应用选择状态。 - -### 5.3 全局/静态 UI 状态较多 - -典型状态: - -- `ui.cpp` 的全局 `home`。 -- `UILaunchPage.cpp` 的 `active_launch_page`。 -- `UILaunchPage.cpp` 的全局 `home_input_group`。 -- `ui_loading.cpp` / `ui_global_hint.cpp` 的静态对象。 - -建议: - -- 统一归入 `LauncherRuntime` 或 `UiContext`。 -- 明确 owner、init、shutdown。 -- 测试或热重启时可以完整重置。 - -### 5.4 页面 timer/动画取消不统一 - -- 多数页面手动删除 timer,但一致性不足。 -- `UIIpPanelPage` 等页面存在析构不清理 timer 的风险。 -- 动画回调捕获裸 `lv_obj_t*` 和 lambda,页面删除时缺少统一取消 token。 - -建议: - -- 页面或模块在自身内部直接持有并释放 timer/event/animation 资源。 -- 页面析构统一取消回调。 -- 回调校验 generation/token,而不是只依赖裸指针有效。 - -### 5.5 资源命名和路径语义不统一 - -资源目录中存在大小写、拼写、尺寸后缀混杂: - -- `game_100.png` / `gmae.png` -- `e_mail_100.png` / `email.png` -- `unitENV.png` / `unitenv_100.png` -- `compass_needle_80.png` - -静态图标使用 `cp0_file_path("xxx.png")`,动态 `.desktop` 的 `Icon` 直接保存字符串,两者路径语义不同。 - -建议: - -- 建立资源 manifest 或 `resources.hpp`。 -- 统一命名规则:`snake_case[_size].png`。 -- `.desktop Icon` 统一解析为绝对路径或基于 `cp0_file_path` 的资源相对路径。 - -### 5.6 状态栏和基础布局重复 - -`UIAppTopBar` 和 `home_base` 各自实现 top/status 区域,时间/WiFi/电池样式和刷新逻辑分叉。 - -建议: - -- 提取单一 `TopBar` 组件。 -- 首页和 App 页通过参数配置 logo/title/颜色。 -- 电池、时间、WiFi 刷新逻辑复用。 - -## 6. 安全与可靠性关注点 - -- `.desktop` 文件解析缺少 owner/permission/大小/数量限制。 -- `Exec` 缺少 allowlist 和路径规范化。 -- Console 命令按空格拆分,不支持引用/转义,语义与 shell 不一致。 -- Setup 页面 shell 拼接风险高。 -- `launch_Exec()` 关闭 LVGL timer/input 后没有 RAII guard;异常或卡死时恢复边界弱。 -- 主循环无限轮询,无退出信号、无健康状态、无顶层错误隔离。 -- 字体未初始化时 `launcher_fonts()` 直接 `abort()`,生产恢复性弱。 -- 启动 GIF 对象 pause/load home 后缺少显式删除或空指针清理。 - -## 7. 测试与可观测性缺口 - -当前未看到 `projects/APPLaunch` 下存在系统性的 test/spec。 - -建议优先补最小测试层: - -1. `.desktop` parser:合法/非法 key、重复 Exec、Icon 路径、Terminal/Sysplause 解析。 -2. `AppRegistry`:平台 feature filter、Settings enable/disable、always-on 规则。 -3. Console tokenizer:空命令、参数、路径、错误反馈。 -4. Setup 命令构造:SSID 转义、禁止 shell 注入。 -5. 页面生命周期:timer 创建/析构、reload、go home。 -6. Launch reload:动态应用增删、selection 保持、carousel 刷新。 - -可观测性建议: - -- 统一 `LauncherErrorCode`。 -- 所有 process/file/hardware 失败进入同一日志路径。 -- UI 增加最近 N 条错误诊断页面。 -- 清理 `SLOG*`、`fprintf(stderr)`、`perror` 混用。 - -## 8. 建议落地路线 - -### 第一阶段:修一致性和安全边界 - -1. 修复 `app::Exec` 未赋值问题。 -2. 引入 `AppDescriptor` / `AppRegistry`。 -3. Settings 和 Launcher 共用同一份 app registry。 -4. Settings 保存后通知 Launcher 重建列表。 -5. `.desktop Exec` 做基础 allowlist/权限/路径检查。 -6. Setup 中高风险 `system()/popen()` 先替换为 argv API 或 service wrapper。 - -### 第二阶段:修生命周期和构建副作用 - -1. 打破 `Launch` / `UILaunchPage` 的 `shared_ptr` 环。 -2. 目录 watcher/timer 由 `Launch` 内部直接管理,PTY 由 `UIConsolePage` 自行管理。 -3. `launch_Exec()` 在外部进程返回后直接恢复 LVGL timer/input/display flag。 -4. `page_app.h` 生成到 build 目录,或改为手动维护。 -5. `generate_page_app_includes.py` 失败即中断构建。 - -### 第三阶段:平台和页面架构重构 - -1. 建立语义平台宏和 feature set。 -2. 配置文件改 base + overlay。 -3. BSP/sysroot 下载移出 SCons 主构建。 -4. 大页面拆分为 model/service/view/controller。 -5. LoRa、Camera、Setup 等硬件/系统能力迁到 platform service。 - -### 第四阶段:资源、布局、测试完善 - -1. 资源 manifest 化,统一命名。 -2. 首页和 App 页共用 `TopBar`。 -3. 建立 SDL2 CI 构建和核心单元测试。 -4. 增加诊断页面和错误历史。 -5. 补充“资源所有权图”“页面生命周期图”“平台能力矩阵”。 - -## 9. 首批建议修改文件 - -优先处理: - -- `projects/APPLaunch/main/ui/Launch.h` -- `projects/APPLaunch/main/ui/Launch.cpp` -- `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` -- `projects/APPLaunch/main/ui/page_app.h` -- `projects/APPLaunch/main/SConstruct` -- `projects/APPLaunch/SConstruct` -- `projects/APPLaunch/main/ui/UILaunchPage.h` -- `projects/APPLaunch/main/ui/UILaunchPage.cpp` - -## 10. 结论 - -APPLaunch 当前最大收益的第一刀不是全面重写,而是先把应用模型统一:`AppRegistry + AppDescriptor + Settings/Launcher 同源 + 配置变更刷新主页`。这一刀可以直接消除用户可见的不一致,也为后续平台拆分、测试和生命周期治理建立核心支点。 - -## 2026-06-15 zero_lvgl_os 启动职责整理 - -本轮对 `zero_lvgl_os` 的启动职责做了语义拆分: - -- 构造函数继续只负责对象装配:display/theme/fonts、`Launch`、`UILaunchPage`、二者引用关系。 -- `start()` 负责运行期 UI 启动,内部拆成: - - `build_launcher_home()`:创建 LVGL screen、绑定 `Launch` UI、初始化输入 group。 - - `show_initial_screen()`:根据启动动画配置加载首页或启动 GIF。 -- 原 `creat_display()` 更正为 `create_display()`。 -- 原 `create_launcher_home()` 移除,避免名字误导为单纯构建函数;现在创建和显示职责分开。 - -验证: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed -``` - -结果:均通过。 diff --git a/projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md b/projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md deleted file mode 100644 index 0a6ebde8..00000000 --- a/projects/APPLaunch/docs/cp0_lvgl_platform_decoupling_plan.md +++ /dev/null @@ -1,242 +0,0 @@ -# cp0_lvgl / APPLaunch 平台解耦计划 - -日期:2026-06-15 - -## 目标 - -让 APPLaunch 保持平台无关:页面和业务逻辑只调用 `cp0_*` 接口,不直接包含或调用 Linux/macOS/Windows/SDL 特有 API、系统命令、`/dev`、`/sys`、GPIO/SPI/I2C、RadioLib HAL 等实现细节。 - -`cp0_lvgl` 负责承载平台和硬件实现,并保持现有风格: - -- APP 层使用 `cp0_*` facade。 -- 平台层能力尽量靠近已有 `hal_*`/service 实现。 -- C ABI、POD struct、简单返回码。 -- SDL/CP0/Win/Web 可以分别提供实现或 stub。 - -## 当前状态 - -已完成: - -- Settings 页中的 `system()`/`popen()`/`nmcli`/账号信息/eth0 信息/BQ27220 I2C 校准已下沉到 `cp0_lvgl`。 -- APPLaunch 使用 `cp0_*` 调用。 -- `Launch` 的 `.desktop Exec` 安全校验使用 `cp0_desktop_exec_is_safe()`。 - -仍需处理: - -- `cp0_app_platform.cpp` 是杂物文件,职责过宽。 -- `cp0_lvgl_app.h` 继续膨胀,需要后续分组收敛。 -- `ui_app_compass.hpp` 仍直接枚举 `/sys/bus/iio/devices`。 -- `ui_app_lora.hpp` 仍包含 GPIO/SPI/I2C/RadioLib 实现。 -- `AppRegistry.cpp`/`Launch.cpp` 仍有少量平台宏判断。 - -## 阶段计划 - -### 阶段 1:收敛新增 cp0 平台服务 - -目的:不改变 APPLaunch 行为,先让 `cp0_lvgl` 内部职责清晰。 - -动作: - -1. 拆分 `ext_components/cp0_lvgl/src/cp0_app_platform.cpp`: - - `cp0_app_process_utils.cpp`:argv 执行、capture、文件首行读取、desktop Exec 校验。 - - `cp0_app_system_info.cpp`:Ethernet、Account 信息。 - - `cp0_app_wifi_profile.cpp`:WiFi profile 查询、删除、断开 active profile。 - - `cp0_app_update.cpp`:apt update、launcher update。 - - `cp0_app_bq27220.cpp`:BQ27220 calibration。 -2. 维持 `cp0_lvgl_app.h` 当前 ABI,避免 APPLaunch 二次改动。 -3. 构建验证 SDL2。 - -### 阶段 2:命名和边界收敛 - -目的:把新增能力放进更合理的服务边界。 - -动作: - -1. 评估是否新增/扩展 `hal_process`:`run_argv`、`capture_argv`。 -2. 评估 `cp0_eth_info_read()` 是否改为扩展 `cp0_network_list()` 或新增 generic `cp0_network_default_info_read()`。 -3. 将 WiFi profile 命名稳定为: - - `cp0_wifi_profile_exists()` - - `cp0_wifi_profile_forget()` - - `cp0_wifi_profile_disconnect_active()` -4. 将 APPLaunch 专属 update 策略从 cp0_lvgl 长期接口中剥离:优先改成通用 package/service API。 - -### 阶段 3:迁移 Compass IIO - -目的:APPLaunch 不再直接读 `/sys/bus/iio/devices`。 - -动作: - -1. 在 `cp0_lvgl` 新增 sensor/imu/compass 数据接口。 -2. 把 IIO 枚举、raw/scale 读取移动到 cp0_lvgl。 -3. `ui_app_compass.hpp` 只保留 UI 和状态渲染。 -4. SDL 提供 fake/stub sensor 数据。 - -### 阶段 4:迁移 LoRa 硬件 HAL - -目的:APPLaunch 不再包含 GPIO/SPI/I2C/RadioLib HAL。 - -动作: - -1. 在 `cp0_lvgl` 定义 `cp0_lora_*` service:init/deinit/status/send/poll/diag。 -2. 移动 GPIO/SPI/I2C/PI4IO/RadioLib HAL 到 cp0_lvgl。 -3. `ui_app_lora.hpp` 改为纯 UI controller,调用 `cp0_lora_*`。 -4. SDL 提供不可用或模拟实现。 - -### 阶段 5:平台 feature 模型 - -目的:APPLaunch 不再用 `__linux__`/`HAL_PLATFORM_SDL` 判断产品功能。 - -动作: - -1. 新增 `cp0_feature_available(feature)` 或枚举 API。 -2. AppRegistry 用 feature gate 控制 Camera/LoRa/IP panel 等 app。 -3. 移除 APPLaunch 平台宏分支。 - -## 验证标准 - -每阶段至少运行: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -``` - -并扫描 APPLaunch 平台残留: - -```sh -rg -n "(/dev/|/sys/|ioctl\(|system\(|popen\(|fork\(|execvp|nmcli|RadioLib|linux/i2c|spidev|gpio|__linux__|HAL_PLATFORM_SDL)" projects/APPLaunch/main -``` - -## 2026-06-15 执行进展 - -本轮已完成: - -- `cp0_app_platform.cpp` 已拆散为 process/system-info/wifi/update/BQ27220 等独立实现文件。 -- `cp0_lvgl_app.h` 新增 C facade:process argv、desktop Exec 安全检查、network/account、WiFi profile、update、time、BQ27220、Compass、LoRa。 -- `ui_app_compass.hpp` 已移除 IIO `/sys/bus/iio/devices` 枚举和 raw/scale 读取,只消费 `cp0_compass_read()`。 -- `ui_app_lora.hpp` 已移除 Linux GPIO/SPI/I2C/RadioLib HAL、`/dev`、`/sys`、`ioctl()` 等实现,只保留 UI、键盘和状态渲染,硬件层改由 `cp0_lora_*` 提供。 -- LoRa 的 GPIO/SPI/I2C/PI4IO/RadioLib 逻辑已移动到 `ext_components/cp0_lvgl/src/cp0_app_lora.cpp`。 -- `RadioLib` 依赖已加入 `cp0_lvgl/SConstruct`;当前由于 SCons 静态库链接顺序限制,`APPLaunch/main/SConstruct` 仍保留直接 `RadioLib` 依赖以通过链接,后续应在构建系统层修复 transitive link order 后移除。 -- WiFi profile 的旧别名接口已移除声明和实现,保留 `cp0_wifi_profile_exists()` / `cp0_wifi_profile_forget()` / `cp0_wifi_profile_disconnect_active()`。 - -验证: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -``` - -结果:通过。 - -当前 APPLaunch 平台残留扫描结果: - -- 主要剩余为 POSIX 文件管理/轻量平台 include:`main.cpp`、`UILaunchPage.cpp`、`Launch.cpp`、`ui_global_hint.cpp`、`ui_app_page.hpp`、`ui_app_file.hpp`、`ui_app_camera.hpp`、`ui_app_rec.hpp`、`ui_app_console.hpp`、`ui_app_setup.hpp`。 -- `AppRegistry.cpp` / `Launch.cpp` / `zero_lvgl_os.cpp` 仍有 `__linux__` / `HAL_PLATFORM_SDL` feature gate。 -- `ui_app_lora.hpp` 和 `ui_app_compass.hpp` 已无直接硬件/平台访问。 -- `APPLaunch/main/SConstruct` 仍直接声明 `RadioLib`,原因见上面的链接顺序说明。 - -后续建议: - -1. 把 `cp0_app_lora.cpp`、`cp0_app_compass.cpp`、`cp0_app_bq27220.cpp` 等 Linux/硬件实现按 cp0_lvgl 原风格继续拆到平台目录或提供 SDL stub,根 `src/` 仅保留平台无关 glue。 -2. 建立 `cp0_feature_available()`,替换 APPLaunch 中的 `__linux__` / `HAL_PLATFORM_SDL` app gate。 -3. 将 `ui_app_file.hpp`、`ui_app_camera.hpp`、`ui_app_rec.hpp` 的目录创建/遍历/删除收敛到 `cp0_file_*`。 -4. 修复构建系统 transitive static link order 后,从 APPLaunch `REQUIREMENTS` 移除 `RadioLib`。 - -补充验证: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -``` - -结果:均通过。当前仅保留既有/迁移后的 `snprintf` 截断 warning:`cp0_app_lora.cpp` 诊断字符串和 `ui_app_setup.hpp` WiFi title。 - -## 2026-06-15 下一步执行:平台目录收敛 - -本轮继续完成: - -- 将 CP0 硬件专用实现移入 `ext_components/cp0_lvgl/src/cp0/`: - - `cp0_app_bq27220.cpp` - - `cp0_app_compass.cpp` - - `cp0_app_lora.cpp` -- 新增 SDL stub:`ext_components/cp0_lvgl/src/sdl/cp0_app_hardware_stub_sdl.cpp`。 - - SDL 下 `cp0_compass_read()` 返回 unavailable。 - - SDL 下 `cp0_lora_*()` 返回 unavailable/stub 信息。 - - SDL 下 `cp0_bq27220_calibrate()` 返回失败。 -- 修复本轮能处理的截断 warning: - - `cp0_app_lora.cpp` 的 SPI 候选/诊断字符串增加长度限制。 - - `ui_app_setup.hpp` 的 WiFi title 对 SSID/IP 做长度限制。 - -验证: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed -``` - -结果:均通过。 - -当前边界: - -- `src/` 根目录仍保留平台工具/策略文件:`cp0_app_process_utils.cpp`、`cp0_app_system_info.cpp`、`cp0_app_update.cpp`、`cp0_app_wifi_profile.cpp`。 -- 其中 `update` 和 `wifi_profile` 仍含 Linux 命令策略;后续若继续严格化,应拆到 `src/cp0/` 并在 `src/sdl/` 提供 stub,或者抽为更通用的 package/service/process API。 - -## 2026-06-15 WiFi API 收敛 - -本轮将 cp0 WiFi 相关接口统一到 `cp0_signal_wifi_api` / `cp0_lvgl_wifi.cpp` 风格: - -- 新增 `cp0_signal_wifi_api`,签名与 audio/camera API 保持一致: - - `std::list` 参数 - - `std::function` 回调 -- 新增统一实现:`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_wifi.cpp`。 - - `init_wifi()` 注册 `cp0_signal_wifi_api` handler。 - - `cp0_wifi_get_status()`、`cp0_wifi_scan()`、`cp0_wifi_connect()`、`cp0_wifi_disconnect()` 均通过 signal API 转发。 - - `cp0_wifi_profile_forget()`、`cp0_wifi_profile_exists()`、`cp0_wifi_profile_disconnect_active()` 也统一到同一个 signal API。 - - 内部改用 `cp0_process_run_argv()` / `cp0_process_capture_argv()`,不再在 WiFi 实现里使用 `system()` / `popen()` / shell 拼接。 -- 新增 SDL 转接:`ext_components/cp0_lvgl/src/sdl/cp0_lvgl_wifi.cpp`,与 audio 当前写法一致,直接复用 `../cp0/cp0_lvgl_wifi.cpp`。 -- 删除旧的 WiFi profile 独立实现:`ext_components/cp0_lvgl/src/cp0_app_wifi_profile.cpp`。 -- 从旧位置移除重复 WiFi 实现: - - `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` - - `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` -- `cp0_lvgl_init()` 在 CP0/SDL 初始化路径中增加 `init_wifi()`。 - -验证: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed --config=force -g++ -std=c++17 -Iext_components/cp0_lvgl/include -ISDK/github_source/eventpp/include -c ext_components/cp0_lvgl/src/cp0/cp0_lvgl_wifi.cpp -o /tmp/cp0_lvgl_wifi.o -``` - -结果:均通过。 - -## 2026-06-15 Process API 收敛 - -本轮将 `cp0_process_*` 相关接口统一到 `cp0_signal_process_api` / `cp0_lvgl_process.cpp` 风格: - -- 新增 `cp0_signal_process_api`,签名与 audio/wifi/camera API 保持一致: - - `std::list` 参数 - - `std::function` 回调 -- 新增统一实现:`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp`。 - - `init_process()` 注册 `cp0_signal_process_api` handler。 - - `cp0_process_exec_blocking()`、`cp0_process_spawn()`、`cp0_process_stop()`、`cp0_process_check_lock()`、`cp0_process_kill()` 均通过 signal API 转发。 - - `cp0_process_run_argv()`、`cp0_process_capture_argv()` 也统一到同一个 process API。 - - `cp0_system_shutdown()` / `cp0_system_reboot()` 随 process service 一起收口,避免继续留在旧 process 文件中。 - - `.desktop Exec` 安全检查和 `cp0_file_read_first_line()` 作为 process/file utility 保留在同一统一文件中。 -- 新增 SDL 转接:`ext_components/cp0_lvgl/src/sdl/cp0_lvgl_process.cpp`,与 audio/wifi 写法一致,复用 `../cp0/cp0_lvgl_process.cpp`。 -- 初始化路径接入:CP0/SDL 两条 `cp0_lvgl_init()` 都在 audio 后、wifi 前调用 `init_process()`,确保 WiFi/update/system-info 中的 argv API 可用。 -- 删除旧实现: - - `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` - - `ext_components/cp0_lvgl/src/cp0_app_process_utils.cpp` -- SDL 旧 compat 中的 `cp0_process_*` wrapper 已移除,PTY wrapper 保留,因为它不属于本次 process API 收敛范围。 - -验证: - -```sh -CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -Q -j8 --implicit-deps-changed -CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -Q -j8 --implicit-deps-changed -g++ -std=c++17 -DHAL_PLATFORM_SDL=1 -Iext_components/cp0_lvgl/include -ISDK/github_source/eventpp/include -c ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp -o /tmp/cp0_lvgl_process_sdl.o -g++ -std=c++17 -Iext_components/cp0_lvgl/include -ISDK/github_source/eventpp/include -c ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp -o /tmp/cp0_lvgl_process_cp0.o -``` - -结果:均通过。 - -协作说明:本轮期间工作树里已有其他人对 filesystem/screenshot/lora/bq27220 等模块的并行修改;编译过程中一度暴露的 filesystem 重复定义属于并行改动范畴,后续再次构建已通过。 From bf852576c17478c1c0521f223cb95c4db2c5dea8 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 18:48:32 +0800 Subject: [PATCH 64/70] Add Compass magnetometer calibration Expose a cp0_compass_calibrate API and implement asynchronous 8-figure magnetometer bias calibration in the cp0 IMU backend. Bind Compass F6/6 to start calibration and show a centered 8-figure calibration prompt while calibration is running. --- .../cp0_lvgl/include/cp0_lvgl_app.h | 1 + .../cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp | 104 +++++++++++++++++- .../cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp | 14 +++ .../main/ui/page_app/ui_app_compass.hpp | 74 ++++++++++++- 4 files changed, 187 insertions(+), 6 deletions(-) diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index 0582a6eb..d60895b0 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -142,6 +142,7 @@ int cp0_system_update_launcher_background(void); int cp0_time_set(const char *timestamp); int cp0_bq27220_calibrate(int command_index); int cp0_compass_read(cp0_compass_read_cb_t callback, void *user); +int cp0_compass_calibrate(void); cp0_battery_info_t cp0_battery_read(void); int cp0_backlight_read(void); int cp0_backlight_max(void); diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp index 46b88a3f..fe16f770 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_imu.cpp @@ -2,13 +2,19 @@ #include "hal_lvgl_bsp.h" #include "../cp0_app_internal_utils.h" +#include +#include +#include #include #include #include #include #include +#include #include +#include #include +#include #include #if !defined(_WIN32) @@ -138,6 +144,11 @@ class ImuSystem { return; } + if (arg.front() == "Calibrate") { + calibrate(callback); + return; + } + report(callback, -1, "unknown imu api\n"); } @@ -156,6 +167,24 @@ class ImuSystem { report(callback, ret, std::string(reinterpret_cast(&info), sizeof(info))); } + void calibrate(callback_t callback) + { +#if defined(_WIN32) + report(callback, -1, "compass calibration unavailable\n"); +#else + if (calibrating_.exchange(true)) { + report(callback, 1, "compass calibration already running\n"); + return; + } + + std::thread([this]() { + calibrate_worker(); + calibrating_.store(false); + }).detach(); + report(callback, 0, "compass calibration started\n"); +#endif + } + int read_info(cp0_compass_info_t *info) { if (!info) @@ -186,6 +215,8 @@ class ImuSystem { if (paths.has_gyro) read_axis_triplet(paths.accel, "in_anglvel", gyr_scale, gyr_x, gyr_y, gyr_z); + apply_mag_bias(mag_x, mag_y, mag_z); + float pitch = std::atan2(-acc_x, std::sqrt(acc_y * acc_y + acc_z * acc_z)); float roll = std::atan2(acc_y, acc_z); float sin_p = std::sin(pitch); @@ -199,7 +230,7 @@ class ImuSystem { if (yaw < 0.0f) yaw += 360.0f; - clear_info(info, "Sensor OK", 1); + clear_info(info, calibrating_.load() ? "Calibrating..." : "Sensor OK", 1); info->yaw = yaw; info->pitch = pitch * 180.0f / 3.1415926f; info->roll = roll * 180.0f / 3.1415926f; @@ -215,6 +246,68 @@ class ImuSystem { return 0; #endif } + +#if !defined(_WIN32) + void apply_mag_bias(float &mag_x, float &mag_y, float &mag_z) + { + std::lock_guard lock(bias_mutex_); + if (!mag_bias_valid_) + return; + + mag_x -= mag_bias_x_; + mag_y -= mag_bias_y_; + mag_z -= mag_bias_z_; + } + + void calibrate_worker() + { + IioDevicePaths paths = enumerate_iio_devices(); + if (!paths.ready()) + return; + + const float mag_scale = read_float_file_or(paths.magn + "/in_magn_scale", 1.0f); + float min_x = std::numeric_limits::max(); + float min_y = std::numeric_limits::max(); + float min_z = std::numeric_limits::max(); + float max_x = std::numeric_limits::lowest(); + float max_y = std::numeric_limits::lowest(); + float max_z = std::numeric_limits::lowest(); + int samples = 0; + + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::chrono::steady_clock::now() < deadline) { + float mag_x = 0.0f; + float mag_y = 0.0f; + float mag_z = 0.0f; + if (read_axis_triplet(paths.magn, "in_magn", mag_scale, mag_x, mag_y, mag_z)) { + min_x = std::min(min_x, mag_x); + min_y = std::min(min_y, mag_y); + min_z = std::min(min_z, mag_z); + max_x = std::max(max_x, mag_x); + max_y = std::max(max_y, mag_y); + max_z = std::max(max_z, mag_z); + samples++; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + + if (samples < 10) + return; + + std::lock_guard lock(bias_mutex_); + mag_bias_x_ = (min_x + max_x) / 2.0f; + mag_bias_y_ = (min_y + max_y) / 2.0f; + mag_bias_z_ = (min_z + max_z) / 2.0f; + mag_bias_valid_ = true; + } +#endif + + std::atomic calibrating_{false}; + std::mutex bias_mutex_; + bool mag_bias_valid_ = false; + float mag_bias_x_ = 0.0f; + float mag_bias_y_ = 0.0f; + float mag_bias_z_ = 0.0f; }; } // namespace @@ -234,6 +327,15 @@ extern "C" int cp0_compass_read(cp0_compass_read_cb_t callback, void *user) return ret; } +extern "C" int cp0_compass_calibrate(void) +{ + int ret = -1; + cp0_signal_imu_api({"Calibrate"}, [&](int code, std::string) { + ret = code; + }); + return ret; +} + extern "C" void init_imu(void) { std::shared_ptr imu = std::make_shared(); diff --git a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp index 07c1db35..a108b48f 100644 --- a/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp +++ b/ext_components/cp0_lvgl/src/sdl/sdl_lvgl_imu.cpp @@ -62,6 +62,11 @@ class ImuSystem return; } + if (arg.front() == "Calibrate") { + report(callback, 0, "SDL simulated compass calibration\n"); + return; + } + report(callback, -1, "unknown imu api\n"); } @@ -99,6 +104,15 @@ extern "C" int cp0_compass_read(cp0_compass_read_cb_t callback, void *user) return ret; } +extern "C" int cp0_compass_calibrate(void) +{ + int ret = -1; + cp0_signal_imu_api({"Calibrate"}, [&](int code, std::string) { + ret = code; + }); + return ret; +} + extern "C" void init_imu(void) { if (g_imu) diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp index 87ab70cf..35eb3889 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp @@ -28,8 +28,8 @@ * Screen: 320 x 170 * * Keys: - * F4 Reserved for the calibration interface - * F6/ESC Return home + * F6/6 Start magnetometer calibration + * ESC Return home * ============================================================ */ class UICompassPage : public AppPageRoot @@ -52,6 +52,8 @@ class UICompassPage : public AppPageRoot static constexpr uint32_t kColorIconList = 0x33CC33; static constexpr uint32_t kColorIconExit = 0xFF0000; static constexpr uint32_t kColorSensorWarn = 0xFF0000; + static constexpr uint32_t kColorCalibrateBg = 0x111827; + static constexpr uint32_t kColorCalibrateBorder = 0x33CC33; static constexpr const char* ICON_EXIT = "\uEA01"; // .svgfont-exit static constexpr const char* ICON_LIST = "\uEA04"; // .svgfont-list @@ -108,6 +110,8 @@ class UICompassPage : public AppPageRoot lv_obj_t* lbl_gyr_ = nullptr; lv_obj_t* sensor_missing_box_ = nullptr; lv_obj_t* sensor_missing_label_ = nullptr; + lv_obj_t* calibration_box_ = nullptr; + lv_obj_t* calibration_label_ = nullptr; std::array lbl_bottom_btns_{}; std::array lbl_bottom_indicators_{}; @@ -136,6 +140,7 @@ class UICompassPage : public AppPageRoot create_imu_panel(root_screen_); create_bottom_bar(root_screen_); create_sensor_missing_overlay(root_screen_); + create_calibration_overlay(root_screen_); update_from_state(CompassUiState{}); sensor_timer_ = lv_timer_create(&UICompassPage::sensor_timer_cb, 50, this); @@ -293,6 +298,45 @@ class UICompassPage : public AppPageRoot lv_obj_move_foreground(sensor_missing_box_); } + void create_calibration_overlay(lv_obj_t* parent) + { + calibration_box_ = lv_obj_create(parent); + lv_obj_remove_style_all(calibration_box_); + lv_obj_set_size(calibration_box_, 230, 78); + lv_obj_center(calibration_box_); + lv_obj_set_style_bg_color(calibration_box_, color(kColorCalibrateBg), 0); + lv_obj_set_style_bg_opa(calibration_box_, LV_OPA_90, 0); + lv_obj_set_style_border_width(calibration_box_, 2, 0); + lv_obj_set_style_border_color(calibration_box_, color(kColorCalibrateBorder), 0); + lv_obj_set_style_radius(calibration_box_, 6, 0); + lv_obj_set_style_pad_all(calibration_box_, 0, 0); + lv_obj_clear_flag(calibration_box_, LV_OBJ_FLAG_SCROLLABLE); + + calibration_label_ = lv_label_create(calibration_box_); + lv_obj_set_size(calibration_label_, 214, 62); + lv_obj_center(calibration_label_); + lv_obj_set_style_text_font(calibration_label_, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(calibration_label_, color(kColorText), 0); + lv_obj_set_style_text_align(calibration_label_, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(calibration_label_, LV_LABEL_LONG_WRAP); + lv_label_set_text(calibration_label_, "8-figure calibration\nMove device in a figure-8\nuntil this dialog closes"); + + lv_obj_add_flag(calibration_box_, LV_OBJ_FLAG_HIDDEN); + } + + void set_calibration_overlay_visible(bool visible) + { + if (!calibration_box_) + return; + + if (visible) { + lv_obj_clear_flag(calibration_box_, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(calibration_box_); + } else { + lv_obj_add_flag(calibration_box_, LV_OBJ_FLAG_HIDDEN); + } + } + void set_bottom_btn(int idx, const char* text, bool icon, uint32_t hex) { lv_obj_set_style_text_font(lbl_bottom_btns_[idx], @@ -333,6 +377,24 @@ class UICompassPage : public AppPageRoot cp0_compass_read(&UICompassPage::compass_read_cb, this); } + void start_calibration() + { + int ret = cp0_compass_calibrate(); + if (ret == 0) { + set_calibration_overlay_visible(true); + if (lbl_status_text_) + lv_label_set_text(lbl_status_text_, "Calibrating..."); + } else if (ret > 0) { + set_calibration_overlay_visible(true); + if (lbl_status_text_) + lv_label_set_text(lbl_status_text_, "Calibration already running"); + } else { + set_calibration_overlay_visible(false); + if (lbl_status_text_) + lv_label_set_text(lbl_status_text_, "Calibration failed"); + } + } + static void compass_read_cb(int code, const cp0_compass_info_t* info, void* user) { auto* self = static_cast(user); @@ -360,6 +422,8 @@ class UICompassPage : public AppPageRoot lv_label_set_text(lbl_status_text_, state.statusText.c_str()); } + set_calibration_overlay_visible(state.statusText == "Calibrating..."); + if (sensor_missing_box_) { if (state.sensorReady) { lv_obj_add_flag(sensor_missing_box_, LV_OBJ_FLAG_HIDDEN); @@ -464,11 +528,11 @@ class UICompassPage : public AppPageRoot void handle_key(uint32_t key) { switch (key) { - case KEY_F4: - // TODO(compass): Trigger magnetometer/IMU calibration after the API is available. + case KEY_F6: + case KEY_6: + start_calibration(); break; - case KEY_F6: case KEY_ESC: if (navigate_home) { navigate_home(); From 4dbda4fd5067c680b41f4c41805a78c174b38a12 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Mon, 15 Jun 2026 19:39:52 +0800 Subject: [PATCH 65/70] docs: sync APPLaunch docs and add Japanese translations --- README.md | 85 +- README_JA.md | 349 ++++++ README_ZH.md | 85 +- docs/APPLaunch-App-packaging-guide-ja.md | 404 +++++++ ...23\345\214\205\346\214\207\345\215\227.md" | 41 +- docs/launcher-project-guide-ja.md | 29 + .../00-overview-and-reading-path.md | 87 ++ ...ject-layout-and-module-responsibilities.md | 249 ++++ .../02-runtime-framework-and-boot-flow.md | 351 ++++++ .../03-ui-framework-and-home-carousel.md | 516 +++++++++ ...-application-model-and-launch-mechanism.md | 445 ++++++++ .../05-built-in-page-framework.md | 323 ++++++ .../06-resources-and-configuration.md | 361 ++++++ .../07-input-system-and-key-mapping.md | 420 +++++++ .../08-build-and-compilation-guide.md | 1003 +++++++++++++++++ .../09-packaging-deployment-and-systemd.md | 934 +++++++++++++++ .../10-extension-development-guide.md | 420 +++++++ .../11-debugging-and-troubleshooting.md | 513 +++++++++ .../12-common-modification-entry-points.md | 215 ++++ .../launcher-project-guide.md | 29 + .../00-overview-and-reading-path.md | 4 +- ...ject-layout-and-module-responsibilities.md | 24 +- .../02-runtime-framework-and-boot-flow.md | 54 +- .../03-ui-framework-and-home-carousel.md | 30 +- ...-application-model-and-launch-mechanism.md | 136 +-- .../05-built-in-page-framework.md | 54 +- .../06-resources-and-configuration.md | 26 +- .../07-input-system-and-key-mapping.md | 18 +- .../08-build-and-compilation-guide.md | 147 ++- .../09-packaging-deployment-and-systemd.md | 113 +- .../10-extension-development-guide.md | 34 +- .../11-debugging-and-troubleshooting.md | 50 +- .../12-common-modification-entry-points.md | 58 +- ...05\350\257\273\350\267\257\347\272\277.md" | 4 +- ...41\345\235\227\350\201\214\350\264\243.md" | 24 +- ...57\345\212\250\346\265\201\347\250\213.md" | 54 +- ...26\351\241\265\350\275\256\346\222\255.md" | 30 +- ...57\345\212\250\346\234\272\345\210\266.md" | 136 +-- ...65\351\235\242\346\241\206\346\236\266.md" | 54 +- ...15\347\275\256\347\263\273\347\273\237.md" | 26 +- ...11\351\224\256\346\230\240\345\260\204.md" | 18 +- ...26\350\257\221\346\214\207\345\215\227.md" | 132 ++- ...203\250\347\275\262\344\270\216systemd.md" | 113 +- ...00\345\217\221\346\214\207\345\215\227.md" | 32 +- ...05\351\232\234\346\216\222\346\237\245.md" | 50 +- ...45\345\217\243\351\200\237\346\237\245.md" | 58 +- docs/macos-docker-build-ja.md | 108 ++ docs/macos-docker-build.md | 10 +- projects/APPLaunch/main/SConstruct | 6 +- 49 files changed, 7704 insertions(+), 758 deletions(-) create mode 100644 README_JA.md create mode 100644 docs/APPLaunch-App-packaging-guide-ja.md create mode 100644 docs/launcher-project-guide-ja.md create mode 100644 docs/launcher-project-guide-ja/00-overview-and-reading-path.md create mode 100644 docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md create mode 100644 docs/launcher-project-guide-ja/02-runtime-framework-and-boot-flow.md create mode 100644 docs/launcher-project-guide-ja/03-ui-framework-and-home-carousel.md create mode 100644 docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md create mode 100644 docs/launcher-project-guide-ja/05-built-in-page-framework.md create mode 100644 docs/launcher-project-guide-ja/06-resources-and-configuration.md create mode 100644 docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md create mode 100644 docs/launcher-project-guide-ja/08-build-and-compilation-guide.md create mode 100644 docs/launcher-project-guide-ja/09-packaging-deployment-and-systemd.md create mode 100644 docs/launcher-project-guide-ja/10-extension-development-guide.md create mode 100644 docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md create mode 100644 docs/launcher-project-guide-ja/12-common-modification-entry-points.md create mode 100644 docs/launcher-project-guide-ja/launcher-project-guide.md create mode 100644 docs/macos-docker-build-ja.md diff --git a/README.md b/README.md index 7e1ab7bf..e7201b2f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # launcher -[中文](./README_ZH.md) +[中文](./README_ZH.md) | [日本語](./README_JA.md) A collection of M5CardputerZero applications developed with the [M5Stack_Linux_Libs](https://github.com/m5stack/M5Stack_Linux_Libs) SDK. This project demonstrates how to build graphical UI applications with **LVGL 9.5** on the M5CardputerZero (AArch64 Linux) device. @@ -26,23 +26,21 @@ launcher/ │ │ ├── SConstruct # Top-level project build script │ │ ├── config_defaults.mk # Default build config (Linux Framebuffer mode) │ │ ├── darwin_config_defaults.mk # macOS build config +│ │ ├── win_x86_sdl2_config_defaults.mk # Windows SDL2 simulator config +│ │ ├── win_x86_cross_config_defaults.mk # Windows-to-device cross config │ │ ├── setup.ini # SSH deployment config │ │ └── main/ # Main application source │ │ ├── SConstruct # Component build script │ │ ├── src/ │ │ │ └── main.cpp # Program entry point -│ │ ├── include/ # Public headers (battery, keyboard_input, etc.) -│ │ ├── hal/ # Hardware abstraction layer -│ │ │ ├── sdl/ # SDL2 platform implementation (PC debugging) -│ │ │ └── linux/ # Linux platform implementation (device side) │ │ └── ui/ # UI code -│ │ ├── ui.h / ui.c # UI initialization -│ │ ├── screens/ # Screen definitions -│ │ ├── components/ # Custom components (app pages, launcher, etc.) -│ │ ├── widgets/ # Custom widgets (carousel component, etc.) -│ │ ├── Animation/ # Animation effects -│ │ ├── fonts/ # Font assets -│ │ └── images/ # Image assets +│ │ ├── ui.h / ui.cpp # UI initialization +│ │ ├── launch.cpp / launch.h # App list and launch logic +│ │ ├── app_registry.* # Built-in app enable/disable registry +│ │ ├── ui_launch_page.* # Home carousel UI +│ │ ├── launcher_ui_runtime.* # LVGL runtime/home bootstrap +│ │ ├── animation/ # Animation effects +│ │ └── page_app/ # Built-in app pages │ ├── Calculator/ # Calculator │ ├── AppStore/ # App store │ └── HelloWorld/ # Hello World example @@ -50,7 +48,8 @@ launcher/ ├── docs/ # Project documentation ├── scripts/ # Repository helper tools ├── README.md -└── README_ZH.md +├── README_ZH.md +└── README_JA.md ``` --- @@ -69,7 +68,7 @@ launcher/ ### APPLaunch Features - Application launcher UI with multi-app navigation (carousel page switching) -- Built-in app pages: Stock, Music, Camera, LoRa, SSH, GPIO, MIDI, Console, Mesh, and more +- Built-in app pages: Game, Setting, Compass, IP Panel, File, SSH, Mesh, Rec, Camera, LoRa, Tank Battle, Console, plus command/external entries such as Python, Store, CLI, and Math - LoRa communication (based on RadioLib SX1262) - Audio playback (based on Miniaudio) - Battery status monitoring and display @@ -208,6 +207,47 @@ scons distclean scons -j8 ``` +### Windows + +Windows builds use the same SCons entry point under `projects/APPLaunch`. Use an MSYS2 MinGW shell for the native SDL2 simulator build so that `gcc`, `g++`, `pkg-config`, SDL2, and FreeType are available in `PATH`. + +#### Install Dependencies (MSYS2 UCRT64 example) + +```bash +pacman -S --needed \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-pkgconf \ + mingw-w64-ucrt-x86_64-SDL2 \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-python-pip + +python -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +#### Build the APPLaunch SDL2 Simulator + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch.exe +``` + +#### Cross-Compile APPLaunch for the Device + +Install the SysGCC Raspberry64 Windows AArch64 Linux cross toolchain from `https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe`, and update `CONFIG_TOOLCHAIN_PATH` in `projects/APPLaunch/win_x86_cross_config_defaults.mk` if it is not installed at `D:\app\SysGCC\bin`. + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_cross_config_defaults.mk +scons -j8 +``` + +The cross-build output is `dist/M5CardputerZero-APPLaunch` and runs on the CardputerZero device. The first cross-build may download the SDK sysroot package into `SDK/github_source/static_lib_v0.0.4`. + ### Configuration Management Commands ```bash @@ -233,7 +273,7 @@ This program is a desktop UI application running on the CardputerZero device. It ### Build -Refer to the HelloWorld build instructions. +For device builds, SDL2 simulator builds, and cross-compilation, use the platform-specific build commands above with `cd projects/APPLaunch`. APPLaunch provides its own configuration files, including `linux_x86_sdl2_config_defaults.mk`, `linux_x86_cross_cp0_config_defaults.mk`, `mac_cross_cp0_config_defaults.mk`, `win_x86_sdl2_config_defaults.mk`, and `win_x86_cross_config_defaults.mk`. ### Package @@ -248,23 +288,23 @@ This command generates a DEB installation package. After transferring the packag #### Auto Run -After installing the DEB package with `dpkg`, the program is automatically started by the `APPLaunch.service` systemd service. +After installing the DEB package with `dpkg`, the program is automatically started by the `APPLaunch.service` systemd user service. After installation, use `systemd` commands to view and control it: ```bash -pi@pi:~ $ sudo systemctl status APPLaunch.service +pi@pi:~ $ systemctl --user status APPLaunch.service ● APPLaunch.service - APPLaunch Service - Loaded: loaded (/usr/lib/systemd/system/APPLaunch.service; enabled; preset: enabled) + Loaded: loaded (/usr/lib/systemd/user/APPLaunch.service; enabled; preset: enabled) Active: active (running) since Mon 2026-06-08 15:58:19 CST; 23min ago Invocation: aa5c4e3ca94742deb2fe0dc67467e670 Main PID: 664 (M5CardputerZero) Tasks: 7 (limit: 448) CPU: 1min 19.265s - CGroup: /system.slice/APPLaunch.service + CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/APPLaunch.service └─664 /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch -Jun 08 15:58:19 pi systemd[1]: Started APPLaunch.service - APPLaunch Service. +Jun 08 15:58:19 pi systemd[664]: Started APPLaunch.service - APPLaunch Service. ``` #### Manual Run @@ -296,10 +336,13 @@ The UI is designed and generated by SquareLine Studio 1.5.0, with a resolution o | Main content area | Application carousel pages (left/right page navigation) | | Global hints | ESC / Shift / SYM shortcut hint overlay | -Built-in app pages include: Stock, Music, Camera, LoRa, SSH, GPIO, MIDI, Console, Mesh, Gallery, Email, File, Hack, Rec, Setup, Store, UnitEnv, IpPanel, LovyanGFX, HikePod, Tank Battle, and more. +Built-in app pages include: Game, Setting, Compass, IP Panel, File, SSH, Mesh, Rec, Camera, LoRa, Tank Battle, and Console. APPLaunch can also expose command or external-process entries such as Python, Store, CLI, and Math. ## Related Resources +- [Japanese Project Guide](./docs/launcher-project-guide-ja.md) +- [Japanese APPLaunch App Packaging Guide](./docs/APPLaunch-App-packaging-guide-ja.md) +- [Japanese macOS Docker Build Guide](./docs/macos-docker-build-ja.md) - [M5Stack_Linux_Libs SDK](https://github.com/m5stack/M5Stack_Linux_Libs) - [LVGL Documentation](https://docs.lvgl.io/) - [SquareLine Studio](https://squareline.io/) diff --git a/README_JA.md b/README_JA.md new file mode 100644 index 00000000..a82ffe18 --- /dev/null +++ b/README_JA.md @@ -0,0 +1,349 @@ +# launcher + +[English](./README.md) | [中文](./README_ZH.md) + +[M5Stack_Linux_Libs](https://github.com/m5stack/M5Stack_Linux_Libs) SDK を使用して開発された M5CardputerZero アプリケーション集です。このプロジェクトは、M5CardputerZero(AArch64 Linux)デバイス上で **LVGL 9.5** を使ったグラフィカル UI アプリケーションを構築する方法を示します。 + +このリポジトリには、2 つの主要プロジェクトと、いくつかのサンプル/補助プロジェクトが含まれています。 +- **HelloWorld** - ステータスバーとシンプルな UI を表示する基本的なユーザーデモ +- **APPLaunch** - 複数アプリのナビゲーション、LoRa 通信、オーディオ再生などの豊富な機能を備えたアプリケーションランチャー + +UI は **SquareLine Studio 1.5.0** によって生成され、ローカルデバッグ用の SDL2 シミュレーションモードと、デバイス上で実行する Linux Framebuffer モードの 2 つの表示バックエンドをサポートします。 + +--- + +## プロジェクト構成 + +``` +launcher/ +├── SDK/ # M5Stack_Linux_Libs SDK (git submodule) +│ ├── components/ # Component libraries (lvgl_component, DeviceDriver, etc.) +│ ├── examples/ # SDK examples +│ └── tools/ # Build toolchain scripts (SCons) +├── projects/ +│ ├── UserDemo/ # Basic user demo project +│ ├── APPLaunch/ # Application launcher project (core) +│ │ ├── SConstruct # Top-level project build script +│ │ ├── config_defaults.mk # Default build config (Linux Framebuffer mode) +│ │ ├── darwin_config_defaults.mk # macOS build config +│ │ ├── win_x86_sdl2_config_defaults.mk # Windows SDL2 simulator config +│ │ ├── win_x86_cross_config_defaults.mk # Windows-to-device cross config +│ │ ├── setup.ini # SSH deployment config +│ │ └── main/ # Main application source +│ │ ├── SConstruct # Component build script +│ │ ├── src/ +│ │ │ └── main.cpp # Program entry point +│ │ └── ui/ # UI code +│ │ ├── ui.h / ui.cpp # UI initialization +│ │ ├── launch.cpp / launch.h # App list and launch logic +│ │ ├── app_registry.* # Built-in app enable/disable registry +│ │ ├── ui_launch_page.* # Home carousel UI +│ │ ├── launcher_ui_runtime.* # LVGL runtime/home bootstrap +│ │ ├── animation/ # Animation effects +│ │ └── page_app/ # Built-in app pages +│ ├── Calculator/ # Calculator +│ ├── AppStore/ # App store +│ └── HelloWorld/ # Hello World example +├── ext_components/ # External components (Miniaudio, RadioLib, etc.) +├── docs/ # Project documentation +├── scripts/ # Repository helper tools +├── README.md +├── README_ZH.md +└── README_JA.md +``` + +--- + +## 機能 + +### 全般的な機能 + +- LVGL 9.5 ベースのグラフィカル UI +- 2 つの表示バックエンドをサポートします。 + - **SDL2**: PC 側でのシミュレーションとデバッグ(デフォルトのビルドモード) + - **Linux Framebuffer (ST7789V)**: M5CardputerZero デバイス上で実行 +- evdev キーボード/タッチ入力のサポート(デバイス側) +- SCons + Kconfig ビルドシステム。`scons menuconfig` による柔軟な設定が可能 + +### APPLaunch の機能 + +- 複数アプリのナビゲーションに対応したアプリケーションランチャー UI(カルーセル形式のページ切り替え) +- 組み込みアプリページ: Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、Console、および Python、Store、CLI、Math などのコマンド/外部エントリ +- LoRa 通信(RadioLib SX1262 ベース) +- オーディオ再生(Miniaudio ベース) +- バッテリー状態の監視と表示 +- グローバルショートカットキーのヒント(ESC / Shift / SYM) +- アプリケーションプロセスのロック管理(Home 長押しでアプリを強制終了) +- マルチスレッドタスクプール(C-Thread-Pool) +- macOS クロスコンパイル対応 + +### UserDemo の機能 + +- ステータスバー表示: M5Stack Zero ロゴ、時計時刻、バッテリー残量 +- メインコンテンツ領域: アプリ名 + ページ内容のプレースホルダー + +--- + +## ビルド + +このプロジェクトは主に Linux 環境で開発されており、クロスプラットフォームビルドにも対応しています。詳細なビルド手順は以下のとおりです。 + +### Linux + +#### 依存関係のインストール(初回のみ必要) + +```bash +sudo apt update +sudo apt install python3 python3-pip libffi-dev libsdl2-dev + +pip3 install parse scons requests tqdm +pip3 install setuptools-rust paramiko scp +``` + +> Python のバージョンは 3.8 以上である必要があります。 + +#### リポジトリのクローン(サブモジュールを含む) + +```bash +git clone --recursive https://github.com/CardputerZero/launcher.git +cd launcher +``` + +リポジトリをすでにクローン済みで、サブモジュールがまだ初期化されていない場合: + +```bash +git submodule update --init --recursive +``` + +ビルドするには `HelloWorld` に移動します。 + +#### デバイス上でのビルド + +```bash +# Enter the project directory +cd projects/HelloWorld +# Clean the environment +scons distclean +# Build with 8 threads +scons -j8 +``` + +#### SDL2 シミュレーターのビルド + +```bash +# Enter the project directory +cd projects/HelloWorld +# Load the configuration +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +# Clean the environment +scons distclean +# Build with 8 threads +scons -j8 +``` + +#### SDL2 シミュレーターの実行 + +```bash +cd dist/ +./HelloWorld +``` + +#### クロスコンパイル + +```bash +# Install the cross-compilation toolchain +sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +# Enter the project directory +cd projects/HelloWorld +# Load the configuration +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +# Clean the environment +scons distclean +# Build with 8 threads +scons -j8 +``` + +### macOS + +#### 依存関係のインストール(初回のみ必要) + +```bash +python3 -m venv launcher-python-venv +source launcher-python-venv/bin/activate +pip3 install parse scons requests tqdm +pip3 install setuptools-rust paramiko scp +``` + +> Python のバージョンは 3.8 以上である必要があります。 + +#### リポジトリのクローン(サブモジュールを含む) + +```bash +git clone --recursive https://github.com/CardputerZero/launcher.git +cd launcher +``` + +リポジトリをすでにクローン済みで、サブモジュールがまだ初期化されていない場合: + +```bash +git submodule update --init --recursive +``` + +ビルドするには `HelloWorld` に移動します。 + +#### クロスコンパイル + +```bash +# Install the cross-compilation toolchain +brew tap messense/macos-cross-toolchains +brew install aarch64-unknown-linux-gnu +# Enter the project directory +cd projects/HelloWorld +# Load the configuration +export CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk +# Clean the environment +scons distclean +# Build with 8 threads +scons -j8 +``` + +### Windows + +Windows ビルドでは、`projects/APPLaunch` 配下の同じ SCons エントリポイントを使用します。ネイティブ SDL2 シミュレーターをビルドするには、MSYS2 MinGW シェルを使用し、`gcc`、`g++`、`pkg-config`、SDL2、FreeType が `PATH` で利用できるようにしてください。 + +#### 依存関係のインストール(MSYS2 UCRT64 の例) + +```bash +pacman -S --needed \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-pkgconf \ + mingw-w64-ucrt-x86_64-SDL2 \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-python-pip + +python -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +#### APPLaunch SDL2 シミュレーターのビルド + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch.exe +``` + +#### デバイス向け APPLaunch のクロスコンパイル + +SysGCC Raspberry64 Windows AArch64 Linux クロスツールチェーンを `https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe` からインストールし、`D:\app\SysGCC\bin` にインストールされていない場合は、`projects/APPLaunch/win_x86_cross_config_defaults.mk` の `CONFIG_TOOLCHAIN_PATH` を更新してください。 + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_cross_config_defaults.mk +scons -j8 +``` + +クロスビルドの出力は `dist/M5CardputerZero-APPLaunch` で、CardputerZero デバイス上で実行できます。初回のクロスビルド時には、SDK sysroot パッケージが `SDK/github_source/static_lib_v0.0.4` にダウンロードされる場合があります。 + +### 設定管理コマンド + +```bash +# View/modify build configuration (graphical menu) +scons menuconfig + +# Clean build artifacts +scons -c + +# Full clean (including configuration cache) +scons distclean + +# After configuring the host IP and operation command in setup.ini, +# use scons push to transfer files to the device and execute the specified command. +scons push +``` + +--- + +## APPLaunch + +このプログラムは、CardputerZero デバイス上で動作するデスクトップ UI アプリケーションです。小型 LCD 画面向けの基本操作インターフェースを提供します。 + +### ビルド + +デバイスビルド、SDL2 シミュレータービルド、クロスコンパイルには、上記のプラットフォーム別ビルドコマンドを `cd projects/APPLaunch` とともに使用してください。APPLaunch は、`linux_x86_sdl2_config_defaults.mk`、`linux_x86_cross_cp0_config_defaults.mk`、`mac_cross_cp0_config_defaults.mk`、`win_x86_sdl2_config_defaults.mk`、`win_x86_cross_config_defaults.mk` などの独自設定ファイルを提供しています。 + +### パッケージ + +```bash +# Optional: install dpkg to use dpkg-deb; otherwise the Python builder is used. +python3 scripts/debian_packager.py +``` + +このコマンドは DEB インストールパッケージを生成します。`scp` でパッケージをデバイスに転送した後、`sudo dpkg -i ./***.deb` でインストールします。 + +### 実行 + +#### 自動実行 + +`dpkg` で DEB パッケージをインストールすると、プログラムは `APPLaunch.service` systemd user service によって自動的に起動されます。 + +インストール後は、`systemd` コマンドで状態の確認と制御ができます。 + +```bash +pi@pi:~ $ systemctl --user status APPLaunch.service +● APPLaunch.service - APPLaunch Service + Loaded: loaded (/usr/lib/systemd/user/APPLaunch.service; enabled; preset: enabled) + Active: active (running) since Mon 2026-06-08 15:58:19 CST; 23min ago + Invocation: aa5c4e3ca94742deb2fe0dc67467e670 + Main PID: 664 (M5CardputerZero) + Tasks: 7 (limit: 448) + CPU: 1min 19.265s + CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/APPLaunch.service + └─664 /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch + +Jun 08 15:58:19 pi systemd[664]: Started APPLaunch.service - APPLaunch Service. +``` + +#### 手動実行 + +デバイス上で実行します(Framebuffer を使用)。 + +```bash +# Auto-detect the ST7789V Framebuffer device +./dist/M5CardputerZero-APPLaunch + +# Or manually specify the Framebuffer device +export LV_LINUX_FBDEV_DEVICE=/dev/fb0 +./dist/M5CardputerZero-APPLaunch + +# Specify the keyboard input device +export LV_LINUX_KEYBOARD_DEVICE=/dev/input/by-path/platform-3f804000.i2c-event +./dist/M5CardputerZero-APPLaunch +``` + +### UI の説明 + +UI は SquareLine Studio 1.5.0 によって設計・生成されており、解像度は **320x170**(ST7789V 画面)です。 + +### APPLaunch UI + +| 領域 | 内容 | +|------|---------| +| 上部ステータスバー | ロゴ、時計時刻、バッテリー残量、WiFi 情報 | +| メインコンテンツ領域 | アプリケーションのカルーセルページ(左右のページナビゲーション) | +| グローバルヒント | ESC / Shift / SYM ショートカットヒントのオーバーレイ | + +組み込みアプリページには、Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、Console が含まれます。APPLaunch は、Python、Store、CLI、Math などのコマンドまたは外部プロセスのエントリも表示できます。 + +## 関連リソース + +- [日本語プロジェクトガイド](./docs/launcher-project-guide-ja.md) +- [日本語 APPLaunch App パッケージングガイド](./docs/APPLaunch-App-packaging-guide-ja.md) +- [日本語 macOS Docker ビルドガイド](./docs/macos-docker-build-ja.md) +- [M5Stack_Linux_Libs SDK](https://github.com/m5stack/M5Stack_Linux_Libs) +- [LVGL Documentation](https://docs.lvgl.io/) +- [SquareLine Studio](https://squareline.io/) +- [M5CardputerZero Product Page](https://docs.m5stack.com/) diff --git a/README_ZH.md b/README_ZH.md index 3c8f0cb6..4be2e035 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -1,6 +1,6 @@ # launcher -[English](./README.md) +[English](./README.md) | [日本語](./README_JA.md) 基于 [M5Stack_Linux_Libs](https://github.com/m5stack/M5Stack_Linux_Libs) SDK 开发的 M5CardputerZero 应用集合。该项目展示了如何在 M5CardputerZero(AArch64 Linux)设备上使用 **LVGL 9.5** 构建图形界面应用。 @@ -26,23 +26,21 @@ launcher/ │ │ ├── SConstruct # 项目顶层编译脚本 │ │ ├── config_defaults.mk # 默认编译配置(Linux Framebuffer 模式) │ │ ├── darwin_config_defaults.mk # macOS 编译配置 +│ │ ├── win_x86_sdl2_config_defaults.mk # Windows SDL2 仿真配置 +│ │ ├── win_x86_cross_config_defaults.mk # Windows 到设备交叉编译配置 │ │ ├── setup.ini # SSH 部署配置 │ │ └── main/ # 主程序源码 │ │ ├── SConstruct # 组件编译脚本 │ │ ├── src/ │ │ │ └── main.cpp # 程序入口 -│ │ ├── include/ # 公共头文件(battery、keyboard_input 等) -│ │ ├── hal/ # 硬件抽象层 -│ │ │ ├── sdl/ # SDL2 平台实现(PC 调试) -│ │ │ └── linux/ # Linux 平台实现(设备端) │ │ └── ui/ # UI 代码 -│ │ ├── ui.h / ui.c # UI 初始化 -│ │ ├── screens/ # 屏幕定义 -│ │ ├── components/ # 自定义组件(应用页面、启动器等) -│ │ ├── widgets/ # 自定义控件(轮播组件等) -│ │ ├── Animation/ # 动画效果 -│ │ ├── fonts/ # 字体资源 -│ │ └── images/ # 图片资源 +│ │ ├── ui.h / ui.cpp # UI 初始化 +│ │ ├── launch.cpp / launch.h # 应用列表与启动逻辑 +│ │ ├── app_registry.* # 内置应用开关注册表 +│ │ ├── ui_launch_page.* # 首页轮播 UI +│ │ ├── launcher_ui_runtime.* # LVGL 运行时/首页引导 +│ │ ├── animation/ # 动画效果 +│ │ └── page_app/ # 内置应用页面 │ ├── Calculator/ # 计算器 │ ├── AppStore/ # 应用商店 │ └── HelloWorld/ # Hello World 示例 @@ -50,7 +48,8 @@ launcher/ ├── docs/ # 工程文档 ├── scripts/ # 仓库辅助工具 ├── README.md -└── README_ZH.md +├── README_ZH.md +└── README_JA.md ``` --- @@ -69,7 +68,7 @@ launcher/ ### APPLaunch 特性 - 应用启动器界面,支持多应用导航(轮播翻页) -- 内置应用页面:Stock、Music、Camera、LoRa、SSH、GPIO、MIDI、Console、Mesh 等 +- 内置应用页面:Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、Console,以及 Python、Store、CLI、Math 等命令/外部进程入口 - LoRa 通信(基于 RadioLib SX1262) - 音频播放(基于 Miniaudio) - 电池状态监控与显示 @@ -201,6 +200,47 @@ scons distclean scons -j8 ``` +### Windows + +Windows 使用 `projects/APPLaunch` 下同一套 SCons 入口。编译本机 SDL2 仿真器时,建议使用 MSYS2 MinGW Shell,确保 `gcc`、`g++`、`pkg-config`、SDL2 和 FreeType 都在 `PATH` 中。 + +#### 依赖安装(MSYS2 UCRT64 示例) + +```bash +pacman -S --needed \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-pkgconf \ + mingw-w64-ucrt-x86_64-SDL2 \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-python-pip + +python -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +#### 编译 APPLaunch SDL2 仿真器 + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch.exe +``` + +#### 交叉编译 APPLaunch 设备端程序 + +安装 SysGCC Raspberry64 Windows AArch64 Linux 交叉工具链:`https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe`。如果工具链未安装在 `D:\app\SysGCC\bin`,请同步修改 `projects/APPLaunch/win_x86_cross_config_defaults.mk` 中的 `CONFIG_TOOLCHAIN_PATH`。 + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_cross_config_defaults.mk +scons -j8 +``` + +交叉编译产物为 `dist/M5CardputerZero-APPLaunch`,用于在 CardputerZero 设备上运行。首次交叉编译可能会下载 SDK sysroot 到 `SDK/github_source/static_lib_v0.0.4`。 + ### 配置管理命令 @@ -225,7 +265,7 @@ scons push 该程序是运行在 CardputerZero 设备上的桌面 UI 程序,为 LCD 小屏幕提供基础操作界面。 ### 编译 -参考 HelloWorld 的编译 +设备端编译、SDL2 仿真器和交叉编译均使用上方平台相关命令,并进入 `projects/APPLaunch` 执行。APPLaunch 提供独立配置文件,包括 `linux_x86_sdl2_config_defaults.mk`、`linux_x86_cross_cp0_config_defaults.mk`、`mac_cross_cp0_config_defaults.mk`、`win_x86_sdl2_config_defaults.mk` 和 `win_x86_cross_config_defaults.mk`。 ### 打包 ```bash @@ -237,22 +277,22 @@ python3 scripts/debian_packager.py ### 运行 #### 自动运行 -使用 `dpkg` 安装 DEB 包后,程序会通过 `systemd` 服务 `APPLaunch.service` 自动启动。 +使用 `dpkg` 安装 DEB 包后,程序会通过 `systemd` 用户服务 `APPLaunch.service` 自动启动。 安装后使用 `systemd` 命令进行查看和操作: ```bash -pi@pi:~ $ sudo systemctl status APPLaunch.service +pi@pi:~ $ systemctl --user status APPLaunch.service ● APPLaunch.service - APPLaunch Service - Loaded: loaded (/usr/lib/systemd/system/APPLaunch.service; enabled; preset: enabled) + Loaded: loaded (/usr/lib/systemd/user/APPLaunch.service; enabled; preset: enabled) Active: active (running) since Mon 2026-06-08 15:58:19 CST; 23min ago Invocation: aa5c4e3ca94742deb2fe0dc67467e670 Main PID: 664 (M5CardputerZero) Tasks: 7 (limit: 448) CPU: 1min 19.265s - CGroup: /system.slice/APPLaunch.service + CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/APPLaunch.service └─664 /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch -Jun 08 15:58:19 pi systemd[1]: Started APPLaunch.service - APPLaunch Service. +Jun 08 15:58:19 pi systemd[664]: Started APPLaunch.service - APPLaunch Service. ``` #### 手动运行 @@ -284,11 +324,14 @@ export LV_LINUX_KEYBOARD_DEVICE=/dev/input/by-path/platform-3f804000.i2c-event | 主内容区 | 应用轮播页面(左右翻页导航) | | 全局提示 | ESC / Shift / SYM 快捷键提示覆盖层 | -内置应用页面包括:Stock、Music、Camera、LoRa、SSH、GPIO、MIDI、Console、Mesh、Gallery、Email、File、Hack、Rec、Setup、Store、UnitEnv、IpPanel、LovyanGFX、HikePod、Tank Battle 等。 +内置应用页面包括:Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle 和 Console。APPLaunch 也可以显示 Python、Store、CLI、Math 等命令或外部进程入口。 ## 相关资源 +- [日文工程指南](./docs/launcher-project-guide-ja.md) +- [日文 APPLaunch App 打包指南](./docs/APPLaunch-App-packaging-guide-ja.md) +- [日文 macOS Docker 编译指南](./docs/macos-docker-build-ja.md) - [M5Stack_Linux_Libs SDK](https://github.com/m5stack/M5Stack_Linux_Libs) - [LVGL 文档](https://docs.lvgl.io/) - [SquareLine Studio](https://squareline.io/) diff --git a/docs/APPLaunch-App-packaging-guide-ja.md b/docs/APPLaunch-App-packaging-guide-ja.md new file mode 100644 index 00000000..b4ec3fab --- /dev/null +++ b/docs/APPLaunch-App-packaging-guide-ja.md @@ -0,0 +1,404 @@ +# M5CardputerZero APPLaunch App パッケージングガイド + +## 目次 + +- [1. 概要](#1-概要) +- [2. .desktop ショートカットファイル](#2-desktop-ショートカットファイル) + - [2.1 フィールド説明](#21-フィールド説明) + - [2.2 完全な例](#22-完全な例) + - [2.3 インストール方法](#23-インストール方法) +- [3. Debian パッケージ構造](#3-debian-パッケージ構造) + - [3.1 各ファイルの説明](#31-各ファイルの説明) +- [4. パッケージング手順](#4-パッケージング手順) +- [5. クイックデプロイ(ショートカット登録のみ)](#5-クイックデプロイショートカット登録のみ) +- [6. よくある質問](#6-よくある質問) + +--- + + +## 1. 概要 + +M5CardputerZero APPLaunch は、デバイス上で動作するアプリケーションランチャーです。 +`/usr/share/APPLaunch/applications/` ディレクトリ内にある `.desktop` で終わるすべてのファイルをスキャンし、 +選択可能な App としてメイン画面のスライドリストに追加します。APPLaunch 起動後はディレクトリ watcher により、 +約 3 秒ごとに変更を確認します。そのため `.desktop` の追加、削除、変更後は通常、自動的にリストが更新されます。 + +そのため、**APPLaunch に新しい App を追加する方法は 2 つあります**: + +| 方法 | 適した用途 | +|------|------------| +| デバイス上に `.desktop` ファイルを直接配置する | すばやい検証、一時的な追加 | +| Debian パッケージ(`.deb`)を作成してインストールする | 正式リリース、永続的なインストール | + +> APPLaunch のメインプログラムは `/usr/share/APPLaunch/` にインストールされ、 +> 作業ディレクトリも `/usr/share/APPLaunch/` です。 +> そのため `.desktop` ファイル内の **相対パスはすべてこのディレクトリをルートとして扱います**。 + + +## 2. .desktop ショートカットファイル + +APPLaunch は XDG Desktop Entry に似た形式の `.desktop` ファイルで App を記述します。 +ファイルはデバイスの `/usr/share/APPLaunch/applications/` ディレクトリに配置する必要があり、ファイル名は +**必ず `.desktop` で終わる必要があります**(例:`myapp.desktop`)。 + + +### 2.1 フィールド説明 + +ファイルには `[Desktop Entry]` セクションヘッダーが必要です。APPLaunch が認識するフィールドは次のとおりです: + +| フィールド | 必須 | 型 | 説明 | +|------|----------|------|------| +| `Name` | **必須** | 文字列 | ランチャーリストに表示される App 名 | +| `Exec` | **必須** | 文字列 | 実行するコマンド、または実行ファイルのパス | +| `Icon` | 任意 | 文字列 | アイコンパス(`/usr/share/APPLaunch/` からの相対パス、または絶対パス) | +| `Terminal` | 任意 | `true`/`false` | 内蔵ターミナル(UIConsolePage)で実行するかどうか。デフォルトは `false` | +| `Sysplause` | 任意 | `true`/`false` | ターミナル実行終了後に「任意のキーで戻る」プロンプトを表示するかどうか。デフォルトは `true` | +| `Type` | 任意 | 文字列 | `Application` を固定で指定(ツールチェーン識別用。APPLaunch 自体は検証しません) | +| `TryExec` | 任意 | 文字列 | ドキュメント説明用のみ。APPLaunch はこのフィールドを解析しません | + +> **注意:** フィールド解析時には key と value の両端の空白/Tab が自動的に取り除かれます。 +> 空行、および `#` または `;` で始まる行はコメントとしてスキップされます。 + +> **Exec の安全制限:** APPLaunch は `Exec` を検証します。shell メタ文字を含むコマンドは拒否されます。 +> `/` を含むパスは実行可能ファイルを指している必要があります。パスを含まないコマンドは現在、`bash`、`sh`、 +> `python3`、`vim`、`vi`、`nano` などの内蔵ホワイトリストコマンドのみ許可されます。 + +#### `Terminal` と `Sysplause` の動作 + +- `Terminal=false`(デフォルト):APPLaunch は `fork` + `execlp` で外部プログラムを直接起動します。 + プログラム実行中、ランチャーの更新は一時停止します。プログラム終了後は自動的にメイン画面へ戻ります。 + Home キーを 5 秒間長押しすると `SIGINT` を送信し、さらに 3 秒待っても終了しない場合は `SIGKILL` を送信します。 +- `Terminal=true`:APPLaunch は内蔵ターミナル画面(UIConsolePage)でコマンドを実行し、 + キーボード入力と出力表示に対応します。 +- `Sysplause=true`(デフォルト。`Terminal=true` の場合のみ有効):コマンド終了後に + "Press any key to return..." を表示し、ユーザーの確認を待ってからメイン画面へ戻ります。 +- `Sysplause=false`:コマンド終了後、ただちにメイン画面へ戻ります。 + + +### 2.2 完全な例 + +**例 1 – ターミナル内で vim を実行(APPLaunch に同梱されるテンプレート)** + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/email.png +Type=Application +``` + +**例 2 – GUI プログラムを直接起動(内蔵ターミナルを使用しない)** + +```ini +[Desktop Entry] +Name=Calculator +Exec=/home/pi/M5CardputerZero-Calculator-linux-aarch64 +Terminal=false +Icon=share/images/math.png +Type=Application +``` + +**例 3 – ターミナル内でスクリプトを実行し、終了後すぐに戻る** + +```ini +[Desktop Entry] +Name=MyScript +Exec=/home/pi/my_script.sh +Terminal=true +Sysplause=false +Icon=share/images/hack.png +Type=Application +``` + +**例 4 – システムコマンドを使用(bash 内蔵。完全パス、または PATH から到達可能であること)** + +```ini +[Desktop Entry] +Name=Python3 +Exec=python3 +Terminal=true +Icon=share/images/python.png +Type=Application +``` + + +### 2.3 インストール方法 + +**方法 A:デバイスへ直接コピー(SSH)** + +```bash +# 在开发机上执行 +scp myapp.desktop pi@:/tmp/ +ssh pi@ "sudo install -m 0644 /tmp/myapp.desktop /usr/share/APPLaunch/applications/" + +# 通常 3 秒内会自动刷新;如需强制刷新,可重启用户服务 +ssh pi@ "systemctl --user restart APPLaunch.service" +``` + +**方法 B:デバイス上で直接作成** + +```bash +sudo tee /usr/share/APPLaunch/applications/myapp.desktop > /dev/null << 'EOF' +[Desktop Entry] +Name=MyApp +Exec=/home/pi/myapp +Terminal=false +Icon=share/images/email.png +Type=Application +EOF + +# 通常 3 秒内会自动刷新;如需强制刷新: +systemctl --user restart APPLaunch.service +``` + + +## 3. Debian パッケージ構造 + +App を Debian パッケージとして配布する場合、パッケージングディレクトリは次の固定構造に従う必要があります: + +``` +debian-/ +├── DEBIAN/ +│ ├── control # 包元数据 +│ ├── postinst # 安装后脚本(启动服务) +│ └── prerm # 卸载前脚本(停止服务) +└── usr/ + └── share/ + └── APPLaunch/ # 主程序安装根目录(WorkingDirectory) + ├── applications/ + │ └── myapp.desktop # App 快捷方式(必须以 .desktop 结尾) + ├── bin/ + │ └── M5CardputerZero-demo # 主程序可执行文件 + ├── lib/ + │ └── lvgl.so # 动态库 + └── share/ + ├── font/ + │ └── *.ttf # 字体文件 + └── images/ + └── *.png # 图标 / 图片资源 +``` + +パッケージングコマンド: +```bash +dpkg-deb -b debian- _-_arm64.deb +# 示例: +dpkg-deb -b debian-example example_0.1-m5stack1_arm64.deb +``` + + + +### 3.1 各ファイルの説明 + +#### `DEBIAN/control` + +パッケージのメタデータ記述ファイルです。形式は次のとおりです: + +``` +Package: applaunch +Version: 0.1 +Architecture: arm64 +Maintainer: yourname +Original-Maintainer: m5stack +Section: APPLaunch +Priority: optional +Homepage: https://www.m5stack.com +Packaged-Date: 2026-04-27 12:03:35 +Description: M5CardputerZero APPLaunch +``` + +| フィールド | 説明 | +|------|------| +| `Package` | パッケージ名(小文字、空白なし) | +| `Version` | バージョン番号。例:`0.1` | +| `Architecture` | `arm64` を固定で指定(M5CardputerZero プラットフォーム) | +| `Maintainer` | メンテナー情報 | +| `Section` | カテゴリ。`APPLaunch` を指定 | + +> `WorkingDirectory=/usr/share/APPLaunch` により、`.desktop` ファイル内の +> 相対パス(例:`share/images/email.png`)の基準ディレクトリが決まります。 + +#### `usr/share/APPLaunch/applications/.desktop` + +APPLaunch に登録される App ショートカットです。形式は第 2 節を参照してください。 +ファイル名は必ず `.desktop` で終わる必要があります。 + +#### `usr/share/APPLaunch/share/images/*.png` + +`.desktop` ファイルの `Icon` フィールドから参照されるアイコンファイルです。 +参照例:`Icon=share/images/myapp.png` + +#### `usr/share/APPLaunch/share/font/*.ttf` + +APPLaunch UI が使用するフォントファイルです(メインプログラムと一緒に配布されます)。 + + +## 4. パッケージング手順 + +--- + +### 4.2 手動パッケージング手順 + +スクリプトを使用しない場合は、次の手順で手動作業できます: + +**手順 1:ディレクトリ構造を作成** + +```bash +PKG=debian-myapp +mkdir -p $PKG/DEBIAN +mkdir -p $PKG/usr/share/APPLaunch/bin +mkdir -p $PKG/usr/share/APPLaunch/lib +mkdir -p $PKG/usr/share/APPLaunch/share/font +mkdir -p $PKG/usr/share/APPLaunch/share/images +mkdir -p $PKG/usr/share/APPLaunch/applications +``` + +**手順 2:メインプログラムとリソースファイルを配置** + +```bash +cp /path/to/myapp $PKG/usr/share/APPLaunch/bin/ +cp /path/to/share/font/*.ttf $PKG/usr/share/APPLaunch/share/font/ +cp /path/to/share/images/*.png $PKG/usr/share/APPLaunch/share/images/ +touch $PKG/usr/share/APPLaunch/lib/lvgl.so # 占位,实际为动态库 +``` + +**手順 3:App ショートカットを追加** + +```bash +cat > $PKG/usr/share/APPLaunch/applications/myapp.desktop << 'EOF' +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/myapp +Terminal=false +Icon=share/images/myapp.png +Type=Application +EOF +``` + +**手順 4:制御ファイルを記述** + +```bash +cat > $PKG/DEBIAN/control << 'EOF' +Package: myapp +Version: 0.1 +Architecture: arm64 +Maintainer: yourname +Section: APPLaunch +Priority: optional +Homepage: https://www.m5stack.com +Description: M5CardputerZero MyApp +EOF +``` + + + +**手順 7:実行権限を設定** + +```bash +chmod 755 $PKG/usr/share/APPLaunch/bin/myapp +``` + +**手順 8:パッケージを作成** + +```bash +dpkg-deb -b $PKG myapp_0.1-m5stack1_arm64.deb +``` + +**手順 9:デバイスへデプロイ** + +```bash +scp myapp_0.1-m5stack1_arm64.deb pi@:/tmp/ +ssh pi@ "sudo dpkg -i /tmp/myapp_0.1-m5stack1_arm64.deb" +``` + +--- + + +## 5. クイックデプロイ(ショートカット登録のみ) + +App の実行ファイルがすでにデバイス上に存在し、APPLaunch に入口を追加するだけでよい場合は、 +完全なパッケージング手順を省略して、次のように直接操作できます: + +```bash +# 1. SSH 登录设备 +ssh pi@ + +# 2. 在 applications 目录创建 .desktop 文件 +sudo tee /usr/share/APPLaunch/applications/myapp.desktop > /dev/null << 'EOF' +[Desktop Entry] +Name=MyApp +Exec=/home/pi/myapp +Terminal=false +Icon=share/images/email.png +Type=Application +EOF + +# 3. 可选:上传自定义图标(普通用户通常没有 /usr/share 写权限,先传 /tmp 再安装) +# (在开发机执行) +scp myapp_icon.png pi@:/tmp/ +ssh pi@ "sudo install -m 0644 /tmp/myapp_icon.png /usr/share/APPLaunch/share/images/" + +# 4. 通常 3 秒内会自动刷新;如需强制刷新: +systemctl --user restart APPLaunch.service +``` + +> **ヒント:** APPLaunch 起動後は約 3 秒ごとに `applications/` ディレクトリの変更を確認します。 +> そのため `.desktop` の追加、削除、変更後も通常はサービスを再起動する必要はありません。 + +--- + + +## 6. よくある質問 + +**Q1:`.desktop` ファイルを配置したのに、App がリストに表示されません。** + +- ファイル名が `.desktop` で終わっているか確認してください(例:`myapp.desktop`。`myapp.desktop.temple` は不可)。 +- ファイルに `[Desktop Entry]` セクションヘッダーがあり、`Name` と `Exec` フィールドがどちらも空でないことを確認してください。 +- ディレクトリ watcher による自動更新を待つか、`systemctl --user restart APPLaunch.service` を実行してから確認してください。 +- ログを確認してください:`journalctl --user -u APPLaunch.service -f` + +**Q2:アイコンが空白、またはデフォルトアイコンで表示されます。** + +- `Icon` フィールドのパスが正しいことを確認してください。相対パスは `/usr/share/APPLaunch/` をルートとします。 + たとえば `Icon=share/images/myapp.png` は実際のパス + `/usr/share/APPLaunch/share/images/myapp.png` に対応します。 +- 絶対パスも使用できます。例:`Icon=/home/pi/myapp_icon.png`。 + +**Q3:`Terminal=true` の App を実行したあと、キーボードが反応しません。** + +- デバイスのキーボードドライバーが正常に動作していることを確認してください。CLI(bash)で入力をテストできます。 +- プログラムが特殊なターミナル環境(例:`ncurses`)を必要としていないか確認してください。APPLaunch の内蔵ターミナルは単純な + 擬似端末(pty)ですが、基本的な I/O は正常に動作します。 + +**Q4:実行中の外部 App(`Terminal=false`)を強制終了するには?** + +- Home キーを 5 秒間長押しすると、APPLaunch が App に `SIGINT` を送信します。 +- App が 3 秒以内に `SIGINT` に応答しない場合、APPLaunch は `SIGKILL` を送信して強制終了します。 + +**Q5:インストール済みの APPLaunch deb パッケージをアンインストールするには?** + +```bash +sudo dpkg -r applaunch +``` + +**Q6:パッケージング時に `dpkg-deb: error: failed to open package info file` が出ます。** + +- `DEBIAN/control` ファイルの形式を確認し、フィールド名の直後に `:` と空白が続いていること、 + さらにファイル末尾に改行があることを確認してください。 +- `DEBIAN/postinst` と `DEBIAN/prerm` ファイルの権限が `755` であることを確認してください。 + +**Q7:`.deb` パッケージの命名規則は?** + +``` +{package_name}_{version}-{revision}_{architecture}.deb +# 示例: +applaunch_0.1-m5stack1_arm64.deb +``` + +| 部分 | 説明 | +|------|------| +| `package_name` | `DEBIAN/control` の `Package` フィールドと一致。小文字 | +| `version` | ソフトウェアバージョン。例:`0.1` | +| `revision` | パッケージングリビジョン。例:`m5stack1` | +| `architecture` | `arm64` 固定 | diff --git "a/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" "b/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" index f7686732..41960ead 100644 --- "a/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" +++ "b/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" @@ -18,9 +18,10 @@ ## 1. 概述 -M5CardputerZero APPLaunch 是运行在设备上的应用启动器。它在启动时会扫描目录 +M5CardputerZero APPLaunch 是运行在设备上的应用启动器。它会扫描目录 `/usr/share/APPLaunch/applications/` 下所有以 `.desktop` 结尾的文件,并将其 -作为可选 App 添加到主界面的滑动列表中。 +作为可选 App 添加到主界面的滑动列表中。启动后 APPLaunch 还会通过目录 watcher +约每 3 秒检查一次变化,新增、删除或修改 `.desktop` 后通常会自动刷新列表。 因此,**为 APPLaunch 添加一个新 App 有两种方式**: @@ -58,6 +59,10 @@ APPLaunch 使用类 XDG Desktop Entry 格式的 `.desktop` 文件来描述一个 > **注意:** 字段解析时会自动去除 key 和 value 两端的空格/Tab; > 空行和以 `#` 或 `;` 开头的行作为注释被跳过。 +> **Exec 安全限制:** APPLaunch 会校验 `Exec`。包含 shell 元字符的命令会被拒绝; +> 带 `/` 的路径必须指向可执行文件;不带路径的命令目前仅允许 `bash`、`sh`、 +> `python3`、`vim`、`vi`、`nano` 等内置白名单命令。 + #### `Terminal` 与 `Sysplause` 的行为说明 - `Terminal=false`(默认):APPLaunch 通过 `fork` + `execlp` 直接启动外部程序, @@ -125,10 +130,11 @@ Type=Application ```bash # 在开发机上执行 -scp myapp.desktop pi@:/usr/share/APPLaunch/applications/ +scp myapp.desktop pi@:/tmp/ +ssh pi@ "sudo install -m 0644 /tmp/myapp.desktop /usr/share/APPLaunch/applications/" -# 重启 APPLaunch 服务使其生效 -ssh pi@ "sudo systemctl restart APPLaunch.service" +# 通常 3 秒内会自动刷新;如需强制刷新,可重启用户服务 +ssh pi@ "systemctl --user restart APPLaunch.service" ``` **方式 B:在设备上直接创建** @@ -143,7 +149,8 @@ Icon=share/images/email.png Type=Application EOF -sudo systemctl restart APPLaunch.service +# 通常 3 秒内会自动刷新;如需强制刷新: +systemctl --user restart APPLaunch.service ``` @@ -240,7 +247,6 @@ APPLaunch UI 使用的字体文件(随主程序一起发布)。 ```bash PKG=debian-myapp mkdir -p $PKG/DEBIAN -mkdir -p $PKG/lib/systemd/system mkdir -p $PKG/usr/share/APPLaunch/bin mkdir -p $PKG/usr/share/APPLaunch/lib mkdir -p $PKG/usr/share/APPLaunch/share/font @@ -274,11 +280,11 @@ EOF ```bash cat > $PKG/DEBIAN/control << 'EOF' -Package: MyApp +Package: myapp Version: 0.1 Architecture: arm64 Maintainer: yourname -Section: MyApp +Section: APPLaunch Priority: optional Homepage: https://www.m5stack.com Description: M5CardputerZero MyApp @@ -328,16 +334,17 @@ Icon=share/images/email.png Type=Application EOF -# 3. 可选:上传自定义图标 +# 3. 可选:上传自定义图标(普通用户通常没有 /usr/share 写权限,先传 /tmp 再安装) # (在开发机执行) -scp myapp_icon.png pi@:/usr/share/APPLaunch/share/images/ +scp myapp_icon.png pi@:/tmp/ +ssh pi@ "sudo install -m 0644 /tmp/myapp_icon.png /usr/share/APPLaunch/share/images/" -# 4. 重启 APPLaunch 使新 App 生效 -sudo systemctl restart APPLaunch.service +# 4. 通常 3 秒内会自动刷新;如需强制刷新: +systemctl --user restart APPLaunch.service ``` -> **提示:** APPLaunch 每次启动时才扫描 `applications/` 目录, -> 因此也可以不重启服务,直接重启设备或重启 APPLaunch 进程来加载新 App。 +> **提示:** APPLaunch 启动后会约每 3 秒检查一次 `applications/` 目录变化, +> 因此新增、删除或修改 `.desktop` 后通常不需要重启服务。 --- @@ -348,8 +355,8 @@ sudo systemctl restart APPLaunch.service - 检查文件名是否以 `.desktop` 结尾(如 `myapp.desktop`,不能是 `myapp.desktop.temple`)。 - 检查文件是否包含 `[Desktop Entry]` 节头,且 `Name` 和 `Exec` 字段均不为空。 -- 执行 `sudo systemctl restart APPLaunch.service` 重启服务后再观察。 -- 查看日志:`sudo journalctl -u APPLaunch.service -f` +- 等待目录 watcher 自动刷新,或执行 `systemctl --user restart APPLaunch.service` 后再观察。 +- 查看日志:`journalctl --user -u APPLaunch.service -f` **Q2:图标显示为空白或默认图标。** diff --git a/docs/launcher-project-guide-ja.md b/docs/launcher-project-guide-ja.md new file mode 100644 index 00000000..30644289 --- /dev/null +++ b/docs/launcher-project-guide-ja.md @@ -0,0 +1,29 @@ +# Launcher プロジェクトガイド + +このドキュメントセットは `launcher` リポジトリ、特に `projects/APPLaunch` ランチャープロジェクトを中心に、アーキテクチャ、ソースフレームワーク、ビルドフロー、実行時の動作、パッケージング、デプロイ、拡張ポイント、トラブルシューティングを説明します。 + +推奨する読み進め方: + +1. [00 - Overview and Reading Path](launcher-project-guide-ja/00-overview-and-reading-path.md) +2. [01 - Project Layout and Module Responsibilities](launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md) +3. [02 - Runtime Framework and Boot Flow](launcher-project-guide-ja/02-runtime-framework-and-boot-flow.md) +4. [03 - UI Framework and Home Carousel](launcher-project-guide-ja/03-ui-framework-and-home-carousel.md) +5. [04 - Application Model and Launch Mechanism](launcher-project-guide-ja/04-application-model-and-launch-mechanism.md) +6. [05 - Built-in Page Framework](launcher-project-guide-ja/05-built-in-page-framework.md) +7. [06 - Resources and Configuration](launcher-project-guide-ja/06-resources-and-configuration.md) +8. [07 - Input System and Key Mapping](launcher-project-guide-ja/07-input-system-and-key-mapping.md) +9. [08 - Build and Compilation Guide](launcher-project-guide-ja/08-build-and-compilation-guide.md) +10. [09 - Packaging, Deployment, and systemd](launcher-project-guide-ja/09-packaging-deployment-and-systemd.md) +11. [10 - Extension Development Guide](launcher-project-guide-ja/10-extension-development-guide.md) +12. [11 - Debugging and Troubleshooting](launcher-project-guide-ja/11-debugging-and-troubleshooting.md) +13. [12 - Common Modification Entry Points](launcher-project-guide-ja/12-common-modification-entry-points.md) + +## Quick Links + +- ビルドして実行するには: [08 - Build and Compilation Guide](launcher-project-guide-ja/08-build-and-compilation-guide.md) を参照してください。 +- アプリの起動方法を理解するには: [04 - Application Model and Launch Mechanism](launcher-project-guide-ja/04-application-model-and-launch-mechanism.md) を参照してください。 +- ホーム UI を変更するには: [03 - UI Framework and Home Carousel](launcher-project-guide-ja/03-ui-framework-and-home-carousel.md) を参照してください。 +- 組み込みページを追加するには: [10 - Extension Development Guide](launcher-project-guide-ja/10-extension-development-guide.md) を参照してください。 +- 黒画面や起動失敗をデバッグするには: [11 - Debugging and Troubleshooting](launcher-project-guide-ja/11-debugging-and-troubleshooting.md) を参照してください。 + +English version: [Launcher Project Guide](launcher-project-guide.md) | 中文版: [Launcher 工程详细说明](launcher工程详细说明.md) diff --git a/docs/launcher-project-guide-ja/00-overview-and-reading-path.md b/docs/launcher-project-guide-ja/00-overview-and-reading-path.md new file mode 100644 index 00000000..396eb837 --- /dev/null +++ b/docs/launcher-project-guide-ja/00-overview-and-reading-path.md @@ -0,0 +1,87 @@ +# 00 - Overview and Reading Path + +`launcher` は M5CardputerZero 向けのアプリケーションプロジェクト群です。中核となるプロジェクトは `projects/APPLaunch` です。APPLaunch はデバイス上で動作するメインランチャーで、起動後に LVGL を初期化し、ホームカルーセルを表示し、ステータスバーを表示し、組み込みページまたは外部アプリケーションを起動します。また、設定、端末、音楽、録音、カメラ、LoRa などの機能も提供します。 + +## 1. Documentation Goals + +このドキュメントセットは、次の疑問に答えることを目的としています。 + +- このリポジトリの各ディレクトリは何を担当しているか。 +- APPLaunch プロセスはどのように開始し、メインループはどこにあるか。 +- ホームカルーセル UI はどのように構成され、更新されるか。 +- 組み込みページと外部アプリケーションは、ランチャーへどのように統一的に登録されるか。 +- 動的な `.desktop` アプリケーションはどのようにスキャンされ、起動されるか。 +- リソース、フォント、画像、音声のパスはどのように解決されるか。 +- SDL2 シミュレーション、デバイス上のネイティブビルド、クロスコンパイルはどのように機能するか。 +- プロジェクトを `.deb` としてパッケージ化し、systemd で自動起動するにはどうするか。 +- ページ、外部アプリケーション、リソースを追加するにはどうするか。 +- 黒画面、リソース欠落、戻れない外部アプリケーションをどう調査するか。 + +## 2. Project in One Sentence + +APPLaunch は、小さな LVGL ベースのデスクトップ環境として理解できます。 + +```text +Linux device / SDL2 simulation + | + v +cp0_lvgl platform adaptation layer + | + v +LVGL 9.5 UI framework + | + v +APPLaunch home, status bar, carousel, application manager + | + +--> Built-in page AppPage + +--> PTY terminal application UIConsolePage + +--> External independent process cp0_process_exec_blocking() +``` + +## 3. Recommended Reading Order + +このプロジェクトが初めての場合は、次の順序で読んでください。 + +1. `01-project-layout-and-module-responsibilities.md`: ディレクトリ構造の全体像をつかみます。 +2. `02-runtime-framework-and-boot-flow.md`: `main()` からホーム画面までの経路を理解します。 +3. `03-ui-framework-and-home-carousel.md`: ホーム UI とカルーセルカードを理解します。 +4. `04-application-model-and-launch-mechanism.md`: アプリケーションリストと起動方式を理解します。 +5. `08-build-and-compilation-guide.md`: プロジェクトをビルドして実行します。 +6. `10-extension-development-guide.md`: ページまたはアプリケーションを追加します。 + +特定の作業だけを行いたい場合: + +| Task | Read | +| --- | --- | +| SDL2 版をローカルでビルドして実行する | `08-build-and-compilation-guide.md` | +| デバイス向けにクロスコンパイルする | `08-build-and-compilation-guide.md` | +| `.deb` をパッケージ化する | `09-packaging-deployment-and-systemd.md` | +| ホームカードのレイアウトを変更する | `03-ui-framework-and-home-carousel.md` | +| 組み込みページを追加する | `10-extension-development-guide.md` | +| `.desktop` 外部アプリケーションを追加する | `04-application-model-and-launch-mechanism.md`, `10-extension-development-guide.md` | +| 黒画面を調査する | `11-debugging-and-troubleshooting.md` | +| 機能のエントリファイルを探す | `12-common-modification-entry-points.md` | + +## 4. Key Project Paths + +| Path | Description | +| --- | --- | +| `projects/APPLaunch` | メインランチャープロジェクト | +| `projects/APPLaunch/main/src/main.cpp` | APPLaunch のエントリポイントと LVGL メインループ | +| `projects/APPLaunch/main/ui/launch.cpp` | アプリケーションリスト、起動ロジック、ステータスバー更新 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | ホーム UI、カルーセル、ホームキー処理 | +| `projects/APPLaunch/main/ui/page_app` | 組み込みページの実装 | +| `projects/APPLaunch/APPLaunch` | 実行環境へパッケージされるリソースツリー | +| `ext_components/cp0_lvgl` | ファイル、プロセス、入力、システムインターフェースをラップするプラットフォーム適応レイヤー | +| `scripts/debian_packager.py` | Debian パッケージビルドスクリプト | + +## 5. Terminology + +- **APPLaunch**: ランチャープロジェクト、またはランチャープロセス。 +- **Home screen**: ステータスバーとアプリケーションカルーセルを持つ APPLaunch のメイン画面。 +- **Built-in page**: `UISetupPage` など、APPLaunch プロセスにコンパイルされるページクラス。 +- **Terminal application**: `bash` のように、`UIConsolePage` + PTY を通して APPLaunch 内で実行されるコマンド。 +- **External application**: 独立した実行可能プログラム。起動時、APPLaunch は自身の LVGL レンダリングを一時停止し、外部プログラムの終了を待ちます。 +- **Resource tree**: `APPLaunch/share/images`、`APPLaunch/share/audio`、`APPLaunch/share/font` などの実行時ファイル。 +- **On-device**: M5CardputerZero 上の AArch64 Linux 環境。 +- **SDL2 mode**: 開発マシン上の SDL2 ウィンドウでシミュレーション実行するモード。 diff --git a/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md new file mode 100644 index 00000000..ecb8eb37 --- /dev/null +++ b/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md @@ -0,0 +1,249 @@ +# 01 - Project Layout and Module Responsibilities + +この章では、リポジトリ全体の構成と、APPLaunch プロジェクト内部の構成を説明します。 + +## 1. Overall Repository Structure + +```text +launcher/ +├── SDK/ +├── ext_components/ +├── projects/ +├── docs/ +├── README.md +└── README_ZH.md +``` + +### 1.1 `SDK/` + +`SDK` は `M5Stack_Linux_Libs` で、このプロジェクトに次のものを提供します。 + +- SCons/Kconfig ビルドフレームワーク。 +- LVGL コンポーネント。 +- デバイスドライバ、ユーティリティ関数、サンプルコード。 +- ビルドスクリプトとコンポーネント登録機構。 + +APPLaunch の `SConstruct` は次を設定します。 + +```python +os.environ["SDK_PATH"] = str(sdk_path) +``` + +続いて次を呼び出します。 + +```python +env = SConscript( + str(sdk_path / "tools" / "scons" / "project.py"), + variant_dir=os.getcwd(), + duplicate=0, +) +``` + +### 1.2 `ext_components/` + +`ext_components` は、このリポジトリの拡張コンポーネントディレクトリです。APPLaunch は `EXT_COMPONENTS_PATH` を通してこれを取り込みます。 + +```text +ext_components/ +├── cp0_lvgl/ +├── Miniaudio/ +├── RadioLib/ +└── Sigslot/ +``` + +| Component | Role | +| --- | --- | +| `cp0_lvgl` | CardputerZero プラットフォーム適応。LVGL 初期化、ファイルパス、入力、プロセス、PTY、システム機能をラップする | +| `Miniaudio` | 音声再生と録音の依存ライブラリ | +| `Sigslot` | signal-slot 機構 | +| `RadioLib` | LoRa/SX126x 無線通信ライブラリコンポーネント | + +### 1.3 `projects/` + +```text +projects/ +├── APPLaunch/ +├── AppStore/ +├── Calculator/ +├── CardputerZero-Emulator/ +├── HelloWorld/ +└── UserDemo/ +``` + +| Project | Description | +| --- | --- | +| `APPLaunch` | メインランチャー。このドキュメントの主対象 | +| `AppStore` | アプリケーションストア。APPLaunch から外部アプリケーションとして起動できる | +| `Calculator` | 電卓アプリケーション。APPLaunch から起動できる | +| `CardputerZero-Emulator` | デバイスエミュレータ | +| `HelloWorld` | ビルドフロー学習用の最小サンプルプロジェクト | +| `UserDemo` | ユーザーデモプロジェクト | + +### 1.4 `docs/`, `scripts/`, and Runtime Helpers + +- `docs/`: 開発者向けドキュメントと単独のパッケージング文書。`APPLaunch-App-打包指南.md` などを含みます。 +- `scripts/`: `firmware_manager.py` や `debian_packager.py` など、リポジトリレベルの補助ツール。 +- `projects/APPLaunch/APPLaunch/bin/`: `/usr/share/APPLaunch/bin/` にコピーされる APPLaunch 実行時ヘルパースクリプト。`store_cache_sync.py` などを含みます。 + +## 2. APPLaunch Top-Level Structure + +```text +projects/APPLaunch/ +├── APPLaunch/ +├── main/ +├── tools/ +├── docs/ +├── SConstruct +├── config_defaults.mk +├── linux_x86_sdl2_config_defaults.mk +├── linux_x86_cross_cp0_config_defaults.mk +├── mac_cross_cp0_config_defaults.mk +├── darwin_config_defaults.mk +└── setup.ini +``` + +### 2.1 Top-Level Build Files + +| File | Description | +| --- | --- | +| `SConstruct` | プロジェクトのエントリポイント。デフォルト設定、SDK パス、クロスコンパイル sysroot を選び、SDK ビルドシステムを呼び出す | +| `config_defaults.mk` | デバイス上のデフォルト設定。Linux framebuffer / evdev を有効化する | +| `linux_x86_sdl2_config_defaults.mk` | Linux x86 SDL2 シミュレーション設定 | +| `linux_x86_cross_cp0_config_defaults.mk` | AArch64 向け Linux x86 クロスコンパイル設定 | +| `mac_cross_cp0_config_defaults.mk` | AArch64 向け macOS クロスコンパイル設定 | +| `darwin_config_defaults.mk` | macOS SDL / Darwin 関連設定 | + +### 2.2 `APPLaunch/` Runtime Resource Tree + +```text +projects/APPLaunch/APPLaunch/ +├── applications/ +│ └── vim.desktop.temple +├── bin/ +│ └── store_cache_sync.py +├── lib/ +│ └── nihao.so +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +このディレクトリは、ビルドまたはパッケージ作成時に実行時ディレクトリへコピーされます。デバイスにインストールされた後は、通常次に対応します。 + +```text +/usr/share/APPLaunch/ +``` + +リソースツリーの責務: + +- `applications/`: 外部アプリケーション用の `.desktop` 記述ファイルを格納します。 +- `share/images/`: アプリケーションアイコン、ホームカルーセル画像、ステータスバー画像、ページ画像。 +- `share/audio/`: 起動音、キー音、切り替え音。 +- `share/font/`: TTF フォント。 +- `lib/`: パッケージに同梱されるライブラリファイル。 + +### 2.3 `main/` Main Source Directory + +```text +projects/APPLaunch/main/ +├── Kconfig +├── SConstruct +├── include/ +├── src/ +└── ui/ +``` + +| Path | Description | +| --- | --- | +| `Kconfig` | コンポーネント設定のエントリポイント | +| `SConstruct` | APPLaunch のビルドターゲットと依存関係を登録する | +| `include/` | APPLaunch のプライベートヘッダと互換ヘッダ | +| `src/main.cpp` | プロセスのエントリポイント、LVGL 初期化、メインループ | +| `ui/` | すべての UI ページ、ホーム画面、アニメーション、Loading などの実装 | + +### 2.4 `main/ui/` UI Directory + +```text +main/ui/ +├── ui.cpp / ui.h +├── launch.cpp / launch.h +├── ui_launch_page.cpp / ui_launch_page.h +├── ui_app_page.hpp +├── generated/page_app.h +├── generate_page_app_includes.py +├── ui_loading.* +├── ui_global_hint.* +├── LauncherUiRuntime.* +├── animation/ +└── page_app/ +``` + +| File/Directory | Role | +| --- | --- | +| `ui.c` / `ui.cpp` / `ui.h` | UI 初期化、グローバルオブジェクト、C/C++ ブリッジ | +| `launch.cpp` | アプリケーションマネージャ。アプリケーションリスト、起動、ステータスバー更新、ディレクトリ監視を実装する | +| `ui_launch_page.cpp` | ホーム UI 作成、カルーセルスロット、キー処理、起動アニメーション | +| `ui_loading.cpp` | Loading オーバーレイ | +| `ui_global_hint.cpp` | グローバルヒント | +| `LauncherUiRuntime.cpp` | LVGL OS/thread 関連ヘルパー | +| `animation/` | ホームカルーセルアニメーションの実装 | +| `components/` | ページ基底クラス、コンポーネント、カスタムページ | + +### 2.5 `components/page_app/` Built-In Page Directory + +```text +main/ui/page_app/ +├── ui_app_camera.hpp +├── ui_app_compass.hpp +├── ui_app_console.hpp +├── ui_app_file.hpp +├── ui_app_game.hpp +├── ui_app_lora.hpp +├── ui_app_mesh.hpp +├── ui_app_game.hpp +├── ui_app_rec.hpp +├── ui_app_setup.hpp +├── ui_app_ssh.hpp +├── ui_app_tank_battle.hpp +└── ui_app_ip_panel.hpp +``` + +これらのページは通常 header-only で実装され、`generate_page_app_includes.py` によって自動的に include されるようにします。 + +## 3. Module Dependencies + +簡略化した依存関係グラフ: + +```text +main.cpp + ├── ui/ui.h + ├── cp0_lvgl_app.h + ├── cp0_lvgl_file.hpp + └── hal_lvgl_bsp.h + +ui_init() + ├── UILaunchPage + ├── Launch + ├── ui_loading + └── page_app/* + +Launch + ├── UILaunchPage::panel()/label() + ├── page_v + ├── cp0_file_path() + ├── cp0_process_* + ├── cp0_dir_watch_* + ├── cp0_wifi_* + └── cp0_battery_* +``` + +## 4. Code Style Characteristics + +APPLaunch には現在、いくつかの明確なコードスタイル上の特徴があります。 + +- C と C++ が混在しています。LVGL 生成コードや互換コードは C であることが多く、ほとんどの業務ページは C++ です。 +- LVGL コールバックは C スタイルの static 関数のままですが、ページディスパッチでは `lv_event_get_user_data()` を使って所有元の C++ ページインスタンスを復元します。 +- ページクラスは通常、追加の UI フレームワークを使わず、LVGL オブジェクトを直接構築します。 +- ハードウェア機能には、できるだけ `cp0_lvgl` がラップした統一インターフェース経由でアクセスします。 +- リソースアクセスでは、デバイス環境と SDL 環境のパス差を避けるため、できるだけ `cp0_file_path()` を使用します。 diff --git a/docs/launcher-project-guide-ja/02-runtime-framework-and-boot-flow.md b/docs/launcher-project-guide-ja/02-runtime-framework-and-boot-flow.md new file mode 100644 index 00000000..12174469 --- /dev/null +++ b/docs/launcher-project-guide-ja/02-runtime-framework-and-boot-flow.md @@ -0,0 +1,351 @@ +# 02 - Runtime Framework and Boot Flow + +この章では、APPLaunch プロセスのエントリポイントからホーム画面の最初のフレームが表示されるまでの全体経路を説明します。主な参照先は `projects/APPLaunch/main/src/main.cpp`、`projects/APPLaunch/main/ui/ui.cpp`、`projects/APPLaunch/main/ui/launcher_ui_runtime.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.cpp` です。 + +## 1. Runtime Framework Overview + +APPLaunch は単一プロセスの LVGL アプリケーションです。メインスレッドがプラットフォーム初期化、UI オブジェクト作成、最初のフレーム更新を行い、その後 `lv_timer_handler()` によって駆動されるループに入ります。 + +```text +APPLaunch process +├── main.cpp +│ ├── lv_init() +│ ├── cp0_lvgl_init() +│ ├── lv_event_register_id() +│ ├── ui_init() +│ └── while (1) +│ ├── APPLaunch_lock() +│ ├── lv_timer_handler() +│ └── usleep(5000) +└── ui_init() + └── LauncherUiRuntime() + ├── create_display() + ├── Create Launch / UILaunchPage bound objects + └── build_launcher_home() +``` + +主要な特徴: + +- LVGL 初期化とプラットフォーム適応初期化は `main()` で一度だけ実行されます。 +- ホーム UI は `LauncherUiRuntime` の制御下で作成され、実際のオブジェクトは `UILaunchPage::create_screen()` で作られます。 +- `Launch` / `Launch` は、アプリケーションリスト、起動方式、ステータスバー更新、動的アプリケーションディレクトリの監視を担当します。 +- `ui_init()` の直後に `lv_obj_invalidate()` + `lv_refr_now(NULL)` で最初のホームフレームを強制更新し、起動後の自然な更新を待つ間に黒画面が出ることを避けます。 + +## 2. Entry Files and Key Source Paths + +| Path | Role | +| --- | --- | +| `projects/APPLaunch/main/src/main.cpp` | プロセスエントリポイント、LVGL メインループ、外部アプリケーション実行時ロック検出 | +| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`、グローバルな `LauncherUiRuntime home` を作成する | +| `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp` | LVGL テーマを設定し、ホーム画面を作成し、Launch 連携オブジェクトを作成する | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | ホーム画面、起動 GIF、ホーム loading、入力グループ | +| `projects/APPLaunch/main/ui/launch.cpp` | アプリケーションマネージャ。外部/端末/組み込みページを起動し、ステータスバータイマーを所有する | +| `ext_components/cp0_lvgl` | `cp0_lvgl_init()`、ファイルパス、入力、プロセス、システム機能のラッパー | + +## 3. `main()` Boot Flow + +`main()` のフレームワークコードは次のとおりです。 + +```cpp +int main(void) +{ + static const std::string default_lock_file = cp0_file_path("lock_file"); + lock_file = default_lock_file.c_str(); + + lv_init(); + cp0_lvgl_init(); + + if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + + ui_init(); + + lv_obj_invalidate(lv_scr_act()); + lv_refr_now(NULL); + + while (1) { + APPLaunch_lock(); + lv_timer_handler(); + usleep(5000); + } +} +``` + +### 3.1 Initialization Phase + +1. `cp0_file_path("lock_file")` が実行時ロックファイルのパスを解決します。 +2. `lv_init()` が LVGL コアオブジェクト、メモリ、タイマー、display/indev 抽象を初期化します。 +3. `cp0_lvgl_init()` がプラットフォーム層を初期化します。display、入力、framebuffer/SDL、システムシグナル、その他の機能が対象です。 +4. `lv_event_register_id()` がカスタムキーボードイベント `LV_EVENT_KEYBOARD` を登録します。 +5. `ui_init()` が APPLaunch 独自の UI 構築フローに入ります。 + +### 3.2 First-Frame Refresh + +`ui_init()` が戻った後、コードはすぐに次を実行します。 + +```cpp +lv_obj_invalidate(lv_scr_act()); +lv_refr_now(NULL); +``` + +このステップの目的は通常の更新ではなく、現在のアクティブ画面の内容を framebuffer/SDL window へ強制的に flush することです。ホームオブジェクトが作成された直後に、後続の `lv_timer_handler()` だけに頼ると一瞬黒画面が見える場合があります。最初のフレームを強制することで、起動時の挙動をより決定的にします。 + +### 3.3 Main Loop + +メインループは 5 ms 間隔で動作します。 + +```text +Each loop iteration + -> APPLaunch_lock() + -> lv_timer_handler() + -> sleep 5ms +``` + +- `APPLaunch_lock()` は外部アプリケーションがフォアグラウンドを占有しているか確認します。 +- `lv_timer_handler()` は LVGL タイマー、アニメーション、入力イベント、再描画を駆動します。 +- `usleep(5000)` は CPU 使用率と更新間隔を制御します。 + +## 4. From `ui_init()` to Home Object Creation + +`ui_init()` は `projects/APPLaunch/main/ui/ui.cpp` にあります。 + +```cpp +std::unique_ptr home; + +void ui_init(void) +{ + home = std::make_unique(); +} +``` + +`LauncherUiRuntime` コンストラクタは次の処理へ進みます。 + +```cpp +LauncherUiRuntime::LauncherUiRuntime() +{ + create_display(); + + launch_ = std::make_shared(); + launch_page_ = std::make_shared(launch_); + launch_->set_launch_page(launch_page_); + + build_launcher_home(); +} +``` + +ここでは順序に注意してください。 + +1. `create_display()` が最初にフォントマネージャを作成し、LVGL テーマを設定します。 +2. `Launch` と `UILaunchPage` を構築し、`Launch::set_launch_page()` によって双方向の協調関係を確立します。 +3. `build_launcher_home()` がホーム画面を作成し、`Launch::bind_ui()` を呼んでアプリケーションリストを構築し、入力グループを初期化し、ホーム画面または起動 GIF を表示します。 + +## 5. Display / Theme Initialization + +`LauncherUiRuntime::create_display()` の中核コード: + +```cpp +void LauncherUiRuntime::create_display() +{ + fonts_ = std::make_shared(); + + dispp_ = lv_disp_get_default(); + theme_ = lv_theme_default_init( + dispp_, + lv_palette_main(LV_PALETTE_BLUE), + lv_palette_main(LV_PALETTE_RED), + false, + LV_FONT_DEFAULT); + lv_disp_set_theme(dispp_, theme_); +} +``` + +注意点: + +- `LauncherFonts` は、ホーム画面とページで共有される FreeType フォントキャッシュです。入口関数は `launcher_fonts()` です。 +- `lv_disp_get_default()` は、`cp0_lvgl_init()` がすでに表示デバイスを登録していることに依存します。 +- このテーマはベーステーマにすぎません。多くのホームコントロールは、サイズ、色、背景画像、フォントを `ui_launch_page.cpp` 内で手動設定します。 + +## 6. Home Creation and Display Flow + +`LauncherUiRuntime::build_launcher_home()` はホーム画面を表示するための主要なエントリポイントです。 + +```cpp +void LauncherUiRuntime::build_launcher_home() +{ + LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); + + launch_page_->create_screen(); + launch_->bind_ui(); + launch_page_->init_input_group(); + +#ifndef APPLAUNCH_STARTUP_ANIMATION + launch_page_->load_home_screen(); +#else +#ifdef HAL_PLATFORM_SDL + launch_page_->load_home_screen(); +#else + const char *gif_path = cp0_file_path_c("logo_output.gif"); + FILE *gif_file = fopen(gif_path, "r"); + if (gif_file) { + fclose(gif_file); + launch_page_->start_startup_gif(); + } else { + launch_page_->load_home_screen(); + } +#endif +#endif +} +``` + +### 6.1 Home Screen Creation + +`UILaunchPage` は `home_base` を継承しているため、ルート画面、上部ステータスバー、コンテンツコンテナ、入力グループは共有ページフレームワークによって準備されます。`UILaunchPage::create_screen()` はホームコンテンツコンテナを埋めるだけで、一度だけ実行されます。 + +```cpp +void UILaunchPage::create_screen() +{ + if (carousel_elements[kCardCenter]) + return; + + create_app_container(content_container()); +} +``` + +ここでホームカルーセル領域を作成します。5 枚のカード、5 つのタイトル、5 つのページドット、左右の矢印です。左上ロゴ、WiFi インジケータ、時刻ラベル、バッテリーバーは `home_base::creat_Top_UI()` が作成します。 + +### 6.2 Input Group Binding + +ホーム入力グループは `AppPageRoot::input_group()` から得られます。`UILaunchPage::init_input_group()` はこれを互換ブリッジに保存し、アクティブなキーボード入力デバイスへバインドします。 + +```cpp +void UILaunchPage::init_input_group() +{ + ::home_input_group = input_group(); + bind_home_input_group(); +} +``` + +これにより、キーボードイベントは `screen()` に配送され、LVGL コールバック `on_home_key()` が `handle_home_key()` へディスパッチして左右切り替えと Enter 起動を処理します。 + +### 6.3 Startup GIF and Home Display + +`APPLAUNCH_STARTUP_ANIMATION` が有効で、プラットフォームが SDL ではない場合: + +```text +Check cp0_file_path_c("logo_output.gif") + -> file exists: UILaunchPage::start_startup_gif() + -> file does not exist: UILaunchPage::load_home_screen() +``` + +`start_startup_gif()` は独立した GIF 画面を作成し、`this` とともにコールバックをバインドします。 + +```cpp +startup_gif_ = lv_gif_create(NULL); +lv_gif_set_src(startup_gif_, startup_gif_path_.data()); +lv_obj_center(startup_gif_); +lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); +lv_disp_load_scr(startup_gif_); +``` + +GIF 再生が終わると `LV_EVENT_READY` を受け取ります。`on_startup_gif_event()` は所有元の `UILaunchPage` インスタンスに戻り、`handle_startup_gif_event()` が GIF を一時停止してホーム画面を一度だけ読み込みます。 + +```cpp +if (event_code == LV_EVENT_READY && !startup_gif_done_) { + startup_gif_done_ = true; + if (startup_gif_) lv_gif_pause(startup_gif_); + load_home_screen(); +} +``` + +`load_home_screen()` の責務: + +```cpp +show_home_screen(); +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +## 7. Boot Sequence Text + +```text +main() + -> cp0_file_path("lock_file") + -> lv_init() + -> cp0_lvgl_init() + -> register LV_EVENT_KEYBOARD + -> ui_init() + -> new LauncherUiRuntime + -> create_display() + -> new LauncherFonts + -> lv_disp_get_default() + -> lv_theme_default_init() + -> new Launch + -> new UILaunchPage(Launch) + -> Launch::set_launch_page() + -> build_launcher_home() + -> register LV_EVENT_GET_COMP_CHILD + -> launch_page_->create_screen() + -> home_base::creat_Top_UI() + -> create_app_container(content_container()) + -> launch_->bind_ui() + -> new Launch + -> Register fixed/dynamic applications and write them into home slots + -> Create status bar and application directory watch timers + -> launch_page_->init_input_group() + -> load_home_screen() or start_startup_gif() + -> lv_obj_invalidate(lv_scr_act()) + -> lv_refr_now(NULL) + -> while forever + -> APPLaunch_lock() + -> lv_timer_handler() + -> usleep(5000) +``` + +## 8. External Application Runtime Lock `APPLaunch_lock()` + +`APPLaunch_lock()` は、APPLaunch と外部の独立プロセスの間で、フォアグラウンド描画の関係を調整します。 + +```cpp +void APPLaunch_lock() +{ + int holder_pid = 0; + cp0_process_check_lock(lock_file, &holder_pid); + + if (holder_pid == 0) { + LVGL_RUN_FLAGE = 1; + lv_obj_invalidate(lv_scr_act()); + } else { + if (LVGL_HOME_KEY_FLAG) { + // Kill the external application after HOME is held for 5 seconds. + cp0_process_kill(holder_pid, 3000); + } + LVGL_RUN_FLAGE = 0; + } +} +``` + +実際のコードには複数の状態変数があります。 + +- `lvgl_lock`: 各ループで LVGL 更新復帰を繰り返すことを避けます。ロック解除後に一度だけ `invalidate` します。 +- `home_back_status` / `start_time`: HOME キーが押され続けている時間を追跡します。 +- `holder_pid`: 現在ロックファイルを保持している外部プロセスの PID。 + +ロジック: + +```text +No external application holds the lock + -> APPLaunch restores LVGL_RUN_FLAGE=1 + -> If just recovered from the locked state, redraw the current screen + +An external application holds the lock + -> APPLaunch sets LVGL_RUN_FLAGE=0 and pauses its own rendering + -> If the HOME key has been held for >= 5 seconds, try to kill the external application +``` + +## 9. Notes + +- `ui_init()` は内部ですでにホーム画面を作成し、読み込む場合があります。`main()` の後続の `lv_refr_now(NULL)` は最初のフレームの安全策であり、安易に削除しないでください。 +- `cp0_lvgl_init()` は `ui_init()` より前に実行する必要があります。そうしないと `lv_disp_get_default()`、入力デバイス、パス、システムインターフェースが準備できていない可能性があります。 +- SDL プラットフォームでは、起動 GIF はデフォルトでスキップされます。`logo_output.gif` を確認して再生するのはデバイス側だけです。 +- ホーム入力は `UILaunchPage::bind_home_input_group()` を通して再バインドする必要があります。組み込みページや端末ページからホーム画面へ戻る場合も、このグループを復元する必要があります。 +- 外部の独立アプリケーションが実行中は `LVGL_RUN_FLAGE=0` になります。この期間に APPLaunch が UI 更新を続けるとは想定しないでください。 +- `APPLaunch_lock()` は `cp0_process_exec_blocking()` とロックファイルの協調に依存します。外部アプリケーションが異常終了してロックが解放されない場合、ホーム画面が更新されないように見えることがあります。その場合はロックファイルと holder PID を調査してください。 diff --git a/docs/launcher-project-guide-ja/03-ui-framework-and-home-carousel.md b/docs/launcher-project-guide-ja/03-ui-framework-and-home-carousel.md new file mode 100644 index 00000000..58112bb0 --- /dev/null +++ b/docs/launcher-project-guide-ja/03-ui-framework-and-home-carousel.md @@ -0,0 +1,516 @@ +# 03 - UI Framework and Home Carousel + +この章では、APPLaunch のホーム UI がどのように構成され、データがカルーセルカードを通してどのように流れ、キーイベントがどのように処理されるかを説明します。主な参照先は `projects/APPLaunch/main/ui/ui_launch_page.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.h`、`projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp`、`projects/APPLaunch/main/ui/launch.cpp` です。 + +## 1. UI Framework Overview + +APPLaunch は従来型のデスクトップフレームワークを使いません。代わりに、共有 LVGL ページ基底とカルーセルコンテンツ領域からホームページを構築します。 + +```text +UILaunchPage : home_base +├── home_base/AppPageRoot root screen +│ ├── home_base::creat_Top_UI() +│ │ ├── ZERO / logo +│ │ ├── WiFi signal bars +│ │ ├── Time panel +│ │ └── Battery panel +│ └── content_container() +└── Home carousel inside content_container() + ├── 5 carousel card panels + ├── 5 title labels + ├── Left/right arrow buttons + └── 5 page dots +``` + +ホームは、ルート画面、ステータスバー、入力グループに共通の `home_base` / `AppPageRoot` ページフレームワークを使用します。`ui_launch_page.cpp` は継承したコンテンツコンテナにカルーセルを配置し、LVGL コールバックを接続します。 + +## 2. Key Source Paths + +| Path | Description | +| --- | --- | +| `projects/APPLaunch/main/ui/ui_launch_page.h` | ホームクラス定義、カルーセル要素 enum、`carousel_elements` 配列 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | ホーム画面作成、カルーセル切り替え、キーボードイベント、起動 GIF、フォントキャッシュ | +| `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | カルーセルの左右切り替えアニメーション | +| `projects/APPLaunch/main/ui/launch.cpp` | 切り替え後の新しいカード内容の設定、現在のアプリケーション起動、ステータスバー更新 | +| `projects/APPLaunch/main/ui/ui.h` | `LABEL_Y_CENTER` や `BORDER_COLOR_CENTER` などのホームレイアウト定数 | + +## 3. Responsibilities of `UILaunchPage` + +`UILaunchPage` はホーム UI の facade クラスです。 + +```cpp +class UILaunchPage : public home_base +{ +public: + explicit UILaunchPage(std::shared_ptr launch); + ~UILaunchPage(); + + void show_home_screen(); + void load_home_screen(); + void start_startup_gif(); + void create_screen(); + void init_input_group(); + + static void bind_home_input_group(); + static lv_group_t *home_input_group(); + static lv_obj_t *panel(size_t slot); + static lv_obj_t *label(size_t slot); + + void update_left_slot(lv_obj_t *panel, lv_obj_t *label); + void update_right_slot(lv_obj_t *panel, lv_obj_t *label); + void launch_selected_app(); + +private: + enum class PendingSwitch { None, Left, Right }; + + void switch_left(); + void switch_right(); + void finish_switch_animation(); + void run_pending_switch(); + void handle_home_key(lv_event_t *event); + void handle_startup_gif_event(lv_event_t *event); + + static void on_left_arrow_clicked(lv_event_t *event); + static void on_right_arrow_clicked(lv_event_t *event); + static void on_app_clicked(lv_event_t *event); + static void on_home_key(lv_event_t *event); + static void on_startup_gif_event(lv_event_t *event); + + bool is_animating_ = false; + PendingSwitch pending_switch_ = PendingSwitch::None; + int switch_current_pos_ = kPageDot2; +}; +``` + +責務は大きく 2 種類です。 + +- static 互換責務: 共有 `carousel_elements` 配列を保持し、ホーム入力グループのブリッジを維持し、`launch.cpp` が使用する `panel()` / `label()` アクセサを提供します。 +- インスタンス責務: `Launch` ポインタを保持し、ページ単位の UI 状態を所有し、LVGL イベントを処理し、カルーセル更新とアプリ起動を `Launch` へ委譲します。 + +LVGL は引き続き C スタイルの static コールバックを必要としますが、現在のコードは通常のイベントディスパッチでグローバル状態に依存しません。各コールバックは LVGL user data を通して所有元ページインスタンスを受け取ります。 + +```cpp +static UILaunchPage *page_from_event(lv_event_t *event) +{ + return event ? static_cast(lv_event_get_user_data(event)) : nullptr; +} + +void UILaunchPage::on_left_arrow_clicked(lv_event_t *event) +{ + if (UILaunchPage *self = page_from_event(event)) + self->switch_right(); +} +``` + +コールバックは `this` とともに登録されます。 + +```cpp +lv_obj_add_event_cb(left_arrow_button_, on_left_arrow_clicked, LV_EVENT_CLICKED, this); +lv_obj_add_event_cb(right_arrow_button_, on_right_arrow_clicked, LV_EVENT_CLICKED, this); +lv_obj_add_event_cb(screen(), on_home_key, (lv_event_code_t)LV_EVENT_KEYBOARD, this); +lv_obj_add_event_cb(startup_gif_, on_startup_gif_event, LV_EVENT_ALL, this); +``` + +`active_launch_page` は、`UILaunchPage::panel()`、`UILaunchPage::label()`、`UILaunchPage::home_input_group()` のような static 外部アクセサ向けの互換ブリッジとしてだけ保持されます。 + +## 4. Carousel Element Array + +ホームカルーセルの中核オブジェクトはすべて固定配列に格納されます。 + +```cpp +std::array + UILaunchPage::carousel_elements = {}; +``` + +enum は `ui_launch_page.h` で定義されています。 + +```cpp +enum LauncherCarouselElement : size_t { + kCardFarLeft = 0, + kCardLeft, + kCardCenter, + kCardRight, + kCardFarRight, + kTitleFarLeft, + kTitleLeft, + kTitleCenter, + kTitleRight, + kTitleFarRight, + kPageDot0, + kPageDot1, + kPageDot2, + kPageDot3, + kPageDot4, + kLauncherCarouselElementCount, +}; +``` + +配列は 3 つの区間に分かれます。 + +| Index Range | Object | Description | +| --- | --- | --- | +| `0..4` | Card panel | far-left、left、center、right、far-right | +| `5..9` | Title label | カードスロットに対応 | +| `10..14` | Page dot | 下部の 5 つの状態ドット | + +ヘルパーアクセサ: + +```cpp +lv_obj_t *UILaunchPage::panel(size_t slot) +{ + return carousel_elements[kCardFarLeft + slot]; +} + +lv_obj_t *UILaunchPage::label(size_t slot) +{ + return carousel_elements[kTitleFarLeft + slot]; +} +``` + +したがって、`panel(2)` は中央カード、`label(2)` は中央タイトルです。 + +## 5. Standard Slot Layout + +`ui_launch_page.cpp` は `CarouselSlot` を使って静的なカルーセルレイアウトを表現します。 + +```cpp +struct CarouselSlot { + lv_coord_t x; + lv_coord_t y; + lv_coord_t width; + lv_coord_t height; + bool hidden; +}; + +static const CarouselSlot CAROUSEL_SLOTS[] = { + {-177, 4, 61, 61, true}, + {-99, -6, 80, 80, false}, + {0, -16, 100, 100, false}, + {99, -6, 80, 80, false}, + {177, 4, 61, 61, true}, + {-177, LABEL_Y_SIDE, 0, 0, true}, + {-99, LABEL_Y_SIDE, 0, 0, false}, + {0, LABEL_Y_CENTER, 0, 0, false}, + {99, LABEL_Y_SIDE, 0, 0, false}, + {177, LABEL_Y_SIDE, 0, 0, true}, +}; +``` + +スロットの意味: + +```text +Cards: far-left(hidden) left center right far-right(hidden) +Titles: far-left(hidden) left center right far-right(hidden) +``` + +非表示の両端スロットはアニメーション用バッファです。切り替え前に、これから入ってくるカードを far side に置き、アニメーション終了後に配列順を回転します。 + +## 6. Home Creation Flow + +`home_base` はルート画面、上部ステータスバー、コンテンツコンテナを構築します。`UILaunchPage::create_screen()` はホームコンテンツ領域を埋めるだけで、カルーセルがすでに存在する場合は再構築を避けます。 + +```cpp +void UILaunchPage::create_screen() +{ + if (carousel_elements[kCardCenter]) + return; + + create_app_container(content_container()); +} +``` + +### 6.1 Top Status Bar + +上部ステータスバーは `home_base::creat_Top_UI()` 由来で、次を含みます。 + +- 左上の `ZERO` テキストまたは `launcher_brand_logo.png`。 +- `ui_wifiPanel` と `ui_wifiBar1..4`。デフォルトでは非表示で、ステータス更新時に信号強度に応じて表示されます。 +- `ui_Panel1`、時刻背景画像 `status_time_background.png`、`ui_timeLabel`。 +- `ui_batteryPanel`、バッテリー背景画像 `status_battery_background.png`、`ui_Bar1`、`ui_powerLabel`。 + +ステータスバーのデータ更新は `UILaunchPage` ではなく `Launch::update_home_status_bar()` で行われます。 + +```cpp +cp0_wifi_status_t wifi = cp0_wifi_get_status(); +cp0_time_str(time_buf, sizeof(time_buf)); +cp0_battery_info_t bat = cp0_battery_read(); +``` + +`Launch` は構築時に 5 秒タイマーを作成します。 + +```cpp +status_timer = lv_timer_create(home_status_timer_cb, 5000, this); +``` + +### 6.2 Carousel Container + +`create_app_container()` は継承した `content_container()` をカルーセルコンテナとして使用します。 + +```cpp +lv_obj_t *app_container = parent; +if (!app_container) + return; + +lv_obj_set_size(app_container, 320, 150); +lv_obj_clear_flag(app_container, + (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); +``` + +その後、次の順で作成します。 + +- 5 つのページドット: `kPageDot0..kPageDot4`。中央ページドットはデフォルトで 10x10、黄色です。 +- 5 つのタイトル: 中央はデフォルトで `CLI`、左右は `STORE` / `GAME`、両端のタイトルは非表示です。 +- 5 枚のカード: 中央は 100x100、左右は 80x80、両端カードは 61x61 で非表示です。 +- 左右ボタン: 背景画像 `carousel_left_arrow.png` / `carousel_right_arrow.png`。 + +デフォルトタイトルは UI プレースホルダにすぎません。実際の内容は、アプリケーションリスト初期化後に `Launch` が書き込みます。 + +## 7. Carousel Switch Flow + +カルーセル切り替えは、UI アニメーションとアプリケーションデータ更新の 2 つに分かれます。 + +### 7.1 Switching Right with `switch_right()` + +`UILaunchPage::switch_right()` はカード群が右へ動き、現在選択がリスト内の前のアプリケーションになることを意味します。 + +```cpp +void UILaunchPage::switch_right() +{ + if (is_animating_) { + pending_switch_ = PendingSwitch::Right; + return; + } + + is_animating_ = true; + lv_obj_clear_flag(carousel_elements[0], LV_OBJ_FLAG_HIDDEN); + launcher_home_animation::animate_right( + carousel_elements.data(), + [this]() { finish_switch_animation(); }); + + snap_panel_to_slot(carousel_elements[4], 0); + snap_label_to_slot(carousel_elements[9], 5); + + update_right_slot(carousel_elements[4], carousel_elements[9]); + rotate_carousel_right(0, 4); + rotate_carousel_right(5, 9); +} +``` + +主要ステップ: + +1. すでにアニメーション中なら `PendingSwitch::Right` を保存します。保持されるのは最新の保留方向だけです。 +2. アニメーション中に viewport へ入る側として、非表示だった far-left カードを表示します。 +3. `launcher_home_animation::animate_right()` を呼び、`this` をキャプチャした lambda を渡します。 +4. far-right オブジェクトを far-left スロットへ事前に snap し、これから入ってくる新しいアプリ内容で埋めます。 +5. `carousel_elements[0..4]` と `[5..9]` を回転し、配列順を新しい見た目の順序に合わせます。 +6. ページドットのハイライトを更新します。 + +### 7.2 Switching Left with `switch_left()` + +`UILaunchPage::switch_left()` はカード群が左へ動き、現在選択がリスト内の次のアプリケーションになることを意味します。 + +```cpp +void UILaunchPage::switch_left() +{ + if (is_animating_) { + pending_switch_ = PendingSwitch::Left; + return; + } + + is_animating_ = true; + lv_obj_clear_flag(carousel_elements[4], LV_OBJ_FLAG_HIDDEN); + launcher_home_animation::animate_left( + carousel_elements.data(), + [this]() { finish_switch_animation(); }); + + snap_panel_to_slot(carousel_elements[0], 4); + snap_label_to_slot(carousel_elements[5], 9); + + update_left_slot(carousel_elements[0], carousel_elements[5]); + rotate_carousel_left(0, 4); + rotate_carousel_left(5, 9); +} +``` + +これは `switch_right()` と対称です。far-right 側が viewport に入り、far-left オブジェクトは far-right スロットへ移動して新しい内容で埋められます。 + +## 8. Snapping Back After Animation + +アニメーション完了経路は `UILaunchPage::finish_switch_animation()` です。 + +```cpp +void UILaunchPage::finish_switch_animation() +{ + for (int i = 0; i < 5; i++) + snap_panel_to_slot(carousel_elements[i], i); + + for (int i = 5; i < 10; i++) + snap_label_to_slot(carousel_elements[i], i); + + is_animating_ = false; + run_pending_switch(); +} +``` + +`run_pending_switch()` は enum 状態を消費し、対応するインスタンスメソッドを呼びます。 + +```cpp +void UILaunchPage::run_pending_switch() +{ + PendingSwitch pending = pending_switch_; + pending_switch_ = PendingSwitch::None; + + if (pending == PendingSwitch::Left) + switch_left(); + else if (pending == PendingSwitch::Right) + switch_right(); +} +``` + +これにより 2 つの問題を解決します。 + +- アニメーション補間で小さな誤差が出る可能性があるため、終了後にオブジェクトを標準スロットへ強制 snap します。 +- ユーザーがアニメーション中に方向キーを連打しても、保留される switch enum は 1 つだけで、アニメーション完了後に実行されます。 + +## 9. How Application Data Is Written into the Carousel + +`Launch` は `current_app` と `app_list` を管理します。切り替え中、`UILaunchPage` は再利用する panel/label を渡すだけで、どのアプリケーションを表示すべきかは `Launch` が計算します。 + +左へ切り替えた後、新しい右端を埋める処理: + +```cpp +void update_left_slot(lv_obj_t *panel, lv_obj_t *label) +{ + current_app = current_app == app_list.size() - 1 ? 0 : current_app + 1; + int next_app = current_app; + next_app = next_app == app_list.size() - 1 ? 0 : next_app + 1; + next_app = next_app == app_list.size() - 1 ? 0 : next_app + 1; + auto it = std::next(app_list.begin(), next_app); + lv_label_set_text(label, it->Name.c_str()); + panel_set_icon(panel, it->Icon.c_str()); +} +``` + +右へ切り替えた後、新しい左端を埋める処理: + +```cpp +void update_right_slot(lv_obj_t *panel, lv_obj_t *label) +{ + current_app = current_app == 0 ? app_list.size() - 1 : current_app - 1; + int next_app = current_app; + next_app = next_app == 0 ? app_list.size() - 1 : next_app - 1; + next_app = next_app == 0 ? app_list.size() - 1 : next_app - 1; + auto it = std::next(app_list.begin(), next_app); + lv_label_set_text(label, it->Name.c_str()); + panel_set_icon(panel, it->Icon.c_str()); +} +``` + +図: + +```text +Visual slots: [far-left] [left] [center] [right] [far-right] +Application index: current-2 current-1 current current+1 current+2 + +Press RIGHT: + current -> current-1 + New far-left needs to display current-2 + +Press LEFT: + current -> current+1 + New far-right needs to display current+2 +``` + +## 10. Input Events and Sound Effects + +ホームのキーボードイベントは、`create_app_container()` の末尾で LVGL コールバックブリッジを通してバインドされます。 + +```cpp +lv_obj_add_event_cb(screen(), on_home_key, + (lv_event_code_t)LV_EVENT_KEYBOARD, this); +``` + +`on_home_key()` は所有元の `UILaunchPage` インスタンス上で `handle_home_key()` を呼びます。ロジックは次のとおりです。 + +```text +Press LEFT/Z + -> audio_play_switch() + -> switch_right() + +Press RIGHT/C + -> audio_play_switch() + -> switch_left() + +Release ENTER + -> audio_play_enter() + -> launch_selected_app() + +Release F12 + -> Toggle green test background lvping_lock +``` + +コードはまず `fzxc_to_arrow()` によって `F/X/Z/C` を矢印キーへマップします。 + +```cpp +KEY_F -> KEY_UP +KEY_X -> KEY_DOWN +KEY_Z -> KEY_LEFT +KEY_C -> KEY_RIGHT +``` + +効果音のエントリポイント: + +```cpp +cp0_signal_system_play_asset("switch.wav"); +cp0_signal_system_play_asset("enter.wav"); +``` + +起動音は `load_home_screen()` で再生されます。 + +```cpp +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +## 11. Home Sequence Text + +```text +UILaunchPage constructed as home_base + -> home_base::creat_Top_UI() + -> Create logo / WiFi / time / battery objects +UILaunchPage::create_screen() + -> create_app_container(content_container()) + -> Create page dots + -> Create labels + -> Create cards + -> Create arrows + -> Bind click and keyboard callbacks + +User presses RIGHT + -> on_home_key() -> handle_home_key() + -> audio_play_switch() + -> switch_left() + -> is_animating=true + -> animate_left() + -> update_left_slot() + -> rotate cards / labels + -> Update page dot + -> finish_switch_animation() + -> Snap objects to standard slots + -> is_animating=false + -> If pending_switch_ exists, continue executing it + +User presses ENTER + -> on_home_key() -> handle_home_key() + -> audio_play_enter() + -> UILaunchPage::launch_selected_app() + -> Launch::launch_app() +``` + +## 12. Notes + +- `carousel_elements` は LVGL オブジェクトポインタを保存します。カルーセル切り替えでは、オブジェクトを破棄して再作成するのではなく、ポインタ配列を回転します。 +- `switch_left()` / `switch_right()` という名前はアニメーション方向を表しており、必ずしもユーザーのキー方向と同一ではありません。現在は `KEY_LEFT` が `switch_right()` を呼び、`KEY_RIGHT` が `switch_left()` を呼びます。 +- アニメーション中は `pending_switch_` の enum 値を 1 つだけ記録するため、連打しても無制限のキューは作られません。 +- ホームカードのクリックイベントは `on_app_clicked()` にバインドされ、`launch_selected_app()` へブリッジします。ただし通常操作では中央選択 + Enter 起動が主です。マウス/タッチ操作を有効にする場合、中央以外のカードクリックが期待どおりか確認してください。 +- ステータスバーオブジェクトは `UILaunchPage` が作成しますが、更新タイマーは `Launch` 構築時に作られます。`Launch::bind_ui()` を実行せずにホーム画面を作成すると、アプリケーションリストとステータスバー更新は開始されません。 +- カルーセルスロットを追加または調整する場合、`CAROUSEL_SLOTS`、`create_app_container()` 内の初期位置、アニメーションファイル内のスロット定義を同時に更新し、アニメーション完了後のジャンプを避けてください。 diff --git a/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md b/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md new file mode 100644 index 00000000..7a7db6f4 --- /dev/null +++ b/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md @@ -0,0 +1,445 @@ +# 04 - Application Model and Launch Mechanism + +この章では、APPLaunch が組み込みページ、端末コマンド、外部スタンドアロンプログラムを 1 つのアプリケーションリストへ統合する方法と、ユーザーが Enter を押した後にアプリケーションがどのように起動されるかを説明します。主な参照先は `projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/launch.h`、`projects/APPLaunch/main/ui/ui_launch_page.cpp`、`projects/APPLaunch/main/ui/page_app/*` です。 + +## 1. Application Model Overview + +APPLaunch はホーム画面上の各エントリを `app` として抽象化します。 + +```text +app +├── Name display title +├── Icon icon path +├── Exec external command; can be empty for built-in pages +└── launch(Launch*) launch action +``` + +この統一により、ホームカルーセルはアプリケーション種別を意識する必要がありません。`Name` と `Icon` を表示し、Enter が押されたら現在の `app.launch()` を呼ぶだけです。 + +```text +Home center card + -> Launch::launch_app() + -> Launch::launch_app() + -> app.launch(this) + ├── Built-in page: new PageT + lv_disp_load_scr() + ├── Terminal app: UIConsolePage + PTY exec() + └── External app: cp0_process_exec_blocking() +``` + +## 2. Key Source Paths + +| Path | Description | +| --- | --- | +| `projects/APPLaunch/main/ui/launch.h` | 公開 `Launch` インターフェースと app モデル宣言 | +| `projects/APPLaunch/main/ui/launch.cpp` | `app`、`Launch`、アプリケーションリスト、起動ロジック、`.desktop` スキャン | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Enter / クリックイベントを `Launch::launch_app()` へ転送する | +| `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | 端末ページ `UIConsolePage` | +| `projects/APPLaunch/main/ui/page_app/*.hpp` | settings、game、file、camera、LoRa などの組み込みページ | +| `projects/APPLaunch/APPLaunch/applications/` | 実行時 `.desktop` アプリケーション記述ディレクトリ | +| `ext_components/cp0_lvgl` | プロセス起動、PTY、ディレクトリ監視、パス解決などの低レベル機能 | + +## 3. `Launch` Runtime State + +`launch.h` は `Launch` クラスを直接公開しています。現在のコードには独立した `LaunchImpl` レイヤーはなく、アプリケーションリスト、ディレクトリ watcher、現在ページの保持、カルーセルヘルパーはすべて `Launch` にあります。 + +重要な private 状態: + +| Field | Description | +| --- | --- | +| `launch_page_` | ホーム `UILaunchPage` への weak reference | +| `current_app` | 現在中央カードに対応するアプリケーション index。デフォルトは `2` なので、初期中央カードは CLI | +| `dir_watcher_` / `watch_timer_` | `applications/` ディレクトリを監視し、動的アプリを再読み込みする | +| `fixed_count` | 組み込み/固定アプリケーション数。動的再読み込みではこの位置より前の要素を保持する | +| `app_list` | 組み込みエントリと動的 `.desktop` エントリ | +| `app_Page` | 現在の組み込みページまたは端末ページの lifetime holder | + +`Launch::bind_ui()` は初期リストを構築し、動的 `.desktop` ファイルを読み込み、ディレクトリ watcher タイマーを開始し、app-registry 変更コールバックを登録します。 + +## 4. `app` Structure and Three Launch Modes + +`app` は `launch.cpp` で定義されています。 + +```cpp +struct app +{ + std::string Name; + std::string Icon; + std::string Exec; + + std::function launch; + + app(std::string name, std::string icon, std::string exec, bool terminal); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause); + app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause, bool run_as_root); + + template + app(std::string name, std::string icon, page_t tag); +}; +``` + +3 種類のアプリケーションカテゴリ: + +| Type | Construction | Launch function | Examples | +| --- | --- | --- | --- | +| Built-in page | `page_v` | ページを構築し `lv_disp_load_scr()` を呼ぶ | `GAME`, `SETTING`, `Compass` | +| Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`, `CLI` | +| External process | `exec, terminal=false` | `launch_Exec()` | AppStore, Calculator | + +## 5. Fixed Application Registration + +組み込みエントリは `launch.cpp` の `kBuiltinApps[]` として宣言されています。各エントリは、ラベル、アイコン、設定 key、Settings で設定可能か、常に有効かを持つ `AppDescriptor` を保持します。 + +代表的なエントリ: + +```cpp +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, + {{"MATH", "math_100.png", "app_Math", true, false}, + "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false, true, false, nullptr}, +}; +``` + +`Launch::rebuild_builtin_apps()` はリストをクリアし、`launcher_app_registry_is_enabled()` を呼びながら有効な組み込みアプリを追加し、`fixed_count` を更新します。Settings の変更は `launcher_app_registry_set_enabled()` で保存され、その後 `Launch::applications_reload()` を発火します。 + +最初の 5 エントリが 5 スロットのホームカルーセルを初期化します。 + +```text +slot 0 far-left : Python +slot 1 left : STORE +slot 2 center : CLI +slot 3 right : GAME +slot 4 far-right: SETTING +current_app : 2 +``` + +## 6. Built-in Page Launch Mechanism + +組み込みページは template constructor を通して構築されます。 + +```cpp +template +app::app(std::string name, std::string icon, page_t) + : Name(std::move(name)), Icon(std::move(icon)) +{ + launch = [](Launch *self) + { + ui_loading_show("Loading..."); + lv_refr_now(NULL); + + auto p = std::make_shared(); + self->app_Page = p; + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&Launch::go_back_home, self); + + ui_loading_hide(); + }; +} +``` + +組み込みページは次の規約に従う必要があります。 + +- ページクラスは引数なしで構築できること。 +- ページのルート画面を返す `screen()` を提供すること。 +- ページ自身の入力グループを返す `input_group()` を提供すること。 +- ホームに戻るための `navigate_home` コールバックを提供または継承すること。 + +起動シーケンス: + +```text +Enter + -> app.launch(Launch*) + -> ui_loading_show("Loading...") + -> lv_refr_now(NULL) + -> make_shared() + -> app_Page = p keeps the lifetime + -> lv_disp_load_scr(p->screen()) + -> Input device switches to p->input_group() + -> p->navigate_home = Launch::go_back_home + -> ui_loading_hide() +``` + +## 7. Terminal Application Launch Mechanism + +端末アプリケーションは `UIConsolePage` を使用し、外部コマンドは APPLaunch プロセス内の端末ページで実行されます。 + +```cpp +void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) +{ + ui_loading_show("Loading..."); + lv_refr_now(NULL); + + auto p = std::make_shared(); + app_Page = p; + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + p->navigate_home = std::bind(&Launch::go_back_home, this); + p->terminal_sysplause = sysplause; + + ui_loading_hide(); + p->exec(exec); +} +``` + +典型的なエントリ: + +```text +Python -> exec = "python3", terminal = true +CLI -> exec = "bash", terminal = true +``` + +組み込みページと比べ、端末アプリケーションには `p->exec(exec)` という追加ステップがあります。通常、コマンドとは PTY を通してやり取りします。ユーザーが見るのは、APPLaunch の外にある別 UI ではなく `UIConsolePage` です。 + +## 8. External Standalone Application Launch Mechanism + +外部アプリケーションは `cp0_process_exec_blocking()` を使用します。 + +```cpp +void launch_Exec(const std::string &exec, bool keep_root = false) +{ + ui_loading_show("Loading..."); + + lv_disp_t *disp = lv_disp_get_default(); + lv_indev_t *indev = lv_indev_get_next(NULL); + + LVGL_RUN_FLAGE = 0; + if (indev) + lv_indev_set_group(indev, NULL); + lv_timer_enable(false); + lv_refr_now(disp); + + int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, + keep_root ? 1 : 0); + + lv_timer_enable(true); + if (indev) + lv_indev_set_group(indev, UILaunchPage::home_input_group()); + if (launch_page_) + launch_page_->show_home_screen(); + ui_loading::hide(); + lv_obj_invalidate(lv_screen_active()); + lv_refr_now(disp); + LVGL_RUN_FLAGE = 1; +} +``` + +重要な点: + +- 起動前に Loading を表示して強制更新するため、ユーザーは即座にフィードバックを得られます。 +- APPLaunch の入力グループを解除し、外部プロセス実行中にホーム画面がキー処理を続けないようにします。 +- `lv_timer_enable(false)` は、外部プログラムがフォアグラウンドを取っている間 LVGL タイマーを一時停止します。 +- `cp0_process_exec_blocking()` は外部プログラムが終了するまでブロックします。 +- 外部プログラム終了後、タイマーを復元し、`launch_page_->show_home_screen()` を呼び、`LVGL_RUN_FLAGE` を戻します。 + +シーケンス: + +```text +Enter external app + -> ui_loading_show() + -> LVGL_RUN_FLAGE=0 + -> lv_indev_set_group(NULL) + -> lv_timer_enable(false) + -> lv_refr_now() + -> cp0_process_exec_blocking() + -> External program runs + -> APPLaunch main rendering is paused + -> Wait for the external program to exit + -> lv_timer_enable(true) + -> launch_page_->show_home_screen() + -> ui_loading_hide() + -> lv_refr_now() + -> LVGL_RUN_FLAGE=1 +``` + +`STORE` は外部アプリケーションの例です。 + +```cpp +app_list.emplace_back("STORE", + img_path("store_100.png"), + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", + false, + true, + true); +``` + +ここでは `run_as_root=true` が `launch_Exec(exec, run_as_root)` に渡され、そこで `keep_root ? 1 : 0` に変換されます。 + +## 9. Return-to-Home Mechanism + +組み込みページと端末ページは、`navigate_home` コールバックを通してホーム画面へ戻ります。 + +```cpp +void go_back_home() +{ + lv_async_call(lv_go_back_home, this); +} + +static void lv_go_back_home(void *arg) +{ + auto self = (Launch *)arg; + lv_timer_enable(true); + if (self->launch_page_) + self->launch_page_->show_home_screen(); + lv_refr_now(NULL); + if (self->app_Page) + self->app_Page.reset(); +} +``` + +`lv_async_call()` を使う理由: + +- ホーム復帰はページイベントや入力コールバックから発火する場合があります。 +- 非同期で実行することで、現在の LVGL イベントスタック内でページオブジェクトを直接破棄することを避けます。 +- `app_Page.reset()` は現在ページを解放するため、そのページオブジェクトが以後使われないことを保証する必要があります。 + +外部アプリケーションは `navigate_home` を使いません。代わりに `cp0_process_exec_blocking()` が戻った後でホーム画面を復元します。 + +## 10. `.desktop` Dynamic Application Scanning + +動的アプリケーションディレクトリ: + +```cpp +const std::string app_dir_path = cp0_file_path("applications"); +``` + +デバイスにインストールされた後、通常は次に対応します。 + +```text +/usr/share/APPLaunch/applications/ +``` + +`.desktop` ファイル例: + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/e-Mail_80.png +``` + +`applications_load()` は `.desktop` 拡張子のファイルだけを処理し、`[Desktop Entry]` セクションからフィールドを読み取ります。 + +| Field | Required | Description | +| --- | --- | --- | +| `Name` | Yes | ホーム画面に表示するタイトル | +| `Exec` | Yes | 起動コマンド | +| `Icon` | No | アイコンパス | +| `Terminal` | No | `true/True/1` は `UIConsolePage` 経由で起動することを意味する | +| `Sysplause` | No | 端末ページへ渡す pause policy。デフォルトは true | + +登録ロジック: + +```cpp +if (page_title.empty() || app_exec.empty()) + continue; + +for (auto it : app_list) { + if (it.Exec == app_exec) { + in_list = true; + break; + } +} + +if (!in_list) + app_list.emplace_back(page_title, app_icon, app_exec, + app_terminal, app_sysplause); +``` + +注意: 動的アプリケーションは `Exec` によって重複排除されます。`Exec` が固定アプリまたは別の `.desktop` アプリと一致する場合、そのエントリはスキップされます。 + +## 11. Dynamic Application Directory Watching and Reloading + +`Launch` コンストラクタの末尾: + +```cpp +fixed_count = app_list.size(); +applications_load(); +inotify_init_watch(); +watch_timer = lv_timer_create(app_dir_watch_cb, 3000, this); +``` + +監視フロー: + +```text +Every 3 seconds via LVGL timer + -> cp0_dir_watch_poll(dir_watcher) + -> If applications/ changed + -> applications_reload() + -> Delete dynamic apps after fixed_count + -> applications_load() + -> refresh_ui_panels() +``` + +`refresh_ui_panels()` は現在の `current_app` に基づき、表示/非表示の 5 スロットを書き換えます。 + +```cpp +app_at(current_app - 2) -> far-left +app_at(current_app - 1) -> left +app_at(current_app) -> center +app_at(current_app + 1) -> right +app_at(current_app + 2) -> far-right +``` + +これにより、動的アプリケーションの追加や削除後でも、ホーム画面は LVGL オブジェクトを再構築する必要がなく、テキストとアイコンだけを更新します。 + +## 12. Icon Setting and Resource Paths + +アイコンは `panel_set_icon()` によって設定されます。 + +```cpp +static void panel_set_icon(lv_obj_t *panel, const char *src) +{ + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_obj_set_align(img, LV_ALIGN_CENTER); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, icon_src); +} +``` + +特徴: + +- 各 panel は最初の child image を再利用し、画像オブジェクトを繰り返し作成しません。 +- 画像は panel サイズに stretch されます。 +- パスが空または読み取れない場合、ログを出しますが `lv_image_set_src()` は呼びます。 + +固定アプリケーションは一般に `img_path("xxx.png")` を使います。動的 `.desktop` アプリケーションの `Icon` フィールドは、現在 `app_icon` としてそのまま渡されます。`.desktop` ファイルを書くときは、LVGL がアイコンパスを読めることを確認してください。 + +## 13. Complete Flow from Key Press to Launch + +```text +User releases ENTER + -> LV_EVENT_KEYBOARD is delivered to UILaunchPage::screen() + -> UILaunchPage::on_home_key() + -> handle_home_key() + -> code == KEY_ENTER and key_state == 0 + -> audio_play_enter() + -> UILaunchPage::launch_selected_app() + -> launch_->launch_app() + -> Launch::launch_app() + -> impl_->launch_app() + -> Launch::launch_app() + -> auto it = std::next(app_list.begin(), current_app) + -> it->launch(this) + -> Enter built-in page / terminal page / external process based on app type +``` + +## 14. Notes + +- `Launch::bind_ui()` は `Launch` 作成前に呼ばれる必要があります。そうしないとホーム画面は表示されても、アプリケーションリスト更新、ステータスバータイマー、ディレクトリ監視、起動ロジックが動作しません。 +- `current_app` のデフォルトは `2` です。最初の 5 つの固定エントリ順は初期中央カードに影響します。この順序を変更するときは、初期ホーム体験を考慮してください。 +- 組み込みページの構築に時間がかかる可能性がある場合は、ユーザーが即時フィードバックを得られるよう `ui_loading_show()` + `lv_refr_now()` を維持してください。 +- 外部アプリケーションの起動は APPLaunch の LVGL タイマーと入力グループを一時停止します。外部プログラムは正常終了するか HOME ロジックに応答する必要があります。そうしないと、ユーザーは外部 UI に閉じ込められたように感じます。 +- 動的 `.desktop` アプリケーションには少なくとも `Name` と `Exec` が必要です。`Terminal=true` はコマンドラインプログラムに適しており、グラフィカルまたは排他的 framebuffer プログラムでは `Terminal=false` を使うべきです。 +- 動的アプリケーションは `Name` ではなく `Exec` で重複排除されます。同じコマンドを使う複数エントリがある場合、最初の 1 つだけが保持されます。 +- `applications/` を変更した後は、watcher が再読み込みするまで最大 3 秒待ってください。watcher が初期化されていない、またはプラットフォームが対応していない場合は、APPLaunch を再起動して変更を確認してください。 diff --git a/docs/launcher-project-guide-ja/05-built-in-page-framework.md b/docs/launcher-project-guide-ja/05-built-in-page-framework.md new file mode 100644 index 00000000..ed2428b0 --- /dev/null +++ b/docs/launcher-project-guide-ja/05-built-in-page-framework.md @@ -0,0 +1,323 @@ +# 05 - Built-in Page Framework + +この章では、組み込み APPLaunch ページを追加するためのクラス階層、ライフサイクル、ページ一覧、ページ登録方法、規約を説明します。主なソースファイルは `projects/APPLaunch/main/ui/ui_app_page.hpp`、`projects/APPLaunch/main/ui/page_app/*.hpp`、`projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.cpp` です。 + +## 1. What a Built-in Page Is + +組み込みページとは、APPLaunch プロセスにコンパイルされる LVGL ページクラスです。外部 `.desktop` アプリケーションとは異なります。 + +- 組み込みページは `lv_obj_t *root_screen_` を直接作成し、`lv_disp_load_scr(page->screen())` によって自身の画面へ切り替えます。 +- ページオブジェクトは `Launch::app_Page` に保存され、終了時には `navigate_home` コールバックによって非同期に解放されます。 +- ページはホーム画面と同じ APPLaunch プロセス、LVGL メインループ、入力スレッド、リソース解決、`cp0_lvgl_app.h` のシステムインターフェースを共有します。 +- ページは通常 header-only で、`projects/APPLaunch/main/ui/page_app/` 配下に置かれ、`build/generated/include/generated/page_app.h` によって集約されます。 + +簡略化した関係: + +```text +UILaunchPage home carousel + | + v +Launch::launch_app() + | + +-- External command: cp0_process_exec_blocking() + +-- Terminal command: UIConsolePage + PTY + +-- Built-in page: std::make_shared() + | + v + lv_disp_load_scr(page->screen()) +``` + +## 2. Page Base-Class Hierarchy + +### 2.1 `AppPageRoot` + +`AppPageRoot` はすべての組み込みページのルート基底クラスです。場所は `projects/APPLaunch/main/ui/ui_app_page.hpp` です。独立した screen と LVGL 入力グループを作成します。 + +```cpp +class AppPageRoot +{ +public: + std::string page_title_ = "APP"; + lv_group_t *input_group_; + lv_obj_t *root_screen_; + std::function navigate_home; + bool has_bottom_bar_ = false; + int top_bar_height_px_ = 20; + + AppPageRoot() + { + creat_base_UI(); + creat_input_group(); + } + + virtual ~AppPageRoot() + { + lv_obj_del(root_screen_); + lv_group_delete(input_group_); + } +}; +``` + +重要な点: + +- `root_screen_` はページ自身の top-level screen であり、ホーム `UILaunchPage::screen()` の子ではありません。 +- デフォルトでは、`input_group_` には `root_screen_` だけが含まれます。ページ起動時、これが現在の `lv_indev_t` にバインドされます。 +- `navigate_home` は `Launch` によって注入されます。ページは ESC またはタスク完了後にこれを呼んでホームへ戻ります。 +- デストラクタは `root_screen_` と `input_group_` を削除するため、ページ内に作成された LVGL child objects は screen とともに解放されます。 + +### 2.2 Top Bar, Content Area, and Bottom Bar Regions + +`ui_app_page.hpp` はページを複数の再利用可能な領域に分割します。 + +| Class | Responsibility | Default size | +| --- | --- | --- | +| `AppTopBarRegion` | タイトル、WiFi、時刻、バッテリーを表示する上部ステータスバーを作成する | Height `20px` | +| `AppContentRegion` | `ui_APP_Container` コンテンツ領域を作成する | Height `150px`、bottom bar がある場合は `130px` | +| `AppBottomBarRegion` | `ui_BOTTOM_Container` bottom bar を作成する | Height `20px` | +| `AppPageLayout` | Top bar + content area | `320x170` 内で `20+150` | +| `AppPageWithBottomBarLayout` | Top bar + content area + bottom bar | `20+130+20` | +| `home_base` | ホーム専用の基底クラス。AppPage と完全に同等ではない | Home status bar + carousel container | + +典型的なページは `AppPage` を直接継承します。 + +```cpp +class UIIpPanelPage : public AppPage +{ +public: + UIIpPanelPage() : AppPage() + { + set_page_title("IP INFO"); + creat_UI(); + event_handler_init(); + } +}; +``` + +一部のゲームやフルスクリーンページは `AppPageRoot` を継承し、デフォルトの top bar を使わずに `320x170` 全体を自分で占有します。例として `UIGamePage`、`UICompassPage`、`UITankBattlePage` があります。 + +## 3. Top Bar and Status Refresh + +共通 top bar は `UIAppTopBar` によって実装され、次を含みます。 + +- 左側タイトル: `set_page_title()` は最終的に `top_bar_.set_title()` を更新します。 +- WiFi 信号: `cp0_wifi_get_status()`。未接続時は WiFi panel を非表示にします。 +- 時刻: `cp0_time_str()`。デフォルトでは 5 秒ごとに更新されます。 +- バッテリー: `LV_EVENT_BATTERY` に応答し、`cp0_battery_info_t` を使ってパーセントと bar を更新します。 + +主要ソースパス: + +- `projects/APPLaunch/main/ui/ui_app_page.hpp`: `UIAppTopBar`、`AppTopBarRegion`。 +- `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`: `cp0_wifi_get_status()`、`cp0_time_str()`、`cp0_battery_read()` などのインターフェース宣言。 + +Top-bar リソースは `cp0_file_path_c()` を使います。 + +```cpp +lv_obj_set_style_bg_img_src(time_panel_, + cp0_file_path_c("status_time_background.png"), + LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +注意: 通常の組み込みページは自身のステータス更新タイマーを持ちます。ページが自分で作成したタイマーはデストラクタで解放する必要があります。`AppTopBarRegion` は top-bar status timer をすでに解放します。 + +## 4. Page Lifecycle + +### 4.1 Launching a Built-in Page from Home + +`launch.cpp` は template を通して組み込みページの app descriptor を構築します。 + +```cpp +template +app::app(std::string name, std::string icon, page_t) + : Name(std::move(name)), Icon(std::move(icon)) +{ + launch = [](Launch *ctx) { + auto p = std::make_shared(); + ctx->app_Page = p; + p->navigate_home = std::bind(&Launch::go_back_home, ctx); + lv_disp_load_scr(p->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); + }; +} +``` + +`projects/APPLaunch/main/ui/launch.cpp` の実際のコードでは、中核フローは次のとおりです。 + +1. ユーザーがホーム画面で ENTER を離すと、`UILaunchPage::handle_home_key()` が `launch_selected_app()` を呼びます。 +2. `UILaunchPage::launch_selected_app()` は `Launch::launch_app()` へ転送します。 +3. `Launch::launch_app()` は現在の app を見つけ、その app の `launch` 関数を実行します。 +4. 組み込みページオブジェクトが作成され、screen が読み込まれ、入力グループが切り替わります。 +5. ページは ESC 後、または業務ロジック完了後に `navigate_home()` を呼びます。 +6. `Launch::go_back_home()` は `lv_async_call()` を使ってホーム画面へ戻り、ホーム入力グループを再バインドし、`app_Page` を reset します。 + +### 4.2 Returning Home + +ホームへ戻る処理はすべて `navigate_home` 経由にしてください。ページ内から直接ページを削除してはいけません。 + +```cpp +if (navigate_home) + navigate_home(); +``` + +`Launch::lv_go_back_home()` は次を行います。 + +- `lv_timer_enable(true)` で LVGL タイマーを復元します。 +- `UILaunchPage::bind_home_input_group()` でホーム入力グループをバインドします。 +- `launch_page_->show_home_screen()` でホーム画面を読み込み、ホーム入力グループをバインドします。 +- `app_Page.reset()` で現在ページオブジェクトを解放します。 + +注意: + +- ページのデストラクタは、ページが作成した `lv_timer_t`、バックグラウンドスレッド、ファイル watcher、PTY、音声リソースを停止する必要があります。 +- キーボードイベントコールバックスタック内で直接 `delete this` しないでください。`navigate_home` を使い、`Launch` に非同期で処理させてください。 +- ページが一時的に子ページや nested page へ切り替える場合は、正しい入力グループを復元する必要があります。 + +## 5. Current Built-in Page List + +ページ実装は `projects/APPLaunch/main/ui/page_app/` に集中しています。 + +| Page class | File | Launcher name | Inheritance | Description | +| --- | --- | --- | --- | --- | +| `UIConsolePage` | `ui_app_console.hpp` | `CLI` or terminal external command | `AppPage` | 端末エミュレータ。PTY read/write、ANSI/VT シーケンス、キーボード escape sequence をサポート | +| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | Snake game。フルスクリーンのカスタム描画で LVGL timer により駆動 | +| `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | システム設定、アプリ切り替え、brightness、volume、WiFi、camera resolution など | +| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPage` | 組み込みゲームエントリ | +| `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | コンパスページ。sensor thread + UI timer | +| `UIIpPanelPage` | `ui_app_ip_panel.hpp` | `IP_PANEL` | `AppPage` | ネットワークインターフェース/IP 情報リスト。毎秒更新 | +| `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | ファイルブラウザ。ディレクトリ一覧と enter/back navigation | +| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH パラメータ入力。接続後に `UIConsolePage` を埋め込む | +| `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh メッセージ一覧、入力 overlay、send/refresh | +| `UIRecPage` | `ui_app_rec.hpp` | `REC` | Custom `rec_page` | 録音/再生/ファイル一覧と非同期リソース管理 | +| `UICameraPage` | `ui_app_camera.hpp` | `CAMERA` | `AppPage` | カメラ preview、gallery、capture、status page | +| `UILoraPage` | `ui_app_lora.hpp` | `LORA` | `AppPage` | LoRa 業務ページ。内部に C スタイルの create/destroy インターフェースも含む | +| `UITankBattlePage` | `ui_app_tank_battle.hpp` | `TANK` | `AppPageRoot` | Tank mini-game。フルスクリーン、固定キー mapping | + +`Python`、`STORE`、`MATH` は組み込みページではありません。コマンドまたは外部プロセスとして起動されます。 + +## 6. Page Registration and Display Order + +組み込みページは `Launch::Launch()` で `app_list` に挿入されます。最初の 5 つの固定アプリケーションが、まず 5 つのホームカルーセルスロットを初期化します。 + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +組み込みページの表示可否は現在 `kBuiltinApps[]` と `AppDescriptor.config_key` によって駆動されます。`Launch::rebuild_builtin_apps()` は各 descriptor を追加する前に `launcher_app_registry_is_enabled()` を呼び、Settings の変更は `launcher_app_registry_set_enabled()` の後に `Launch::applications_reload()` を呼びます。 + +規約: + +- `Store`、`CLI`、`Game`、`Setting` は settings page で常に有効で、無効化できません。 +- `Compass` は現在 `launch.cpp` で無条件に追加され、`UISetupPage` の Launcher toggle list では制御されません。 +- `IP_PANEL`、`FILE`、`SSH`、`MESH`、`REC`、`CAMERA`、`LORA`、`TANK` などのページは Linux デバイスビルドでのみ追加されます。SDL ビルドでは `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` により制限されます。 +- 動的 `.desktop` アプリケーションは組み込みページの後にスキャンされ追加されます。ディレクトリ変更は watcher により 3 秒ごとに確認されます。 + +## 7. Page Code Skeleton + +新しい通常ページは、一般に `AppPage` を継承します。 + +```cpp +#pragma once +#include "../ui_app_page.hpp" +#include "compat/input_keys.h" + +class UINewPage : public AppPage +{ +public: + UINewPage() : AppPage() + { + set_page_title("NEW"); + create_ui(); + event_handler_init(); + } + + ~UINewPage() + { + if (timer_) { + lv_timer_delete(timer_); + timer_ = nullptr; + } + } + +private: + lv_timer_t *timer_ = nullptr; + + void create_ui() + { + lv_obj_t *bg = lv_obj_create(ui_APP_Container); + lv_obj_set_size(bg, 320, 150); + lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); + } + + void event_handler_init() + { + lv_obj_add_event_cb(root_screen_, &UINewPage::event_cb, LV_EVENT_ALL, this); + } + + static void event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self || !IS_KEY_RELEASED(e)) + return; + + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + if (key == KEY_ESC && self->navigate_home) + self->navigate_home(); + } +}; +``` + +新しいフルスクリーンページは `AppPageRoot` を継承してもかまいませんが、`320x170` レイアウト、ステータスヒント、戻るキーを自分で扱う必要があります。 + +## 8. Page UI Conventions + +- `320x170` 解像度を前提に設計します。共通ページコンテンツ領域は `320x150` で、上部 `20px` は top bar が占有します。 +- ページオブジェクトは通常 `std::unordered_map ui_obj_` に保存し、再描画や削除をしやすくします。 +- リストページでは、小さな画面で focus が混乱しないよう、自由な LVGL container scrolling よりも固定 row height + virtual scrolling を優先してください。 +- 頻繁に更新するページでは `lv_timer_create()` を使い、デストラクタで `lv_timer_delete()` を呼びます。 +- バックグラウンドスレッドや非同期コールバックには `std::atomic` alive flag を使い、解放済みページにコールバックが触れないようデストラクタでスレッドを停止します。 +- 画像、音声、フォントの相対パスをハードコードしないでください。`img_path()`、`audio_path()`、`cp0_file_path_c()` を使います。 + +## 9. Nested Pages and Special Pages + +`UISSHPage` は典型的な nested page です。SSH パラメータ入力中は `UISSHPage` がキーボードを処理します。接続後は `UIConsolePage` を作成し、screen と input group を切り替えます。 + +```cpp +console_page_ = std::make_shared(); +console_page_->navigate_home = [this]() { + console_page_.reset(); + view_state_ = ViewState::INPUT; + lv_disp_load_scr(this->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); +}; + +lv_disp_load_scr(console_page_->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); +``` + +この種のページには特別な注意が必要です。 + +- 子ページの終了は必ずしもホーム復帰を意味しません。親ページへ戻るだけの場合があります。 +- 入力グループは現在の画面と一緒に切り替える必要があります。そうしないとキーが見えないページへ配送されます。 +- 親ページのデストラクタは、先に子ページオブジェクトを解放する必要があります。 + +## 10. Relationship with the Home Carousel + +ホームカルーセル自体は `ui_launch_page.cpp` が管理します。 + +- `carousel_elements` は 5 枚のカード、5 つのタイトル、5 つのページドットを保存します。 +- 左右切り替え時には `switch_left()` / `switch_right()` が呼ばれます。アニメーション完了後、配列が回転され、`Launch` が far-side スロットの内容を更新します。 +- ENTER は `UILaunchPage::launch_selected_app()` を発火し、最終的に現在 app の `launch()` を呼びます。 + +組み込みページはホームカルーセルを直接操作しません。ホームへ戻った後、カルーセル状態は `Launch` により保持されます。 + +## 11. Common Notes + +- ページコンストラクタ内で長時間ブロックする処理を行わないでください。まずページまたは loading 状態を表示し、その後タスクを開始してください。 +- `lv_indev_get_next(NULL)` が常に non-null だと仮定しないでください。入力グループを切り替える前に確認するのが望ましいです。 +- 明確にホーム画面機能でない限り、ページからホームのグローバルオブジェクトへ直接アクセスしないでください。 +- ページタイトルは内部 top-bar label を直接変更せず、`set_page_title()` を呼びます。 +- 終了可能なすべてのページは `KEY_ESC` をサポートし、`navigate_home` または前の view へ戻る処理を呼ぶ必要があります。 +- ページ toggle key は `UISetupPage::save_app_toggle()` と `launch.cpp` の `APP_ENABLED()` と一貫している必要があります。 diff --git a/docs/launcher-project-guide-ja/06-resources-and-configuration.md b/docs/launcher-project-guide-ja/06-resources-and-configuration.md new file mode 100644 index 00000000..b0ccc172 --- /dev/null +++ b/docs/launcher-project-guide-ja/06-resources-and-configuration.md @@ -0,0 +1,361 @@ +# 06 - リソースと設定システム + +この章では、APPLaunch の実行時リソースディレクトリ、パス解決ルール、`.desktop` 動的アプリケーションファイル、設定 API、設定ページの設定キー、リソース利用時の注意点を説明します。主なソースファイルは `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`、`projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` です。 + +## 1. リソースシステムの概要 + +APPLaunch のページコードでは、実行時パスを手動で連結しないでください。代わりに `cp0_file_path()` / `cp0_file_path_c()` を使い、統一されたルールで解決します。 + +```text +Page code / launch.cpp + | + v +img_path(), audio_path(), cp0_file_path_c() + | + v +cp0_lvgl_file.cpp / sdl_lvgl_file.cpp + | + +-- Images: share/images/... + +-- Audio: /usr/share/APPLaunch/share/audio/... + +-- Fonts: /usr/share/APPLaunch/share/font/... + +-- applications: /usr/share/APPLaunch/applications + +-- Special paths such as keyboard_device / keyboard_map / lock_file +``` + +ページ向けの共通ラッパー関数は `projects/APPLaunch/main/ui/ui_app_page.hpp` にあります。 + +```cpp +static inline std::string img_path(const char *name) +{ + return cp0_file_path(name); +} + +static inline std::string audio_path(const char *name) +{ + return cp0_file_path(name); +} +``` + +## 2. 実行時リソースツリー + +ソース側のリソースツリーは次の場所にあります。 + +```text +projects/APPLaunch/APPLaunch/ +├── applications/ +├── bin/ +├── lib/ +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +デバイスへインストールした後は、通常次の場所へ対応します。 + +```text +/usr/share/APPLaunch/ +├── applications/ +├── bin/ +├── lib/ +└── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +| Directory | 内容 | 使用箇所 | +| --- | --- | --- | +| `applications/` | `.desktop` アプリケーション記述子 | `Launch::applications_load()` | +| `share/images/` | アイコン、ステータスバー背景、ページ画像、GIF | ホーム画面、トップバー、組み込みページ | +| `share/audio/` | `startup.mp3`、`switch.wav`、`enter.wav`、ページ用キー音 | ホーム効果音、設定ページ、ページ効果音 | +| `share/font/` | TTF/OTF フォント | `LauncherFonts`、ページのカスタムフォント | +| `bin/` | パッケージ同梱スクリプトと外部プログラム | Store、更新スクリプト、動的アプリケーション | +| `lib/` | パッケージ同梱の動的ライブラリ | 外部プログラムまたはプラットフォーム機能 | + +## 3. パス解決ルール + +### 3.1 デバイス側 `cp0_lvgl_file.cpp` + +デバイス側の実装は `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` にあり、ルートディレクトリは固定です。 + +```cpp +constexpr const char *kAppRoot = "/usr/share/APPLaunch"; +``` + +主なルール: + +| Input | Output | +| --- | --- | +| `applications` | `/usr/share/APPLaunch/applications` | +| `lock_file` | `/tmp/M5CardputerZero-APPLaunch_fcntl.lock` | +| `keyboard_device` | `/dev/input/by-path/platform-3f804000.i2c-event` | +| `keyboard_map` | `/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map` | +| `store_sync_cmd` | `python /usr/share/APPLaunch/bin/store_cache_sync.py` | +| `*.png` / `*.gif` / `*.jpg` / `*.jpeg` / `*.svg` | `share/images/` | +| `*.wav` / `*.mp3` / `*.ogg` | `/usr/share/APPLaunch/share/audio/` | +| `*.ttf` / `*.otf` | `/usr/share/APPLaunch/share/font/` | +| その他の文字列 | そのまま返す | + +現在のデバイス側画像ルールは `share/images/` のような相対パスを返します。一方、音声とフォントは `/usr/share/APPLaunch/...` 配下の絶対パスを返します。ページコードでは既存の `img_path("xxx.png")` という慣例に従い、複数のルートディレクトリを混在させないでください。 + +### 3.2 SDL 実装 `sdl_lvgl_file.cpp` + +SDL 実装は `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` にあります。ルールはデバイス側とほぼ同じですが、ルートは `get_app_root_path()` で決まり、`app_relative_path(root_path, file, "share/images/")` などの関数が開発マシン上の実行ディレクトリに合わせて調整します。 + +特殊名は SDL 側でも維持されます。 + +```cpp +if (file == "applications") return root_path + "/applications"; +if (file == "keyboard_device") return "/dev/input/by-path/platform-3f804000.i2c-event"; +if (file == "keyboard_map") return "/usr/share/keymaps/tca8418_keypad_m5stack_keymap.map"; +``` + +注意: SDL モードは、APPLaunch を開発マシンで実行できるようにするためのものです。すべてのデバイスリソースが存在するという意味ではありません。たとえば camera、LoRa、一部の Linux デバイスページは、コンパイル時条件で除外されるか、実行時に利用できない場合があります。 + +### 3.3 `cp0_file_path_c()` のキャッシュ + +C インターフェースは `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` で宣言されています。 + +```c +const char *cp0_file_path_c(const char *file); +``` + +実装では `thread_local std::unordered_map` キャッシュを使います。 + +```cpp +extern "C" const char *cp0_file_path_c(const char *file) +{ + static thread_local std::unordered_map paths; + std::string key = file ? std::string(file) : std::string(); + auto it = paths.find(key); + if (it == paths.end()) { + it = paths.emplace(key, cp0_file_path(key)).first; + } + return it->second.c_str(); +} +``` + +そのため、返される `const char *` は同じスレッド内では安定しており、LVGL の style API や image API に直接渡せます。スレッドをまたいでポインタを保存する場合は、代わりに `std::string` として保存してください。 + +## 4. 画像、音声、フォントの使用例 + +### 4.1 画像 + +ホーム画面や組み込みページでの一般的な使用例: + +```cpp +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); + +lv_obj_set_style_bg_img_src(time_panel_, + cp0_file_path_c("status_time_background.png"), + LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +ホームカードのアイコンは `launch.cpp::panel_set_icon()` で設定されます。 + +```cpp +static void panel_set_icon(lv_obj_t *panel, const char *src) +{ + lv_obj_t *img = lv_obj_get_child(panel, 0); + if (!img || !lv_obj_check_type(img, &lv_image_class)) { + img = lv_image_create(panel); + lv_obj_set_size(img, LV_PCT(100), LV_PCT(100)); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_STRETCH); + } + lv_image_set_src(img, src); +} +``` + +注意: `panel_set_icon()` は `access(icon_src, R_OK)` を確認してログを書きます。デバイス側の画像パスが相対パスの場合、実行時のカレントディレクトリが正しくなければ、ログには画像が存在しない、または読めないと出ます。 + +### 4.2 音声 + +ホームの効果音は、アセット名をシステム音声シグナルへ渡して再生します。 + +```cpp +static void audio_play_ui_asset(const char *name) +{ + cp0_signal_system_play_asset(name); +} + +static void audio_play_switch(void) { audio_play_ui_asset("switch.wav"); } +static void audio_play_enter(void) { audio_play_ui_asset("enter.wav"); } +``` + +起動音はホーム画面の読み込み後に再生されます。 + +```cpp +cp0_signal_audio_api_play_asset("startup.mp3"); +``` + +ページ内でファイルパスが必要な場合は `audio_path("key_enter.wav")` を使います。下位 API がパスではなくアセット名を期待している場合は、二重にパス解決しないよう、アセット名を直接渡してください。 + +### 4.3 フォント + +ホーム画面とトップバーは `LauncherFonts` で freetype フォントを管理します。 + +```cpp +launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD) +``` + +フォントパスは最終的に `cp0_file_path()` により `share/font/` へ解決されます。freetype フォントの読み込みに失敗した場合、`LauncherFonts` は LVGL 内蔵の Montserrat フォントへフォールバックします。 + +## 5. `.desktop` 動的アプリケーション + +動的アプリケーションファイルは `cp0_file_path("applications")` が指すディレクトリに置きます。`Launch::applications_load()` は `*.desktop` ファイルだけを処理し、`[Desktop Entry]` セクションを解析します。 + +対応しているキー: + +| key | 必須 | 説明 | +| --- | --- | --- | +| `Name` | Yes | カルーセルに表示する名前 | +| `Exec` | Yes | 起動コマンドまたは実行ファイルパス | +| `Icon` | No | アイコンパス。`share/images/...` または LVGL が読めるパスを指定可能 | +| `Terminal` | No | `true`/`True`/`1` の場合は `UIConsolePage` 内で実行 | +| `Sysplause` | No | ターミナルコマンド終了後に一時停止してユーザー確認を待つかどうか。既定は `true` | + +例: + +```ini +[Desktop Entry] +Name=Vim +TryExec=vim +Exec=vim +Terminal=true +Icon=share/images/e-Mail_80.png +Sysplause=true +``` + +読み込みルール: + +- `[Desktop Entry]` セクション内の key-value ペアだけを読みます。 +- 空行、および `#` または `;` で始まるコメントはスキップされます。 +- `Name` または `Exec` のどちらかが欠けているエントリはスキップされます。 +- `Exec` が既存アプリと重複する場合、そのエントリはスキップされます。 +- `TryExec` は現在 `applications_load()` では使われていません。 +- `applications/` ディレクトリは監視されます。3 秒ごとにポーリングされ、変更があると動的アプリをクリアしてディレクトリを再スキャンします。 + +## 6. 設定 API + +設定インターフェースは `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` で宣言されています。 + +```c +void cp0_config_init(void); +int cp0_config_get_int(const char *key, int default_val); +void cp0_config_set_int(const char *key, int val); +const char *cp0_config_get_str(const char *key, const char *default_val); +void cp0_config_set_str(const char *key, const char *val); +void cp0_config_save(void); +``` + +利用上の慣例: + +- 読み取り時は必ずデフォルト値を渡し、設定が欠けていてもページが動作を継続できるようにします。 +- 変更を永続化するには、書き込み後に `cp0_config_save()` を呼びます。 +- デバイス側実装は `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` にあります。 +- SDL 互換実装は `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` にあります。 + +典型的な使い方: + +```cpp +int volume = cp0_config_get_int("volume", cp0_volume_read()); +cp0_volume_write(new_val); +cp0_config_set_int("volume", new_val); +cp0_config_save(); +``` + +## 7. 設定キー一覧 + +### 7.1 Launcher アプリケーショントグル + +`UISetupPage` の `Launcher` メニューは `app_` を保存します。 + +| Configuration key | Default | 意味 | 備考 | +| --- | --- | --- | --- | +| `app_Python` | `1` | Python エントリ表示トグル | 設定には表示されますが、Python は `launch.cpp` で固定されています。現在このトグルは固定エントリには影響しません | +| `app_Store` | `1` | Store エントリ | 常時有効、無効化不可 | +| `app_CLI` | `1` | CLI エントリ | 常時有効、無効化不可 | +| `app_Game` | `1` | GAME エントリ | 常時有効、無効化不可 | +| `app_Setting` | `1` | SETTING エントリ | 常時有効、無効化不可 | +| `app_Game` | `1` | GAME 組み込みページ | `launch.cpp` から読み取られる | +| `app_Math` | `1` | Calculator 外部アプリケーション | `launch.cpp` から読み取られる | +| `app_IP_Panel` | `1` | IP_PANEL 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_File` | `1` | FILE 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_SSH` | `1` | SSH 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_Mesh` | `1` | MESH 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_Rec` | `1` | REC 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_Camera` | `1` | CAMERA 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_LoRa` | `1` | LORA 組み込みページ | Linux 非 SDL ビルドで読み取られる | +| `app_Tank` | `1` | TANK 組み込みページ | Linux 非 SDL ビルドで読み取られる | + +注意: `Compass` には現在対応する `app_Compass` 設定がなく、`launch.cpp` により無条件で追加されます。 + +### 7.2 システムとページ設定 + +| Configuration key | 読み書き箇所 | 意味 | +| --- | --- | --- | +| `brightness` | `UISetupPage`, `ext_components/cp0_lvgl/src/commount.c` | バックライト輝度値。起動時に復元され、設定ページから書き込まれる | +| `volume` | `UISetupPage`, `commount.c` | システム音量。起動時に復元され、設定ページから書き込まれる | +| `dark_time` | `UISetupPage` | 画面オフタイムアウト。選択肢は `0/10/30/60/300` 秒 | +| `cam_resolution` | `UISetupPage`, camera page may read it | カメラ解像度オプションのインデックス | +| `startup_mode` | `UISetupPage` | 起動モード。現在は `Launcher` / `CLI` | +| `extport_usb` | `UISetupPage` | 拡張ポート USB トグル | +| `extport_5vout` | `UISetupPage` | 拡張ポート 5V 出力トグル | +| `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | 外部プロセス / PTY コマンドで権限を下げる際のユーザー設定 | + +### 7.3 一時的な業務入力 + +次のものは主にメモリ上のページ状態で、既定では永続化されません。 + +- `UISSHPage` の Host/Port/User のデフォルト値はコンストラクタで初期化され、設定には書き込まれません。 +- `UIMeshPage` のメッセージ入力バッファはページメモリ内にだけ存在します。 +- `UIFilePage` の現在パスと選択行はページメモリ内にだけ存在します。 +- `UIIpPanelPage` のネットワークインターフェース一覧は `cp0_network_list()` から毎秒更新されます。 + +## 8. 設定ページの書き込み経路 + +`UISetupPage` は設定キーが集中している場所です。典型的な関数: + +- `menu_init()`: 設定メニューを構築し、`app_*`、`extport_*` を読み取ります。 +- `save_app_toggle()`: ランチャーアプリのトグルを保存します。 +- `enter_brightness_adjust()` / `apply_value_selection()`: 輝度、音量、画面オフタイムアウト、解像度、起動モードを適用します。 +- `apply_volume()`: システム音量を書き込み、`volume` を保存します。 + +例: + +```cpp +void save_app_toggle(int idx) +{ + char cfg_key[64]; + snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); + bool enabled = menu_items_[0].sub_items[idx].toggle_state; + cp0_config_set_int(cfg_key, enabled ? 1 : 0); + cp0_config_save(); +} +``` + +設定キーを変更するときは、次のすべてを同期して確認してください。 + +- `UISetupPage::menu_init()` の `app_keys` / `app_labels`。 +- `UISetupPage::save_app_toggle()` の `app_keys` と常時有効リスト。 +- `launch.cpp` の `APP_ENABLED("...")`。 +- ドキュメントとデフォルト設定。 + +## 9. リソース命名の推奨事項 + +- ホームアイコンは `game_100.png` や `setting_100.png` のように `_100.png` と命名します。 +- 小さなアイコンやステータス背景は、`status_time_background.png` や `status_battery_background.png` のように機能で命名します。 +- ページ固有リソースには `setting_ok.png` や `setting_cross.png` のようにページ接頭辞を付けます。 +- 効果音は `switch.wav`、`enter.wav`、`key_back.wav` のように短い名前を使います。 +- フォントは `Montserrat-Bold.ttf` のように実ファイル名を使い、`launcher_fonts().get()` で読み込みます。 + +## 10. よくある問題と注意事項 + +- 画像と音声の拡張子は分類時に小文字化されますが、ファイルシステム自体は大文字小文字を区別するため、ファイル名そのものは一致している必要があります。 +- `cp0_file_path()` は拡張子だけで分類し、ファイルが存在するかは確認しません。 +- `.desktop` の `Icon` 値は自動的に `cp0_file_path()` を呼びません。LVGL が直接読めるパスにするか、既存テンプレートと整合させてください。 +- デバイス側で新しいリソースを使う場合、パッケージングスクリプトが `projects/APPLaunch/APPLaunch/share/...` をインストールパッケージへ含めていることを確認してください。 +- 設定を書き込んだ後に `cp0_config_save()` を忘れると、再起動後に値が失われます。 +- `app_*` トグルは次回 `Launch` が構築されるときのリストに影響します。実行中に変更しても、再構築/再起動が発生するかどうかによって、固定ホームリストにはすぐ反映されない場合があります。 +- `run_as_user` は外部プロセスと PTY コマンドの実行ユーザーに影響します。権限問題をデバッグするときはこの設定を確認してください。 diff --git a/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md b/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md new file mode 100644 index 00000000..cdb6097b --- /dev/null +++ b/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md @@ -0,0 +1,420 @@ +# 07 - 入力システムとキー割り当て + +この章では、APPLaunch のキーボード入力スレッド、`key_item` イベント構造、LVGL イベント配送、ホーム画面と組み込みページのキー割り当て、ターミナル入力のエスケープ、デバッグ時の注意点を説明します。主なソースファイルは `ext_components/cp0_lvgl/include/keyboard_input.h`、`projects/APPLaunch/main/ui/ui.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`、`projects/APPLaunch/main/ui/ui_launch_page.cpp`、`projects/APPLaunch/main/ui/page_app/*.hpp` です。 + +## 1. 入力システムの概要 + +APPLaunch には 2 つの入力経路があります。 + +1. カスタム `LV_EVENT_KEYBOARD`: 完全な `struct key_item` を運び、多くのページが直接これを監視します。 +2. LVGL indev key: `cp0_keypad_read_cb()` は evdev キーを `LV_KEY_*` にも変換し、LVGL の group/focus 機構で使えるようにします。 + +データフロー: + +```text +Physical keyboard / SDL keyboard + | + v +libinput / SDL keyboard backend + | + v +keyboard_read_thread() + | + v +enqueue_key(struct key_item) + | + v +keyboard_queue + keyboard_mutex + | + v +cp0_keypad_read_cb() + | + +-- lv_obj_send_event(lv_screen_active(), LV_EVENT_KEYBOARD, key_item) + +-- ui_global_hint_on_key(key_item) + +-- data->key = cp0_evdev_process_key(key_code) +``` + +`LV_EVENT_KEYBOARD` は APPLaunch のカスタムイベントであり、LVGL 組み込みのキーイベントではありません。起動時に `main.cpp` で登録されます。 + +```cpp +if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); +``` + +## 2. `key_item` データ構造 + +`ext_components/cp0_lvgl/include/keyboard_input.h` は入力イベントを定義します。 + +```c +struct key_item { + uint32_t key_code; // Linux evdev key code + uint32_t keysym; // primary XKB keysym + uint32_t codepoint; // Unicode code point, 0 if there is no character + uint32_t mods; // KBD_MOD_* modifier bitmap + int key_state; // 0=released, 1=pressed, 2=repeat + char sym_name[65]; // XKB keysym name + char utf8[16]; // UTF-8 character + char flage; + STAILQ_ENTRY(key_item) entries; +}; +``` + +定数: + +| Constant | 値/意味 | +| --- | --- | +| `KBD_KEY_RELEASED` | `0`、リリース | +| `KBD_KEY_PRESSED` | `1`、押下 | +| `KBD_KEY_REPEATED` | `2`、長押しリピート | +| `KBD_MOD_SHIFT` | Shift 修飾子 | +| `KBD_MOD_CTRL` | Ctrl 修飾子 | +| `KBD_MOD_ALT` | Alt 修飾子 | +| `KBD_MOD_LOGO` | Logo 修飾子 | +| `KBD_MOD_CAPS` | CapsLock 状態 | +| `KBD_MOD_NUM` | NumLock 状態 | + +ページは物理キー判定に `key_code` を使うことも、テキスト入力の読み取りに `utf8` / `codepoint` を使うこともできます。 + +## 3. イベントマクロとページ側アクセスパターン + +`projects/APPLaunch/main/ui/ui.h` は共通マクロを提供します。 + +```c +#define LV_EVENT_KEYBOARD_GET_KEY(e) \ + ((struct key_item *)lv_event_get_param(e))->key_code + +#define LV_EVENT_KEYBOARD_GET_KEY_STATE(e) \ + ((struct key_item *)lv_event_get_param(e))->key_state + +#define IS_KEY_PRESSED(e) \ + ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && \ + (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) > 0)) + +#define IS_KEY_RELEASED(e) \ + ((lv_event_get_code(e) == LV_EVENT_KEYBOARD) && \ + (LV_EVENT_KEYBOARD_GET_KEY_STATE(e) == 0)) +``` + +典型的なページイベントのバインド: + +```cpp +void event_handler_init() +{ + lv_obj_add_event_cb(root_screen_, UIIpPanelPage::static_lvgl_handler, + LV_EVENT_ALL, this); +} + +static void static_lvgl_handler(lv_event_t *e) +{ + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self || !IS_KEY_RELEASED(e)) + return; + + uint32_t key = LV_EVENT_KEYBOARD_GET_KEY(e); + self->handle_key(key); +} +``` + +注意: 多くのメニューページは press と repeat による重複トリガーを避けるため、キーリリース時だけ処理します。ゲーム系ページでは、移動や発射を press/repeat で処理することがあります。 + +## 4. デバイス側入力スレッド + +デバイス実装は `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` にあります。 + +### 4.1 初期化 + +`init_input()` は 3 つの処理を行います。 + +```c +if (LV_EVENT_KEYBOARD == 0) + LV_EVENT_KEYBOARD = lv_event_register_id(); + +pthread_create(&keyboard_read_thread_id, NULL, + keyboard_read_thread, (void *)keyboard_device); + +cp0_create_lvgl_input_devices(); +``` + +キーボードデバイスのデフォルトは次の環境変数から取得されます。 + +```c +const char *keyboard_device = getenv("APPLAUNCH_LINUX_KEYBOARD_DEVICE"); +``` + +環境変数が空の場合、`keyboard_read_thread()` は次のデフォルトを使います。 + +```text +/dev/input/by-path/platform-3f804000.i2c-event +``` + +このパスは `cp0_file_path("keyboard_device")` でも取得できます。 + +### 4.2 読み取りとキュー投入 + +`keyboard_read_thread()` は libinput でキーボードイベントを監視し、xkbcommon で `keysym`、`codepoint`、`utf8` を生成し、timerfd でリピートイベントを生成します。 + +キュー投入関数 `enqueue_key()`: + +```c +static void enqueue_key(const struct key_item *src) { + struct key_item *elm = malloc(sizeof(*elm)); + *elm = *src; + + if (elm->key_code == KEY_ESC) { + LVGL_HOME_KEY_FLAG = elm->key_state; + } + + if (LVGL_RUN_FLAGE) { + pthread_mutex_lock(&keyboard_mutex); + STAILQ_INSERT_TAIL(&keyboard_queue, elm, entries); + pthread_mutex_unlock(&keyboard_mutex); + } else { + free(elm); + } +} +``` + +主なグローバル状態: + +| Variable | 意味 | +| --- | --- | +| `keyboard_queue` | LVGL が消費するのを待っている `key_item` イベントのキュー | +| `keyboard_mutex` | キューのロック | +| `LVGL_HOME_KEY_FLAG` | 現在の ESC 状態。外部アプリ実行中の長押し戻り / プロセス kill ロジックで使用 | +| `LVGL_RUN_FLAGE` | LVGL が入力を受け付けるかどうか。外部アプリ実行中は 0 にされる場合がある | +| `LV_EVENT_KEYBOARD` | カスタム LVGL イベント id | + +### 4.3 キュー取り出しと配送 + +`cp0_keypad_read_cb()` はキューからイベントを取り出し、現在のアクティブ画面へ配送します。 + +```c +lv_obj_t *root = lv_screen_active(); +if (root) + lv_obj_send_event(root, (lv_event_code_t)LV_EVENT_KEYBOARD, elm); + +ui_global_hint_on_key(elm); + +data->key = cp0_evdev_process_key(elm->key_code); +if (data->key) { + data->state = (lv_indev_state_t)elm->key_state; + data->continue_reading = !STAILQ_EMPTY(&keyboard_queue); +} +free(elm); +``` + +注意: `elm` はコールバック復帰後に解放されるため、ページは `lv_event_get_param(e)` で返されたポインタを長期保存してはいけません。非同期利用が必要ならフィールドをコピーしてください。 + +## 5. evdev から LVGL キーへの変換 + +`cp0_evdev_process_key()` は一部の Linux evdev キーを LVGL ナビゲーションキーへ変換します。 + +| evdev key | LVGL key | +| --- | --- | +| `KEY_UP` | `LV_KEY_UP` | +| `KEY_DOWN` | `LV_KEY_DOWN` | +| `KEY_LEFT` | `LV_KEY_LEFT` | +| `KEY_RIGHT` | `LV_KEY_RIGHT` | +| `KEY_ESC` | `LV_KEY_ESC` | +| `KEY_DELETE` | `LV_KEY_DEL` | +| `KEY_BACKSPACE` | `LV_KEY_BACKSPACE` | +| `KEY_ENTER` | `LV_KEY_ENTER` | +| `KEY_NEXT` | `LV_KEY_NEXT` | +| `KEY_PREVIOUS` | `LV_KEY_PREV` | +| `KEY_HOME` | `LV_KEY_HOME` | +| `KEY_END` | `LV_KEY_END` | + +ページが `LV_EVENT_KEYBOARD` を直接処理する場合、通常は生の `KEY_*` 値を使います。ページが LVGL のウィジェットフォーカス機構へ委譲する場合は、`data->key` に依存します。 + +`ext_components/cp0_lvgl/include/compat/input_keys.h` は Linux では `` をインクルードし、非 Linux プラットフォームでは一般的な互換 `KEY_*` 定義を提供するため、SDL/デスクトップビルドでもページコードをコンパイルできます。 + +## 6. ホーム画面のキー割り当て + +ホーム画面のキー処理は `UILaunchPage::handle_home_key()` にあります。LVGL C コールバックの入口は `projects/APPLaunch/main/ui/ui_launch_page.cpp` の `UILaunchPage::on_home_key()` です。 + +まず、CardputerZero でよく使う `F/X/Z/C` キーを矢印キーへ対応付けます。 + +```cpp +static uint32_t fzxc_to_arrow(uint32_t key) +{ + switch (key) { + case KEY_F: return KEY_UP; + case KEY_X: return KEY_DOWN; + case KEY_Z: return KEY_LEFT; + case KEY_C: return KEY_RIGHT; + default: return key; + } +} +``` + +ホーム画面の挙動: + +| Input | Trigger timing | 挙動 | +| --- | --- | --- | +| `KEY_LEFT` or `Z` | pressed/repeat | `switch.wav` を再生し、`switch_right()` を呼び、右方向に次の項目へ回転 | +| `KEY_RIGHT` or `C` | pressed/repeat | `switch.wav` を再生し、`switch_left()` を呼び、左方向に次の項目へ回転 | +| `KEY_ENTER` | released | `enter.wav` を再生し、現在のアプリを起動 | +| `KEY_F12` | released | 緑色の全画面デバッグオーバーレイを切り替え、`lvping_lock` を設定 | +| `KEY_UP` / `KEY_DOWN` or `F` / `X` | pressed/repeat | ホーム画面では現在アクション未定義 | + +注意: `handle_home_key()` は left/right キーを press 時に処理するため、長押しすると repeat イベントで連続切り替えが発生します。ENTER はキーを押し続けたまま起動が繰り返されないよう release 時に起動します。ログタグには古いデバッグ出力との互換性のため、まだ `main_key_switch` が含まれています。 + +## 7. 組み込みページのキー割り当て概要 + +各ページはそれぞれの `root_screen_` に `LV_EVENT_KEYBOARD` をバインドします。一般的な慣例は次のとおりです。 + +| Page | File | Main keys | +| --- | --- | --- | +| `UIConsolePage` | `ui_app_console.hpp` | ESC/arrow/Enter/Backspace は PTY 制御シーケンスへ変換。HOME 関連状態は終了/外部ロックに使用 | +| `UIGamePage` | `ui_app_game.hpp` | 矢印キーで移動、ENTER で開始/再開始、ESC で戻る | +| `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN または F/X で選択、ENTER/RIGHT または C で入る/確定、ESC/LEFT または Z で戻る。一部ページは R/D 対応 | +| `UIGamePage` | `ui_app_game.hpp` | 共通ページキー処理を使用。ESC で戻る | +| `UIIpPanelPage` | `ui_app_ip_panel.hpp` | F/X/Z/C を LV_KEY_* へ変換。UP/DOWN で選択、ESC で戻る | +| `UIFilePage` | `ui_app_file.hpp` | UP/DOWN で選択、RIGHT/ENTER で入る、LEFT で親へ、ESC でホームまたは親へ戻る | +| `UISSHPage` | `ui_app_ssh.hpp` | UP/DOWN で Host/Port/User 切替、文字入力、BACKSPACE で削除、ENTER で接続、ESC で戻る | +| `UIMeshPage` | `ui_app_mesh.hpp` | S で入力を開く、R で更新、UP/DOWN で閲覧、ENTER で送信、BACKSPACE で削除、ESC でキャンセル/戻る | +| `UICameraPage` | `ui_app_camera.hpp` | ESC で戻る/ページ終了、ENTER で撮影/確定、UP/DOWN/LEFT/RIGHT で移動、1-5 はショートカットボタン | +| `UIRecPage` | `ui_app_rec.hpp` | 録音/一覧状態に応じてナビゲーション、確定、戻るを処理 | +| `UICompassPage` | `ui_app_compass.hpp` | F4/F6 でキャリブレーションまたは切替、ESC で戻る | +| `UILoraPage` | `ui_app_lora.hpp` | KEY_UP/DOWN/LEFT/RIGHT/ENTER/ESC/BACKSPACE/DELETE を LV_KEY_* へ変換し、業務ロジックへ渡す | +| `UITankBattlePage` | `ui_app_tank_battle.hpp` | `33(F)` 上、`45(X)` 下、`44(Z)` 左、`46(C)` 右、`57(SPACE)` 発射、ESC で戻る | + +## 8. F/X/Z/C 方向キーの慣例 + +CardputerZero キーボードでは、`F/X/Z/C` が矢印キーの代替としてよく使われます。コードベースには 3 つのパターンがあります。 + +1. ホーム画面 `ui_launch_page.cpp`: `fzxc_to_arrow()` が `F/X/Z/C` を `KEY_UP/DOWN/LEFT/RIGHT` へ変換します。 +2. `UIGamePage` や `UIIpPanelPage` のように、ページ内で LVGL キーへ変換します。 + +```cpp +switch (key) { +case KEY_F: return LV_KEY_UP; +case KEY_X: return LV_KEY_DOWN; +case KEY_Z: return LV_KEY_LEFT; +case KEY_C: return LV_KEY_RIGHT; +} +``` + +3. ゲームでは evdev 番号を直接使うことがあります。`UITankBattlePage` では `KEY_MOVE_UP = 33`、`KEY_MOVE_DOWN = 45`、`KEY_MOVE_LEFT = 44`、`KEY_MOVE_RIGHT = 46` です。 + +新しいページでは `KEY_F` のようなシンボル名を優先し、生の数値は避けてください。過去のヒントとの互換性のために数値を残す場合は、対応するキー名をコメントで明記してください。 + +## 9. テキスト入力 + +SSH、Mesh、WiFi パスワード、ターミナルなど、一部ページでは文字入力が必要です。 + +### 9.1 単純な ASCII マッピング + +`UISSHPage` と `UIMeshPage` は `keycode_to_char()` を使い、`KEY_1`、`KEY_Q` などを小文字へ変換します。 + +```cpp +static char keycode_to_char(uint32_t key) +{ + if (key >= KEY_1 && key <= KEY_9) return '1' + (key - KEY_1); + if (key == KEY_0) return '0'; + if (key >= KEY_Q && key <= KEY_P) return qwerty[key - KEY_Q]; + if (key == KEY_SPACE) return ' '; + if (key == 52) return '.'; // KEY_DOT + if (key == 12) return '-'; // KEY_MINUS + return 0; +} +``` + +この方法は単純ですが、Shift による大文字、入力メソッド、マルチバイト文字には対応しません。完全なテキスト入力能力が必要な場合は、`key_item::utf8` または `codepoint` を読んでください。 + +### 9.2 ターミナル入力 + +`UIConsolePage` は `struct key_item` を直接読み、物理キーと UTF-8 テキストを PTY バイトストリームへ変換します。 + +- `KEY_ENTER` -> `\r` +- `KEY_BACKSPACE` -> `0x7f` +- `KEY_ESC` -> `0x1b` +- 矢印キー -> application cursor mode に応じて `\033[A/B/C/D` または `\033OA/OB/OC/OD` +- 通常文字 -> `key_item::utf8` + +ターミナルページは子プロセス終了、画面更新、カーソル点滅、ESC/Home の戻りセマンティクスも扱うため、通常のページより複雑です。 + +## 10. 外部アプリ実行中の入力処理 + +外部アプリは `Launch::launch_Exec()` から起動されます。 + +```cpp +LVGL_RUN_FLAGE = 0; +lv_indev_set_group(indev, NULL); +lv_timer_enable(false); + +int ret = cp0_process_exec_blocking(exec.c_str(), &LVGL_HOME_KEY_FLAG, keep_root ? 1 : 0); + +lv_timer_enable(true); +launch_page_->show_home_screen(); +LVGL_RUN_FLAGE = 1; +``` + +意味: + +- 外部プロセス実行中、APPLaunch は LVGL タイマーを停止し、通常のキュー経由キーボードイベントを受け取らなくなります。 +- ESC 状態は `LVGL_HOME_KEY_FLAG` に更新され続け、`APPLaunch_lock()` や外部プロセス復帰ロジックで使われます。 +- 外部プロセス終了後、ホーム画面、入力グループ、LVGL タイマーが復元されます。 + +`main.cpp::APPLaunch_lock()` はロックファイル保持者も確認します。外部アプリがロックを保持しており、ESC が約 5 秒押され続けると、`cp0_process_kill(holder_pid, 3000)` を呼んで外部アプリの終了を試みます。 + +## 11. 入力グループの切り替え + +ホーム画面とページはそれぞれ独自の LVGL group を持っています。 + +- ホーム画面: `UILaunchPage::home_input_group()`。 +- 組み込みページ: `AppPageRoot::input_group()`。 +- ネストしたターミナル: `UIConsolePage::input_group()`。 + +ページを切り替えるときは、同時に入力グループも切り替える必要があります。 + +```cpp +lv_disp_load_scr(p->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); +``` + +ホーム画面へ戻る場合: + +```cpp +launch_page_->show_home_screen(); +``` + +`show_home_screen()` はホーム画面を読み込み、`UILaunchPage::bind_home_input_group()` を呼びます。 + +画面は切り替わったのに group が古いページを指していると、次のような問題が起こります。 + +- 表示中のページがキーに反応しない。 +- 非表示のページがキーに反応する。 +- ネストしたページから出た後の ESC 挙動が異常になる。 + +## 12. 入力問題のデバッグ + +デバイスキーボード層には既にログがあります。 + +```text +[KBD] enqueue code=... state=... sym=... utf8=... cp=... mods=... run=... home_flag=... +[INDEV] dequeue code=... state=... sym=... utf8=... cp=... active_screen=... +[LAUNCHER] main_key_switch raw=...->code=... state=... sym=... +``` + +推奨する調査順序: + +1. `keyboard_read_thread()` が起動したか、デバイスパスが正しいかを確認します。 +2. `[KBD] enqueue` が出るか確認します。出なければ libinput/device/xkb 層の問題です。 +3. `[INDEV] dequeue` が出るか確認します。出なければキューが LVGL indev に消費されていない可能性があります。 +4. `active_screen` が現在のページ画面か確認します。 +5. ページが `root_screen_` に `LV_EVENT_KEYBOARD` をバインドしているか確認します。 +6. ページが press、release、repeat のどれを処理しているか、トリガータイミングが不整合でないか確認します。 +7. `LVGL_RUN_FLAGE` が 0 か確認します。外部アプリ実行中は通常イベントが破棄されます。 + +## 13. 新規ページのキー処理に関する推奨事項 + +新しいページでは次のルールに従ってください。 + +- リスト/メニューページ: `IS_KEY_RELEASED(e)` で `KEY_UP/DOWN/LEFT/RIGHT/ENTER/ESC` を処理します。 +- ゲームページ: 連続動作は `IS_KEY_PRESSED(e)` で処理し、必要なら repeat も受け付けます。 +- テキスト入力ページ: `key_item::utf8` を優先します。`keycode_to_char()` は単純な場合だけ使います。 +- 戻るキー: ESC は現在のページまたは現在のポップアップを閉じる必要があります。多階層ビューではまず前の階層に戻り、その後ホームへ戻ります。 +- 方向代替キー: デバイスキーボード対応ページでは `F/X/Z/C` を一貫してサポートしてください。 +- `struct key_item *` ポインタを保存しないでください。非同期処理が必要なら `key_code`、`utf8` などのフィールドをコピーします。 +- 長押し可能なキーでは、確認や起動が繰り返されないよう `KBD_KEY_PRESSED`、`KBD_KEY_REPEATED`、`KBD_KEY_RELEASED` を明示的に区別してください。 diff --git a/docs/launcher-project-guide-ja/08-build-and-compilation-guide.md b/docs/launcher-project-guide-ja/08-build-and-compilation-guide.md new file mode 100644 index 00000000..4857a015 --- /dev/null +++ b/docs/launcher-project-guide-ja/08-build-and-compilation-guide.md @@ -0,0 +1,1003 @@ +# 08 - ビルドとコンパイルガイド + +この章では、`projects/APPLaunch` の完全なビルド手順を説明します。Linux SDL2 ネイティブシミュレーション、デバイス上のネイティブビルド、Linux x86 クロスコンパイル、macOS クロスコンパイル、Windows SDL2/クロスビルド、依存関係のインストール、環境変数、主要な SCons ロジック、よくあるエラーへの対処を扱います。 + +特に記載がない限り、すべてのコマンドはリポジトリルートから開始するものとします。 + +```bash +cd /home/nihao/w2T/github/launcher +``` + +## 1. ビルドターゲット概要 + +APPLaunch は複数の形式でビルドできます。中核的な違いは、`CONFIG_DEFAULT_FILE` が指す設定ファイルで決まります。 + +| Build target | 実行場所 | Configuration file | 表示/入力バックエンド | 典型的な用途 | +| --- | --- | --- | --- | --- | +| Linux SDL2 native simulation | Linux x86_64 開発マシン | `linux_x86_sdl2_config_defaults.mk` | SDL2 window + SDL input | 日常的な UI デバッグと高速開発 | +| Native device build | M5CardputerZero AArch64 Linux | `config_defaults.mk` | Linux framebuffer + evdev | デバイス上で直接ビルドして実行 | +| Linux x86 cross-compilation | Linux x86_64 開発マシン、出力はデバイスで実行 | `linux_x86_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | 公式デバイス成果物の推奨ビルド方法 | +| macOS cross-compilation | macOS 開発マシン、出力はデバイスで実行 | `mac_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | macOS で arm64 デバイス成果物を生成 | +| macOS SDL/Darwin configuration | macOS 開発マシン | `darwin_config_defaults.mk` | SDL-related configuration | ネイティブ SDL 作業のベース設定 | +| Windows SDL2 native simulation | Windows x86_64 開発マシン | `win_x86_sdl2_config_defaults.mk` | SDL2 window + SDL input | Windows 上の UI デバッグ | +| Windows x86 cross-compilation | Windows x86_64 開発マシン、出力はデバイスで実行 | `win_x86_cross_config_defaults.mk` | Linux framebuffer + evdev | Windows で arm64 デバイス成果物を生成 | + +ビルド成果物は通常次の場所に生成されます。 + +```text +projects/APPLaunch/dist/ +├── M5CardputerZero-APPLaunch +└── APPLaunch/ + └── bin/ + └── store_cache_sync.py +``` + +各項目の意味: + +- `M5CardputerZero-APPLaunch` はメイン実行ファイルです。 +- `APPLaunch/` は実行時リソースツリーで、`dist/APPLaunch` にコピーされます。 +- `store_cache_sync.py` は `projects/APPLaunch/APPLaunch/bin/store_cache_sync.py` にあり、実行時リソースツリーの一部としてコピーされます。 + +## 2. 前提条件 + +### 2.1 サブモジュールとディレクトリ構成 + +初回 clone では次を使います。 + +```bash +git clone --recursive https://github.com/CardputerZero/launcher.git +cd launcher +``` + +リポジトリは clone 済みだがサブモジュールが未初期化の場合: + +```bash +git submodule update --init --recursive +``` + +APPLaunch のトップレベル `SConstruct` は次のディレクトリ関係を前提にしています。 + +```text +launcher/ +├── SDK/ +├── ext_components/ +└── projects/ + └── APPLaunch/ + ├── SConstruct + └── main/SConstruct +``` + +ビルド前に APPLaunch プロジェクトディレクトリへ移動してください。 + +```bash +cd projects/APPLaunch +``` + +リポジトリルートから APPLaunch の `scons` を直接実行しないでください。`PROJECT_PATH`、`SDK_PATH`、`EXT_COMPONENTS_PATH` は現在のプロジェクトディレクトリから導出されます。 + +### 2.2 Python 依存関係 + +SCons と Kconfig ツールには Python 3.8 以降が必要です。 + +```bash +python3 --version +``` + +一般的な Python パッケージ: + +```bash +python3 -m pip install --user parse scons requests tqdm +python3 -m pip install --user setuptools-rust paramiko scp +``` + +パッケージの用途: + +| Package | Purpose | +| --- | --- | +| `scons` | メインのビルド入口 | +| `parse` | SCons スクリプトと SDK ビルドツールが設定/コマンド出力を解析するために使用 | +| `requests`, `tqdm` | SDK ツールが依存ソースコードや sysroot パッケージをダウンロードするときに使用 | +| `paramiko`, `scp` | `scons push` が SSH 経由で `dist` をアップロードするときに使用 | +| `setuptools-rust` | 一部 Python 依存関係のビルド時に必要になる場合がある | + +仮想環境を使う場合: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +## 3. Linux 開発マシンへの依存関係インストール + +### 3.1 基本依存関係 + +Debian/Ubuntu の例: + +```bash +sudo apt update +sudo apt install -y \ + python3 python3-pip python3-venv \ + build-essential pkg-config git \ + libffi-dev +``` + +### 3.2 SDL2 シミュレーション依存関係 + +Linux SDL2 ビルドは `main/SConstruct` で次を呼びます。 + +```python +pkg_config_cflags("freetype2") +pkg_config_cflags("sdl2") +pkg_config_ldflags("sdl2") +``` + +そのため、ホストには SDL2、FreeType、入力関連ライブラリが必要です。 + +```bash +sudo apt install -y \ + libsdl2-dev libfreetype6-dev \ + libinput-dev libxkbcommon-dev libudev-dev +``` + +まず `pkg-config` がライブラリを見つけられるか確認することを推奨します。 + +```bash +pkg-config --cflags sdl2 +pkg-config --libs sdl2 +pkg-config --cflags freetype2 +pkg-config --libs freetype2 +``` + +### 3.3 Linux x86 クロスコンパイル依存関係 + +Linux x86_64 から M5CardputerZero AArch64 へクロスコンパイルするには、GNU AArch64 クロスツールチェーンが必要です。 + +```bash +sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +``` + +確認: + +```bash +aarch64-linux-gnu-gcc --version +aarch64-linux-gnu-g++ --version +``` + +クロスコンパイルにはデバイス側ヘッダーとライブラリも必要です。APPLaunch のトップレベル `SConstruct` は、クロスコンパイル時に SDK 静的 sysroot を自動準備します。 + +```text +SDK/github_source/static_lib_v0.0.4 +``` + +このディレクトリが存在しない、または `version` ファイルが `v0.0.4` と一致しない場合、ビルドスクリプトは次のリリースパッケージをダウンロードします。 + +```text +https://github.com/CardputerZero/M5CardputerZero-UserDemo/releases/download/v0.0.4/sdk_bsp.tar.gz +``` + +そのため、初回クロスコンパイルにはネットワークアクセスが必要です。オフライン環境では、事前に `SDK/github_source/static_lib_v0.0.4` を用意してください。 + +## 4. macOS への依存関係インストール + +### 4.1 Python 環境 + +仮想環境を推奨します。 + +```bash +python3 -m venv launcher-python-venv +source launcher-python-venv/bin/activate +pip3 install parse scons requests tqdm setuptools-rust paramiko scp +``` + +### 4.2 macOS クロスツールチェーン + +`mac_cross_cp0_config_defaults.mk` は次を指定します。 + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-unknown-linux-gnu-" +``` + +インストール: + +```bash +brew tap messense/macos-cross-toolchains +brew install aarch64-unknown-linux-gnu +``` + +確認: + +```bash +aarch64-unknown-linux-gnu-gcc --version +aarch64-unknown-linux-gnu-g++ --version +``` + +### 4.3 macOS SDL/Darwin 依存関係 + +ネイティブ SDL デバッグに `darwin_config_defaults.mk` を使う場合は、SDL2 と FreeType を用意します。一般的なインストール方法: + +```bash +brew install sdl2 freetype pkg-config +``` + +確認: + +```bash +pkg-config --cflags sdl2 +pkg-config --cflags freetype2 +``` + +## 5. 主要な環境変数 + +### 5.1 `CONFIG_DEFAULT_FILE` + +`CONFIG_DEFAULT_FILE` は最も重要なビルド選択変数です。 + +例: + +```bash +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +``` + +SCons はこれを Kconfig に渡し、次を生成します。 + +```text +build/config/global_config.mk +build/config/global_config.h +``` + +未設定の場合、`projects/APPLaunch/SConstruct` には自動ロジックがあります。 + +- `platform.machine()` が `x86_64` のとき、既定で `linux_x86_sdl2_config_defaults.mk` になります。 +- 環境変数 `CardputerZero=y` がある場合、`linux_x86_cross_cp0_config_defaults.mk` を強制します。 +- デバイス上のネイティブビルドでは、既定ロジックによる誤検出を避けるため、通常 `CONFIG_DEFAULT_FILE=config_defaults.mk` を明示指定する必要があります。 + +### 5.2 `CardputerZero` + +クロスコンパイル設定を選択するショートカットです。 + +```bash +export CardputerZero=y +``` + +これはトップレベル `SConstruct` が次を設定するのと同等です。 + +```text +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +``` + +自動化スクリプトでは、ビルドターゲットのトラブルシュートがしやすいため、引き続き `CONFIG_DEFAULT_FILE` を明示的に書くことを推奨します。 + +### 5.3 `SDK_PATH` と `EXT_COMPONENTS_PATH` + +APPLaunch のトップレベル `SConstruct` は自動的に次を設定します。 + +```python +os.environ["SDK_PATH"] = str(sdk_path) +os.environ["EXT_COMPONENTS_PATH"] = str(sdk_path.parent / "ext_components") +``` + +意味: + +| Variable | Default value | Purpose | +| --- | --- | --- | +| `SDK_PATH` | リポジトリルート配下の `SDK` | SDK ビルドシステムが Kconfig、SCons ツール、組み込みコンポーネントを見つけるため | +| `EXT_COMPONENTS_PATH` | リポジトリルート配下の `ext_components` | `cp0_lvgl`、`Miniaudio`、`Sigslot`、`RadioLib` などの拡張コンポーネントを読み込むため | + +実際に外部 SDK やコンポーネントディレクトリをテストする場合を除き、通常これらの変数を手動で上書きしないでください。 + +### 5.4 `CONFIG_TOOLCHAIN_SYSROOT` + +クロスコンパイル時、トップレベル `SConstruct` は一時設定を自動的に書き込みます。 + +```text +build/config/config_tmp.mk +``` + +内容は次のようになります。 + +```make +CONFIG_TOOLCHAIN_SYSROOT="/path/to/launcher/SDK/github_source/static_lib_v0.0.4" +CONFIG_TOOLCHAIN_FLAGS="-I/path/to/launcher/SDK/github_source/static_lib_v0.0.4/usr/include/aarch64-linux-gnu" +``` + +これを読み込んだ後、SDK ビルドシステムは次を追加します。 + +```text +--sysroot=$CONFIG_TOOLCHAIN_SYSROOT +-I$CONFIG_TOOLCHAIN_SYSROOT/usr/include +-I$CONFIG_TOOLCHAIN_SYSROOT/usr/include/ +-L$CONFIG_TOOLCHAIN_SYSROOT/lib/ +-L$CONFIG_TOOLCHAIN_SYSROOT/usr/lib/ +``` + +`main/SConstruct` もこれを使って FreeType、libpng、libcamera の include/link パスを追加します。 + +### 5.5 `APPLAUNCH_STARTUP_ANIMATION` + +起動アニメーションはオプションのコンパイル時マクロです。 + +```bash +export APPLAUNCH_STARTUP_ANIMATION=1 +``` + +この変数が `1` の場合、`main/SConstruct` は次を追加します。 + +```text +-DAPPLAUNCH_STARTUP_ANIMATION +``` + +未設定の場合、起動アニメーションコードは有効になりません。 + +### 5.6 ビルド出力のデバッグ + +`CONFIG_COMMPILE_DEBUG` が未設定の場合、SDK ビルドシステムは `CXX ...` や `Linking ...` のような簡潔な出力を使います。完全なコンパイラコマンドを見るには次を試してください。 + +```bash +export CONFIG_COMMPILE_DEBUG=y +scons -j8 +``` + +## 6. Linux SDL2 ネイティブビルドと実行 + +これは UI 開発で最も一般的なモードです。成果物は Linux x86_64 開発マシン上の SDL2 ウィンドウで実行されます。 + +### 6.1 古い設定のクリーン + +ビルドターゲットを切り替える前にクリーンしてください。特にクロスコンパイルから SDL2 へ戻す場合、古い `build/config/global_config.mk` が前のターゲット設定を保持するため重要です。 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +``` + +### 6.2 ビルド + +```bash +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons -j8 +``` + +現在のマシンが `x86_64` の場合、トップレベル `SConstruct` は SDL2 設定を既定にするため、`CONFIG_DEFAULT_FILE` を省略することもできます。ドキュメントやスクリプトでは明示指定を推奨します。 + +### 6.3 実行 + +```bash +cd dist +./M5CardputerZero-APPLaunch +``` + +SDL2 設定は次を有効にします。 + +```make +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_V9_5_LV_OS_PTHREAD=y +``` + +そのため、`dist` ディレクトリから実行すると、LVGL の POSIX ファイルシステムルートはカレントディレクトリになり、リソースパスは `./APPLaunch/...` 経由で解決できます。`projects/APPLaunch` から `dist/M5CardputerZero-APPLaunch` を直接実行すると、リソース相対パスが異なる場合があるため、先に `dist` へ入ることを推奨します。 + +### 6.4 SDL2 ビルドでリンクされるライブラリ + +設定ファイルが `linux_x86_sdl2_config_defaults.mk` を含む場合、`main/SConstruct` はさらに次を行います。 + +- FreeType のコンパイル/リンクパラメータを LVGL コンポーネントへ追加。 +- SDL2 のコンパイル/リンクパラメータを APPLaunch へ追加。 +- `input`、`xkbcommon`、`udev` をリンク。 +- プロジェクト独自のキーボード入力経路との衝突を避けるため、LVGL コンポーネントから `lv_sdl_keyboard.c` を除外。 + +## 7. デバイス上のネイティブビルド + +ネイティブデバイスビルドとは、M5CardputerZero AArch64 Linux システム上で APPLaunch を直接ビルドすることです。利点はツールチェーンと実行時ライブラリが自然にデバイスと一致することです。欠点はデバイスの性能とストレージが限られているため、ビルドが遅いことです。 + +### 7.1 デバイスへの依存関係インストール + +デバイス上で実行: + +```bash +sudo apt update +sudo apt install -y \ + python3 python3-pip python3-venv \ + build-essential pkg-config git \ + libffi-dev libfreetype6-dev \ + libinput-dev libxkbcommon-dev libudev-dev \ + libcamera-dev libjpeg-dev +python3 -m pip install --user parse scons requests tqdm setuptools-rust paramiko scp +``` + +パッケージ名はデバイスイメージによって少し異なる場合があります。`libcamera-dev` が存在しない場合は、まずイメージのパッケージソースが有効か確認するか、システムに既に提供されている libcamera ヘッダーとライブラリを使ってください。 + +### 7.2 ビルド + +```bash +cd /home/pi/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=config_defaults.mk +scons -j2 +``` + +デバイス上では、メモリ不足を避けるため `-j2` または `-j4` を推奨します。`config_defaults.mk` は次を有効にします。 + +```make +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +### 7.3 実行 + +デバイス設定のリソースルートパスは `/usr/share/APPLaunch/` なので、`dist` から直接実行すると正式な配置パス配下のリソースを見つけられない場合があります。一時テストには次のいずれかを選びます。 + +1. リソースを正式な場所へコピーする: + +```bash +sudo mkdir -p /usr/share/APPLaunch/bin +sudo cp -a dist/APPLaunch/. /usr/share/APPLaunch/ +sudo install -m 0755 dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +2. ホスト側のリソースパスデバッグには SDL2 設定を使います。正式なデバイス実行には使わないでください。 + +正式なデバイス配備には、第 09 章で説明する `.deb` パッケージングと systemd サービスを使用してください。 + +## 8. Linux x86 からデバイスへのクロスコンパイル + +これは推奨される正式なビルド方法です。Linux x86_64 開発マシンで arm64 成果物を生成し、その後パッケージ化またはデバイスへアップロードします。 + +### 8.1 クリーンと設定選択 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +次も使用できます。 + +```bash +export CardputerZero=y +scons -j8 +``` + +ただし `CONFIG_DEFAULT_FILE` を明示的に設定することを推奨します。 + +### 8.2 クロスコンパイル設定の詳細 + +`linux_x86_cross_cp0_config_defaults.mk` の主要エントリ: + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_LINUX_FBDEV_RENDER_MODE_FULL=y +CONFIG_V9_5_LV_DRAW_SW_ASM_NEON=y +CONFIG_V9_5_LV_USE_DRAW_SW_ASM=1 +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +意味: + +- `aarch64-linux-gnu-gcc/g++` を使用します。 +- デバイス framebuffer を使用し、SDL2 ウィンドウは作成しません。 +- evdev でキーボード/入力イベントを読み取ります。 +- リソースパスを `/usr/share/APPLaunch/` に固定します。 +- NEON アセンブリ最適化を有効にします。 +- デバイスの全画面更新戦略に適した full render mode を使います。 + +### 8.3 自動 sysroot ロジック + +トップレベル `SConstruct` は `CONFIG_DEFAULT_FILE` に `cross` が含まれると `cross_package_enabled` を有効にします。 + +```python +if "cross" in os.environ.get("CONFIG_DEFAULT_FILE", ''): + cross_package_enabled = True +``` + +その後、`build/config/config_tmp.mk` に次を生成します。 + +```text +CONFIG_TOOLCHAIN_SYSROOT="SDK/github_source/static_lib_v0.0.4" +CONFIG_TOOLCHAIN_FLAGS="-I.../usr/include/aarch64-linux-gnu" +``` + +`SDK/github_source/static_lib_v0.0.4` が存在しない、またはバージョンが一致しない場合、`sdk_bsp.tar.gz` をダウンロードします。この sysroot はクロスコンパイル用に次を提供します。 + +- デバイス側システムライブラリ。 +- FreeType、libpng、libcamera、libjpeg などのヘッダーとライブラリ。 +- クロスリンクに必要な `libstdc++.so.6` などの実行時ライブラリ参照。 + +### 8.4 成果物アーキテクチャの確認 + +ビルド完了後: + +```bash +file dist/M5CardputerZero-APPLaunch +``` + +期待される出力には次のような文字列が含まれます。 + +```text +ELF 64-bit LSB executable, ARM aarch64 +``` + +動的依存関係名を確認: + +```bash +aarch64-linux-gnu-readelf -d dist/M5CardputerZero-APPLaunch | grep NEEDED +``` + +開発マシン上でシンボルやセグメント情報を確認する場合: + +```bash +aarch64-linux-gnu-readelf -h dist/M5CardputerZero-APPLaunch +aarch64-linux-gnu-objdump -p dist/M5CardputerZero-APPLaunch | grep NEEDED +``` + +## 9. macOS からデバイスへのクロスコンパイル + +### 9.1 ビルドコマンド + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk +scons -j8 +``` + +`mac_cross_cp0_config_defaults.mk` は次を使います。 + +```make +CONFIG_TOOLCHAIN_PREFIX="aarch64-unknown-linux-gnu-" +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +``` + +### 9.2 macOS 追加リンクパス + +`main/SConstruct` は `mac_cross_cp0_config_defaults.mk` に対して追加処理を行います。 + +- FreeType と libpng の include を追加: `$CONFIG_TOOLCHAIN_SYSROOT/usr/include/freetype2` と `libpng16`。 +- libcamera include を追加。`pkg-config --cflags libcamera` を優先し、失敗時は `$CONFIG_TOOLCHAIN_SYSROOT/usr/include/libcamera` へフォールバック。 +- `$CONFIG_TOOLCHAIN_SYSROOT/usr/lib/aarch64-linux-gnu/libstdc++.so.6` をリンク。 +- macOS クロスリンカーが sysroot 内の Linux ライブラリを見つけやすいよう、`-Wl,-rpath-link,...` と `-B...` を追加。 + +### 9.3 macOS の一般的な注意 + +- Homebrew は Apple Silicon では通常 `/opt/homebrew`、Intel Mac では `/usr/local` 配下です。ツールチェーンが `PATH` にない場合は手動で追加してください。 +- `pkg-config` が `libcamera` を見つけられない場合、スクリプトはフォールバックしますが、sysroot には実際のヘッダーとライブラリが含まれている必要があります。 +- 生成ファイルは Linux arm64 ELF であり、macOS では直接実行できません。 + +確認: + +```bash +file dist/M5CardputerZero-APPLaunch +``` + +期待される結果は `ARM aarch64` Linux ELF であり、Mach-O ではありません。 + +## 10. Windows ビルド + +Windows ビルドは `projects/APPLaunch` 配下の同じ SCons 入口を使いますが、設定で `CONFIG_TOOLCHAIN_SYSTEM_WIN=y` と `CONFIG_TOOLCHAIN_GCCSUFFIX=".exe"` を指定し、SDK ビルドシステムが Windows ツールチェーン実行ファイルを呼ぶようにします。 + +### 10.1 Windows SDL2 ネイティブビルドと実行 + +MSYS2 MinGW シェルを使用し、`gcc`、`g++`、`pkg-config`、SDL2、FreeType がすべて `PATH` から利用できるようにします。 + +MSYS2 UCRT64 の例: + +```bash +pacman -S --needed \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-pkgconf \ + mingw-w64-ucrt-x86_64-SDL2 \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-python-pip + +python -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +ビルドと実行: + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch.exe +``` + +`win_x86_sdl2_config_defaults.mk` の主要エントリ: + +```make +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_TOOLCHAIN_SYSTEM_WIN=y +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_APPLAUNCH_WIN_X86_SDL2=y +``` + +SDL2 出力は `dist/M5CardputerZero-APPLaunch.exe` です。 + +### 10.2 Windows からデバイスへのクロスコンパイル + +`https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe` から SysGCC Raspberry64 Windows AArch64 Linux クロスツールチェーンをインストールします。デフォルト設定は次を想定します。 + +```make +CONFIG_TOOLCHAIN_PATH="D:\\app\\SysGCC\\bin" +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_GCC_DUMPMACHINE="aarch64-linux-gnu" +``` + +ツールチェーンを別の場所へインストールした場合は、ビルド前に `projects/APPLaunch/win_x86_cross_config_defaults.mk` の `CONFIG_TOOLCHAIN_PATH` を更新してください。 + +ビルド: + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_cross_config_defaults.mk +scons -j8 +``` + +`win_x86_cross_config_defaults.mk` の主要エントリ: + +```make +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +CONFIG_APPLAUNCH_WIN_X86_CROSS_CP0=y +``` + +クロスビルド出力は、ターゲットが Linux AArch64 のため `.exe` サフィックスなしの `dist/M5CardputerZero-APPLaunch` です。初回クロスビルドでは、SDK sysroot パッケージが `SDK/github_source/static_lib_v0.0.4` へダウンロードされる場合があります。 + +## 11. 主要な SCons ロジック + +### 11.1 トップレベル `projects/APPLaunch/SConstruct` + +このファイルはビルド入口とグローバル環境準備を担当します。 + +1. SDK パスを定義します。 + +```text +sdk_path = projects/APPLaunch/../../SDK +``` + +2. 環境変数に基づいて既定設定を選択します。 + +```text +CardputerZero=y -> linux_x86_cross_cp0_config_defaults.mk +x86_64 and CONFIG_DEFAULT_FILE unset -> linux_x86_sdl2_config_defaults.mk +``` + +3. クロスコンパイル時に `build/config/config_tmp.mk` を生成し、sysroot を追加します。 + +4. 次を設定します。 + +```text +SDK_PATH +EXT_COMPONENTS_PATH +``` + +5. SDK ビルドシステムを呼び出します。 + +```python +SConscript(str(sdk_path / "tools" / "scons" / "project.py"), variant_dir=os.getcwd(), duplicate=0) +``` + +6. クロスコンパイル時に `static_lib_v0.0.4` を確認し、必要ならダウンロードします。 + +### 11.2 SDK `project.py` + +SDK ビルドシステムは次を行います。 + +1. 特殊コマンド `menuconfig`、`clean`、`distclean`、`save`、`SET_CROSS`、`push` を処理します。 +2. Kconfig ツールを呼び、`global_config.mk` と `global_config.h` を生成します。 +3. `global_config.mk` から `CONFIG_...` 変数を環境変数へ読み込みます。 +4. SCons ビルド環境とツールチェーンプレフィックスを作成します。 +5. SDK コンポーネントディレクトリと `ext_components` ディレクトリをスキャンします。 +6. `projects/APPLaunch/main/SConstruct` を読み込み、メインプロジェクトコンポーネントを登録します。 +7. 静的ライブラリ、共有ライブラリ、実行ファイルをビルドします。 +8. 実行ファイルと `STATIC_FILES` を `dist` へコピーします。 + +### 11.3 `projects/APPLaunch/main/SConstruct` + +このファイルは APPLaunch メインプログラムコンポーネントを登録します。 + +- `ui/generate_page_app_includes.py` を実行し、組み込みページ include 集約ファイルを生成します。 +- 現在の短い git hash を読み取り、コンパイルマクロ `LAUNCHER_GIT_COMMIT_RAW` として注入します。 +- `src/*.c*` と `ui` ディレクトリ配下のすべてのソースファイルを収集します。 +- include として `main`、`main/include`、`ext_components/cp0_lvgl/include`、`SDK/components/utilities/include` を追加します。 +- 依存コンポーネント: `cp0_lvgl`、`eventpp`、`lvgl_component`、`pthread`、`Miniaudio`、`RadioLib`。 +- オプション依存: `Backward_cpp`。 +- 設定ファイルに応じて SDL2、FreeType、libinput、xkbcommon、udev、libcamera、jpeg などの依存を追加します。Windows SDL2 も Linux SDL2 と同じ SDL2/FreeType `pkg-config` フラグ処理を共有します。 +- `ext_components/RadioLib` を静的コンポーネントとして使います。RadioLib コンポーネントは `wget_github('https://github.com/jgromes/RadioLib.git')` のソースキャッシュと SX1262 関連ソースリストを所有します。 +- `../APPLaunch` 実行時リソースツリーを `STATIC_FILES` に追加します。このツリーには `bin/store_cache_sync.py` が含まれます。 +- プロジェクトターゲット `M5CardputerZero-APPLaunch` を登録します。 + +## 12. よく使う SCons コマンド + +| Command | Purpose | +| --- | --- | +| `scons -j8` | 8 並列ジョブでビルド | +| `scons -c` | SCons が把握するターゲットをクリーン | +| `scons distclean` | `build`、`dist`、`.sconsign.dblite`、`.config*` など設定/成果物ファイルを削除 | +| `scons menuconfig` | Kconfig メニューを開き、設定を再生成 | +| `scons save` | 現在の `build/config/global_config.mk` を `CONFIG_DEFAULT_FILE` が指すファイルへ保存 | +| `scons push` | `setup.ini` に従って `dist` を SSH 経由でアップロード | + +ターゲット切替時の推奨フロー: + +```bash +scons distclean +export CONFIG_DEFAULT_FILE=target-configuration-file +scons -j8 +``` + +`CONFIG_DEFAULT_FILE` だけを変更してすぐ `scons -j8` を実行しないでください。古い `build/config/global_config.mk` が既に存在すると、SDK ビルドシステムは設定を自動再生成しない場合があります。 + +## 13. `menuconfig` の推奨事項 + +実行: + +```bash +cd projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons menuconfig +``` + +`menuconfig` は `CONFIG_DEFAULT_FILE` と一時設定に基づいて最終設定を生成します。変更後、出力先は次です。 + +```text +build/config/global_config.mk +build/config/global_config.h +``` + +変更を永続化したいことが確実な場合: + +```bash +scons save +``` + +注意: `scons save` は設定ファイルへ書き戻します。複数人で作業している場合、このタスクが明示的に要求していない限り、共有の `*_config_defaults.mk` ファイルへ気軽に保存しないでください。 + +## 14. よくあるエラーと修正 + +### 14.1 `scons: command not found` + +原因: SCons がインストールされていない、または Python user bin ディレクトリが `PATH` にありません。 + +修正: + +```bash +python3 -m pip install --user scons +python3 -m scons --version +``` + +`python3 -m scons` が動く場合、次の方法でもビルドできます。 + +```bash +python3 -m scons -j8 +``` + +### 14.2 `ModuleNotFoundError: No module named 'parse'` + +原因: Python パッケージ不足。 + +修正: + +```bash +python3 -m pip install --user parse requests tqdm paramiko scp +``` + +仮想環境では先に `source .venv/bin/activate` を実行します。 + +### 14.3 `Package sdl2 was not found in the pkg-config search path` + +原因: Linux SDL2 シミュレーション依存関係がインストールされていない、または `PKG_CONFIG_PATH` に SDL2 `.pc` ファイルのディレクトリが含まれていません。 + +修正: + +```bash +sudo apt install -y libsdl2-dev pkg-config +pkg-config --cflags sdl2 +``` + +macOS: + +```bash +brew install sdl2 pkg-config +pkg-config --cflags sdl2 +``` + +Windows/MSYS2: + +```bash +pacman -S --needed mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-SDL2 +pkg-config --cflags sdl2 +``` + +### 14.4 `Package freetype2 was not found` + +修正: + +```bash +sudo apt install -y libfreetype6-dev +pkg-config --cflags freetype2 +``` + +macOS: + +```bash +brew install freetype pkg-config +pkg-config --cflags freetype2 +``` + +Windows/MSYS2: + +```bash +pacman -S --needed mingw-w64-ucrt-x86_64-freetype +pkg-config --cflags freetype2 +``` + +### 14.5 `aarch64-linux-gnu-gcc: not found` + +原因: Linux クロスツールチェーンがインストールされていない、または `PATH` に含まれていません。 + +修正: + +```bash +sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu +aarch64-linux-gnu-gcc --version +``` + +macOS クロスコンパイルでは `aarch64-unknown-linux-gnu-gcc` を使います。対応する設定ファイルは `mac_cross_cp0_config_defaults.mk` です。 + +Windows クロスコンパイルでは `aarch64-linux-gnu-gcc.exe` を使います。`win_x86_cross_config_defaults.mk` の `CONFIG_TOOLCHAIN_PATH` と `CONFIG_TOOLCHAIN_PREFIX` を確認してください。 + +### 14.6 `sdk_bsp.tar.gz` のダウンロード失敗 + +原因: 初回クロスコンパイルでは `static_lib_v0.0.4` をダウンロードする必要がありますが、ネットワークがない、または GitHub アクセスに失敗しています。 + +修正: + +1. ネットワークから GitHub release にアクセスできることを確認します。 +2. `scons -j8` を再実行します。 +3. オフライン環境では次を手動で準備します。 + +```text +SDK/github_source/static_lib_v0.0.4/ +└── version # content should be v0.0.4 +``` + +ディレクトリが存在してもバージョンが一致しない場合、トップレベル `SConstruct` は更新を試みます。 + +### 14.7 `libcamera` ヘッダーまたはライブラリが見つからない + +クロスコンパイル設定では、`main/SConstruct` が次を追加します。 + +```text +$CONFIG_TOOLCHAIN_SYSROOT/usr/include/libcamera +-lcamera -lcamera-base -ljpeg +``` + +修正: + +```bash +ls SDK/github_source/static_lib_v0.0.4/usr/include/libcamera +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu | grep camera +``` + +不足している場合は sysroot パッケージを更新するか、デバイス側開発ライブラリをインストールして sysroot を再構築してください。 + +### 14.8 リンクエラー: `cannot find -linput`, `-lxkbcommon`, or `-ludev` + +ネイティブ SDL2 ビルド: 開発パッケージをインストールします。 + +```bash +sudo apt install -y libinput-dev libxkbcommon-dev libudev-dev +``` + +クロスコンパイル: sysroot を確認します。 + +```bash +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libinput.* +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libxkbcommon.* +ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libudev.* +``` + +### 14.9 設定切替後も古いバックエンドが使われる + +原因: `build/config/global_config.mk` が既に存在し、環境変数を変更しただけではビルドシステムが設定を自動再生成しません。 + +修正: + +```bash +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +最終設定を確認: + +```bash +grep -E 'LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk +``` + +### 14.10 SDL2 実行が黒画面またはリソース欠落になる + +よくある原因: プログラムを `dist` ディレクトリから実行しておらず、`CONFIG_V9_5_LV_FS_POSIX_PATH="./"` が間違った場所を指しています。 + +修正: + +```bash +cd projects/APPLaunch/dist +ls APPLaunch/share/images +./M5CardputerZero-APPLaunch +``` + +### 14.11 デバイスでリソースファイル欠落が報告される + +デバイス設定のリソースパス: + +```text +/usr/share/APPLaunch/ +``` + +確認: + +```bash +ls /usr/share/APPLaunch/share/images +ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +手動配備では、実行ファイルだけでなく `dist/APPLaunch` の内容をコピーしたことを確認してください。 + +### 14.12 RadioLib ダウンロード失敗 + +`ext_components/RadioLib/SConstruct` は `CONFIG_RADIOLIB_COMPONENT_ENABLED=y` のとき、`wget_github('https://github.com/jgromes/RadioLib.git')` で RadioLib を取得します。初回ビルドではネットワークアクセスが必要な場合があります。 + +修正: + +- ネットワークから GitHub にアクセスできることを確認します。 +- `SDK/github_source` 配下に RadioLib キャッシュが既に存在するか確認します。 +- オフライン環境では対応するソースキャッシュを事前に用意します。 + +## 15. 推奨ビルドフロー + +### 15.1 日常的な UI 開発 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch +``` + +### 15.2 正式なデバイス成果物の生成 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +その後、第 09 章に従って `.deb` パッケージング、インストール、systemd 検証を行います。 + +### 15.3 ビルドターゲットを素早く確認 + +```bash +grep CONFIG_DEFAULT_FILE /proc/$$/environ 2>/dev/null || true +grep -E 'CONFIG_TOOLCHAIN_PREFIX|LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk +file dist/M5CardputerZero-APPLaunch +``` diff --git a/docs/launcher-project-guide-ja/09-packaging-deployment-and-systemd.md b/docs/launcher-project-guide-ja/09-packaging-deployment-and-systemd.md new file mode 100644 index 00000000..e8edb0d1 --- /dev/null +++ b/docs/launcher-project-guide-ja/09-packaging-deployment-and-systemd.md @@ -0,0 +1,934 @@ +# 09 - パッケージング、配備、systemd + +この章では、APPLaunch を `dist` ディレクトリから Debian `.deb` としてパッケージ化する方法、M5CardputerZero へ配備する方法、systemd 自動起動を設定する方法、配備問題の確認とトラブルシュート方法を説明します。 + +特に記載がない限り、すべてのコマンドはリポジトリルートから開始するものとします。 + +```bash +cd /home/nihao/w2T/github/launcher +``` + +## 1. 配備形態の概要 + +APPLaunch はデバイス上で 2 種類のファイルに依存します。 + +1. メインプログラム: `M5CardputerZero-APPLaunch`。 +2. 実行時リソースツリー: `APPLaunch/`。アプリケーション記述子、フォント、画像、音声、スクリプト、任意のサブアプリケーションを含みます。 + +正式インストール後のターゲットパスは次のとおりです。 + +```text +/usr/share/APPLaunch/ +├── applications/ +├── bin/ +│ ├── M5CardputerZero-APPLaunch +│ ├── M5CardputerZero-AppStore # packaged if it exists in dist/bin +│ ├── M5CardputerZero-Calculator # packaged if it exists in dist/bin +│ └── appstore.py # packaged if it exists in dist/bin +├── lib/ +├── share/ +│ ├── font/ +│ └── images/ +└── cache -> /var/cache/APPLaunch # created by postinst +``` + +systemd サービスファイルは次へインストールされます。 + +```text +/usr/lib/systemd/user/APPLaunch.service +``` + +サービス起動コマンド: + +```text +/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +作業ディレクトリ: + +```text +/usr/share/APPLaunch +``` + +パッケージは APPLaunch を UID 1000 ユーザーの systemd user service としてインストールします。サービスを手動で確認する場合は、そのユーザーでログインして `systemctl --user ...` を実行するか、`runuser`/SSH 自動化経由では `XDG_RUNTIME_DIR=/run/user/1000` を設定してください。 + +## 2. パッケージング前にデバイスターゲットをビルドする + +`.deb` には Linux SDL2 x86_64 シミュレーション成果物ではなく、arm64 デバイス成果物を使う必要があります。 + +Linux x86_64 開発マシンで推奨されるクロスコンパイル: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +`file` の結果には次が含まれている必要があります。 + +```text +ARM aarch64 +``` + +`x86-64` と表示される場合、SDL2 ホスト成果物をパッケージ化しており、正式なランチャーとしてデバイスへインストールできません。 + +デバイス上のネイティブビルドもパッケージングに使用できます。 + +```bash +cd /home/pi/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=config_defaults.mk +scons -j2 +file dist/M5CardputerZero-APPLaunch +``` + +## 3. `debian_packager.py` 共通パッケージングスクリプト + +リポジトリレベルのパッケージングスクリプトは次にあります。 + +```text +scripts/debian_packager.py +``` + +これは以前の APPLaunch ローカルパッケージングスクリプトを置き換えるもので、`projects/` 配下の他プロジェクトでも同じ Debian パッケージングフローを再利用できます。APPLaunch は引き続きデフォルトターゲットなので、引数なしでスクリプトを実行すると APPLaunch がパッケージ化されます。 + +主なデフォルトとオプション: + +| Option / Default | Value | Description | +| --- | --- | --- | +| `--project` | `APPLaunch` | `projects/` 配下のプロジェクト名、またはプロジェクトパス | +| `--package-name` | `applaunch` | Debian パッケージ名 | +| `--app-name` | `APPLaunch` | インストールされるアプリケーション名と systemd サービス名 | +| `--bin-name` | `M5CardputerZero-APPLaunch` | メイン実行ファイル名 | +| `--src` / `--src-folder` | `dist` | ビルド出力ディレクトリ。プロジェクトディレクトリ相対で解決 | +| `--app-tree` | auto | 実行時リソースツリー。既定は `/`、次に `/` | +| `--output-dir` | `/tools` | 生成される `.deb` の出力ディレクトリ | +| `--work-dir` | output directory | ステージングディレクトリの親 | +| `--builder` | `auto` | 利用可能なら `dpkg-deb`、なければ pure Python writer を使用 | + +生成される APPLaunch パッケージファイル名の形式: + +```text +applaunch_0.2.1-m5stack1_arm64.deb +``` + +## 4. `.deb` パッケージディレクトリ構造 + +デフォルト APPLaunch オプションでスクリプトを実行すると、ステージングディレクトリは `projects/APPLaunch/tools` 配下に生成されます。 + +```text +projects/APPLaunch/tools/debian-APPLaunch/ +├── DEBIAN/ +│ ├── control +│ ├── postinst +│ └── prerm +└── usr/ + ├── lib/ + │ └── systemd/ + │ └── user/ + │ └── APPLaunch.service + └── share/ + └── APPLaunch/ + ├── applications/ + ├── bin/ + │ └── M5CardputerZero-APPLaunch + ├── lib/ + └── share/ + ├── audio/ + ├── font/ + └── images/ +``` + +最終的な APPLaunch `.deb` ファイルは次にあります。 + +```text +projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +``` + +## 5. パッケージングコマンド + +### 5.1 パッケージングツールのインストール + +`debian_packager.py` は Python だけでも `.deb` ファイルをビルドできます。`dpkg-deb` がインストールされている場合、既定の `--builder auto` 経路では自動的にそれを使用します。 + +Linux 開発マシン: + +```bash +sudo apt update +sudo apt install -y dpkg-dev fakeroot +``` + +macOS では Python builder または Homebrew `dpkg` を使えます。 + +```bash +brew install dpkg +``` + +### 5.2 APPLaunch パッケージングを実行 + +リポジトリルートから実行: + +```bash +python3 scripts/debian_packager.py +``` + +同等の明示的コマンド: + +```bash +python3 scripts/debian_packager.py build \ + --project APPLaunch \ + --package-name applaunch \ + --app-name APPLaunch \ + --bin-name M5CardputerZero-APPLaunch +``` + +成功すると、次のような出力が表示されます。 + +```text +Creating Debian package applaunch_0.2.1-m5stack1_arm64.deb ... +Staged package tree: .../projects/APPLaunch/tools/debian-APPLaunch +Debian package created: .../projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +Builder: dpkg-deb +``` + +### 5.3 カスタムバージョンを指定 + +```bash +python3 scripts/debian_packager.py build --version 0.2.2 --revision m5stack2 +``` + +別プロジェクトの場合は、プロジェクトメタデータと実行ファイル名を上書きします。 + +```bash +python3 scripts/debian_packager.py build \ + --project Calculator \ + --package-name calculator \ + --app-name Calculator \ + --bin-name M5CardputerZero-Calculator \ + --src dist \ + --app-tree share +``` + +`--app-tree` は、パッケージ内で `/usr/share/` になるべきリソースツリーに合わせて調整してください。 + +### 5.4 パッケージング成果物のクリーン + +スクリプトは次をサポートします。 + +```bash +python3 scripts/debian_packager.py clean +python3 scripts/debian_packager.py distclean +``` + +違い: + +| Command | Behavior | +| --- | --- | +| `clean` | `projects/APPLaunch/tools` 配下のデフォルト APPLaunch `*.deb` ファイルと `debian-APPLaunch` を削除 | +| `distclean` | `clean` に加えて、`projects/APPLaunch/tools` 配下のレガシー `m5stack_*` 出力も削除 | + +同じ clean コマンドは、非デフォルトプロジェクト向けに `--project`、`--project-dir`、`--app-name`、`--output-dir` も受け付けます。 + +## 6. パッケージングスクリプトのコピー規則 + +### 6.1 メインプログラムの検索 + +スクリプトは `src_folder` 配下でメインプログラムを探します。デフォルト APPLaunch ターゲットでは、これは `projects/APPLaunch` からの相対 `dist` です。 + +検索順序: + +1. `projects/APPLaunch/dist/M5CardputerZero-APPLaunch` +2. `projects/APPLaunch/dist/bin/M5CardputerZero-APPLaunch` + +どちらも存在しない場合、次を送出します。 + +```text +PackError: binary M5CardputerZero-APPLaunch not found in /dist or /dist/bin +``` + +### 6.2 追加アプリとバックエンド + +スクリプトは次の任意ファイルを含めようとします。 + +```text +projects/APPLaunch/dist/bin/M5CardputerZero-AppStore +projects/APPLaunch/dist/bin/appstore.py +projects/APPLaunch/dist/bin/M5CardputerZero-Calculator +``` + +存在する場合、次へコピーされます。 + +```text +/usr/share/APPLaunch/bin/ +``` + +`.py` 以外のファイルは `0755` に設定されます。 + +### 6.3 リソースツリーコピー + +スクリプトはソース側リソースツリーを優先してコピーします。 + +```text +projects/APPLaunch/APPLaunch +``` + +パッケージ内のターゲット: + +```text +usr/share/APPLaunch +``` + +ソースリソースツリーが存在しない場合は、次を試します。 + +```text +projects/APPLaunch/dist/APPLaunch +``` + +つまり、パッケージングは通常 `dist/APPLaunch` だけに依存せず、プロジェクトソースディレクトリの `APPLaunch/` リソースツリーもコピーします。 + +### 6.4 AppStore 画像の追加 + +次のディレクトリが存在する場合: + +```text +projects/AppStore/share/images +``` + +スクリプトは次の画像をパッケージ内の `usr/share/APPLaunch/share/images` へコピーします。 + +```text +store_wordmark.png +store_arrow_*.png +``` + +## 7. Debian 制御スクリプト + +### 7.1 `DEBIAN/control` + +生成される control ファイルには次が含まれます。 + +```text +Package: applaunch +Version: 0.2.1 +Architecture: arm64 +Maintainer: dianjixz +Original-Maintainer: m5stack +Section: APPLaunch +Priority: optional +Homepage: https://www.m5stack.com +Packaged-Date: +Description: M5CardputerZero APPLaunch +``` + +重要点: + +- `Architecture` は `arm64` 固定です。 +- スクリプトは `Depends` を自動宣言しないため、依存ライブラリはベースイメージに含めるか、将来版で明示する必要があります。 + +### 7.2 `DEBIAN/postinst` + +インストール後スクリプトは次を実行します。 + +```sh +mkdir -p /var/cache/APPLaunch +ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +loginctl enable-linger "$APP_USER" || true +systemctl start "user@$APP_UID.service" || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user enable APPLaunch.service || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user restart APPLaunch.service || true +exit 0 +``` + +目的: + +- 書き込み可能なキャッシュディレクトリ `/var/cache/APPLaunch` を作成します。 +- 読み取り専用/システムリソースディレクトリ配下に `cache` シンボリックリンクを作成します。 +- UID 1000 ユーザーの lingering を有効にし、systemd user service を有効化して起動します。 + +注意: 現在の共通 packager は `ln -sfn` を使うため、繰り返しインストールしてもキャッシュリンクを安全に更新できます。 + +### 7.3 `DEBIAN/prerm` + +削除前スクリプトは次を実行します。 + +```sh +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user stop APPLaunch.service || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user disable APPLaunch.service || true +rm -rf /var/cache/APPLaunch +exit 0 +``` + +目的: + +- サービスを停止します。 +- 起動時の自動開始を無効化します。 +- キャッシュディレクトリを削除します。 + +注意: アンインストールすると `/var/cache/APPLaunch` が削除されます。そこに保存された実行時キャッシュや app-store キャッシュも削除されます。 + +## 8. systemd サービスファイル + +スクリプトは次を生成します。 + +```ini +[Unit] +Description=APPLaunch Service +After=pipewire-pulse.service +Wants=pipewire-pulse.service + +[Service] +ExecStart=/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +WorkingDirectory=/usr/share/APPLaunch +Restart=always +RestartSec=1 +StartLimitInterval=0 + +[Install] +WantedBy=default.target +``` + +フィールド説明: + +| Field | Description | +| --- | --- | +| `ExecStart` | APPLaunch メインプログラムを起動 | +| `WorkingDirectory` | カレントディレクトリを `/usr/share/APPLaunch` に設定し、相対パスアクセスを扱いやすくする | +| `Restart=always` | プロセス終了後は常に再起動 | +| `RestartSec=1` | 終了 1 秒後に再起動 | +| `StartLimitInterval=0` | 頻繁なクラッシュ後に systemd が再起動を止めないよう、既定の起動レート制限を無効化 | +| `After` / `Wants` | 利用可能な場合は PipeWire PulseAudio サポートの後に起動 | +| `WantedBy=default.target` | ユーザーの既定 systemd target でサービスを有効化 | + +現在のパッケージは root 所有の system service ではなく、`/usr/lib/systemd/user` 配下の user service をインストールします。`postinst` により UID 1000 ユーザー向けに有効化されるため、framebuffer、evdev、GPIO、audio、camera へのデバイス権限は、イメージのユーザー/グループ規則で提供されている必要があります。 + +## 9. デバイスへのインストール + +### 9.1 `.deb` をデバイスへコピー + +デバイス IP が `192.168.28.177`、ユーザー名が `pi` だとします。 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch/tools +scp applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +``` + +### 9.2 デバイスでインストール + +```bash +ssh pi@192.168.28.177 +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +インストーラが依存関係不足を報告した場合は、先に依存関係を修正します。 + +```bash +sudo apt-get -f install +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +### 9.3 上書きインストール + +同じパッケージ名またはより高いバージョンを再度インストールします。 + +```bash +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +``` + +サービスが実行中の場合、`postinst` は有効化/起動を試みます。インストール中の framebuffer または入力デバイス競合を減らすには、先にサービスを手動停止できます。 + +```bash +systemctl --user stop APPLaunch.service || true +sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb +systemctl --user restart APPLaunch.service +``` + +## 10. `scons push` による簡易配備 + +`.deb` に加えて、プロジェクトは `setup.ini` 経由で `dist` ディレクトリをアップロードすることもサポートします。 + +設定ファイル: + +```text +projects/APPLaunch/setup.ini +``` + +デフォルト内容の例: + +```ini +[ssh] +local_file_path = dist +remote_file_path = /home/pi/dist +remote_host = 192.168.28.177 +remote_port = 22 +username = pi +password = pi +; before_cmd = 'echo pi | sudo -S systemctl --user stop APPLaunch.service' +; after_cmd = 'echo pi | sudo -S systemctl --user stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl --user start APPLaunch.service' +``` + +実行: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons push +``` + +`SDK/tools/scons/push.py` は次を行います。 + +1. `setup.ini` を読みます。 +2. `local_file_path` 配下のすべてのファイルを反復します。 +3. ローカル MD5 ハッシュを計算します。 +4. SSH 経由でリモートファイルの MD5 ハッシュを取得します。 +5. 変更されたファイルだけをアップロードします。 +6. 任意で `before_cmd` と `after_cmd` を実行します。 + +適している用途: + +- 開発中に `dist` を素早く置き換える。 +- 単一のビルド結果を素早くアップロードする。 +- Debian インストールスクリプトをテストする必要がない場合。 + +適していない用途: + +- 正式なインストールパスの検証。 +- `postinst` と `prerm` の検証。 +- systemd の enable/install 挙動の検証。 +- 配布可能なインストールパッケージの生成。 + +## 11. 手動配備 + +`.deb` や `scons push` を使いたくない場合、ファイルを手動でコピーできます。 + +開発マシンからアップロード: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scp dist/M5CardputerZero-APPLaunch pi@192.168.28.177:/home/pi/ +scp -r dist/APPLaunch pi@192.168.28.177:/home/pi/APPLaunch-new +``` + +デバイスでインストール: + +```bash +systemctl --user stop APPLaunch.service || true +sudo mkdir -p /usr/share/APPLaunch/bin +sudo install -m 0755 /home/pi/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +sudo rsync -a --delete /home/pi/APPLaunch-new/ /usr/share/APPLaunch/ +sudo mkdir -p /var/cache/APPLaunch +sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +systemctl --user daemon-reload +systemctl --user restart APPLaunch.service +``` + +サービスファイルがまだインストールされていない場合は、Section 8 の内容を参考に `/usr/lib/systemd/user/APPLaunch.service` を手動作成してください。 + +## 12. 配備確認コマンド + +### 12.1 パッケージ状態 + +```bash +dpkg -l | grep applaunch +dpkg -s applaunch +``` + +パッケージがインストールしたファイルを一覧: + +```bash +dpkg -L applaunch +``` + +インストールせずに `.deb` パッケージ内容を確認: + +```bash +dpkg-deb -c applaunch_0.2.1-m5stack1_arm64.deb +``` + +`.deb` メタデータを確認: + +```bash +dpkg-deb -I applaunch_0.2.1-m5stack1_arm64.deb +``` + +### 12.2 ファイルと権限 + +```bash +ls -l /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +file /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +ls -ld /usr/share/APPLaunch +ls -l /usr/share/APPLaunch/cache +ls -l /var/cache/APPLaunch +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | head +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | head +``` + +期待値: + +- メインプログラムに実行権限がある。 +- メインプログラムのアーキテクチャが `ARM aarch64`。 +- `/usr/share/APPLaunch/cache` が `/var/cache/APPLaunch` を指している。 +- 画像とフォントリソースが存在する。 + +### 12.3 動的ライブラリ依存関係 + +デバイス上で: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +不足依存を確認: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +``` + +ライブラリが不足している場合は、対応するシステムパッケージをインストールするか、パッケージング規則を拡張して private ライブラリを `/usr/share/APPLaunch/lib` に配置し、実行時検索パスを設定します。 + +### 12.4 systemd 状態 + +```bash +systemctl --user status APPLaunch.service --no-pager +systemctl --user is-enabled APPLaunch.service +systemctl --user is-active APPLaunch.service +``` + +ログ表示: + +```bash +journalctl --user -u APPLaunch.service -b --no-pager +journalctl --user -u APPLaunch.service -b -f +``` + +再起動: + +```bash +systemctl --user restart APPLaunch.service +``` + +停止: + +```bash +systemctl --user stop APPLaunch.service +``` + +起動時自動開始を有効化: + +```bash +systemctl --user enable APPLaunch.service +``` + +起動時自動開始を無効化: + +```bash +systemctl --user disable APPLaunch.service +``` + +サービスファイルを再読み込み: + +```bash +systemctl --user daemon-reload +``` + +### 12.5 手動フォアグラウンド実行 + +systemd をトラブルシュートする前に、まずフォアグラウンドで実行します。 + +```bash +systemctl --user stop APPLaunch.service || true +cd /usr/share/APPLaunch +sudo ./bin/M5CardputerZero-APPLaunch +``` + +標準出力とクラッシュメッセージを直接確認できます。フォアグラウンド実行は動くが systemd では動かない場合、サービスファイル、権限、作業ディレクトリを確認してください。 + +### 12.6 framebuffer と入力デバイス + +framebuffer を確認: + +```bash +ls -l /dev/fb* +cat /sys/class/graphics/fb0/name 2>/dev/null || true +``` + +入力デバイスを確認: + +```bash +ls -l /dev/input/ +cat /proc/bus/input/devices +``` + +framebuffer または入力デバイスを保持しているプロセスを確認: + +```bash +sudo fuser -v /dev/fb0 2>/dev/null || true +sudo fuser -v /dev/input/event* 2>/dev/null || true +``` + +別のグラフィックスプログラムが動作中の場合、APPLaunch が正しく表示または入力読み取りできないことがあります。 + +## 13. アンインストールとロールバック + +### 13.1 アンインストール + +```bash +sudo dpkg -r applaunch +``` + +これにより `prerm` が実行され、サービス停止、無効化、`/var/cache/APPLaunch` 削除が行われます。 + +設定ファイルもクリーンする場合: + +```bash +sudo dpkg -P applaunch +``` + +### 13.2 古いパッケージをインストールしてロールバック + +```bash +systemctl --user stop APPLaunch.service || true +sudo dpkg -i /home/pi/applaunch_old-version-m5stack1_arm64.deb +systemctl --user restart APPLaunch.service +``` + +確認: + +```bash +dpkg -s applaunch | grep Version +systemctl --user status APPLaunch.service --no-pager +``` + +### 13.3 ランチャーを一時的に無効化 + +```bash +systemctl --user disable --now APPLaunch.service +``` + +復元: + +```bash +systemctl --user enable --now APPLaunch.service +``` + +## 14. よくある配備エラー + +### 14.1 インストールエラー: `package architecture (arm64) does not match system` + +原因: デバイスシステムが arm64 ではない、または arm64 パッケージを x86_64 開発マシンへ直接インストールしています。 + +修正: + +```bash +uname -m +dpkg --print-architecture +``` + +`.deb` は Linux x86_64 開発マシンではなく、M5CardputerZero デバイスにインストールしてください。 + +### 14.2 実行時エラー: `Exec format error` + +原因: メインプログラムのアーキテクチャが間違っています。よくある例は、Linux SDL2 x86_64 成果物を arm64 パッケージへ入れている場合です。 + +確認: + +```bash +file /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +正しい修正: 再度クロスコンパイルします。 + +```bash +cd projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +``` + +その後、再パッケージ化して再インストールします。 + +### 14.3 サービスが再起動し続ける + +確認: + +```bash +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +よくある原因: + +- 動的ライブラリ不足。 +- リソースパスが存在しない。 +- framebuffer または入力デバイスが利用できない。 +- プログラムが起動直後にクラッシュする。 +- インストールされた成果物のアーキテクチャが間違っている。 + +追加確認: + +```bash +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +ls /usr/share/APPLaunch/share/images +ls /dev/fb0 +``` + +### 14.4 `ln: failed to create symbolic link '/usr/share/APPLaunch/cache': File exists` + +原因: 古いパッケージが非冪等な `ln -s` でキャッシュリンクを作成しており、ターゲットが既に存在しています。 + +修正: + +```bash +sudo rm -rf /usr/share/APPLaunch/cache +sudo mkdir -p /var/cache/APPLaunch +sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +systemctl --user restart APPLaunch.service +``` + +現在の共通 packager は既に `ln -sfn` を書き込みます。パッケージを再ビルドして再インストールすると修正を永続化できます。 + +### 14.5 `dpkg-deb: error: failed to open package info file .../DEBIAN/control` + +原因: パッケージングディレクトリ構造が不完全、またはスクリプトが途中で失敗して異常なディレクトリを残しています。 + +修正: + +```bash +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py clean +python3 scripts/debian_packager.py +``` + +### 14.6 `Binary M5CardputerZero-APPLaunch not found in .../dist` + +原因: プロジェクトがビルドされていない、またはビルドディレクトリが `projects/APPLaunch/dist` ではありません。 + +修正: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +ls -l dist/M5CardputerZero-APPLaunch +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +``` + +### 14.7 サービス起動後に黒画面 + +調査順序: + +1. 実行ファイルをフォアグラウンドで起動できるか確認します。 +2. framebuffer が存在するか確認します。 +3. 他プロセスがディスプレイを占有していないか確認します。 +4. リソースパスが存在するか確認します。 +5. journal ログを確認します。 + +コマンド: + +```bash +systemctl --user stop APPLaunch.service || true +cd /usr/share/APPLaunch +sudo ./bin/M5CardputerZero-APPLaunch +ls -l /dev/fb0 +sudo fuser -v /dev/fb0 2>/dev/null || true +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +### 14.8 外部アプリが起動できない + +APPLaunch はリソースツリーと `.desktop` 記述子から外部アプリを探します。まず確認: + +```bash +find /usr/share/APPLaunch/applications -maxdepth 1 -type f -print +find /usr/share/APPLaunch/bin -maxdepth 1 -type f -print +``` + +外部アプリに実行権限があるか確認: + +```bash +ls -l /usr/share/APPLaunch/bin +``` + +`.desktop` ファイル内の `Exec` が存在しないパスを指す場合、リソースツリーを修正するか再パッケージしてください。 + +## 15. リリース前チェックリスト + +パッケージング前: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +``` + +パッケージング後: + +```bash +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +dpkg-deb -I projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb +dpkg-deb -c projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb | head -n 50 +``` + +インストール後: + +```bash +dpkg -s applaunch | grep -E 'Package|Version|Architecture' +systemctl --user status APPLaunch.service --no-pager +systemctl --user is-enabled APPLaunch.service +ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true +ls -l /usr/share/APPLaunch/cache +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 +``` + +機能確認: + +- デバイス起動後、APPLaunch が自動的にホーム画面を表示する。 +- キーボード/ボタン入力が動作する。 +- ホーム画面のアプリケーションカルーセルで項目を切り替えられる。 +- リソース画像とフォントが正しく表示される。 +- 組み込みページに入って戻れる。 +- 外部アプリを起動した後、終了して APPLaunch へ戻れる。 +- AppStore/Calculator など任意のサブアプリケーションがパッケージされている場合、ランチャーから正常に起動できる。 + +## 16. 推奨配備フロー + +正式リリースでは次を使います。 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +file dist/M5CardputerZero-APPLaunch +cd /home/nihao/w2T/github/launcher +python3 scripts/debian_packager.py +scp projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ +ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl --user status APPLaunch.service --no-pager' +``` + +開発中の高速置き換えでは次を使います。 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +export CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk +scons -j8 +scons push +``` + +違い: `.deb` は完全なインストールと systemd ライフサイクルを検証できます。`scons push` は高速ですが、正式なパッケージング検証の代替にはなりません。 diff --git a/docs/launcher-project-guide-ja/10-extension-development-guide.md b/docs/launcher-project-guide-ja/10-extension-development-guide.md new file mode 100644 index 00000000..c191853a --- /dev/null +++ b/docs/launcher-project-guide-ja/10-extension-development-guide.md @@ -0,0 +1,420 @@ +# 10 - 拡張開発ガイド + +この章では APPLaunch の拡張方法を説明します。特に、組み込みページの追加、外部 `.desktop` アプリの追加、画像/音声/フォントアセットの追加、設定トグルの変更という 4 つの一般的な変更に焦点を当てます。中核コードは `projects/APPLaunch/main/ui` 配下にあり、プラットフォーム適応とパス解決は `ext_components/cp0_lvgl` 配下にあります。 + +## 1. 拡張前に理解しておく入口 + +| Entry point | Purpose | +| --- | --- | +| `projects/APPLaunch/main/ui/launch.cpp` | 固定アプリリスト、動的 `.desktop` スキャン、組み込みページまたは外部プロセスの起動 | +| `projects/APPLaunch/main/ui/page_app/` | 組み込みページ実装ディレクトリ。ページは通常 header-only の `.hpp` ファイル | +| `projects/APPLaunch/main/ui/ui_app_page.hpp` | `AppPage`、トップバー、`img_path()`、`audio_path()` などの共通ページ機能 | +| `projects/APPLaunch/main/ui/generate_page_app_includes.py` | ビルド前に `generated/page_app.h` を自動生成し、すべての `page_app/*.hpp` ファイルを include | +| `projects/APPLaunch/APPLaunch/` | 実行時アセットツリー。パッケージング後はデバイス上の `/usr/share/APPLaunch/` に対応 | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | デバイス側 `cp0_file_path()` パスルール | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 開発ホスト側 `cp0_file_path()` パスルール | +| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | デバイス側設定永続化。`/var/lib/applaunch/settings` に保存 | + +APPLaunch には 2 種類のアプリソースがあります。 + +- **組み込みページ**: APPLaunch プロセスにコンパイルされ、`app("NAME", icon, page_v)` で登録されます。開くと APPLaunch が `PageT` オブジェクトを作成し、その画面へ切り替えます。 +- **外部アプリ**: 固定 `Exec` 値または `.desktop` 記述子を通じて独立プロセスとして起動されます。非ターミナルアプリでは、Launcher は LVGL タイマーを一時停止し、子プロセス終了を待ってからホームページへ戻ります。 + +## 2. 組み込みページを追加する + +組み込みページは、Launcher と同じプロセスで動作し、LVGL を直接使い、入力グループ、トップバー、ステータスバーを共有する必要がある機能に適しています。例として Settings、Game、File、Camera、LoRa ページがあります。 + +### 2.1 ページファイルを作成 + +`projects/APPLaunch/main/ui/page_app/` 配下に新しい `.hpp` を作成します。推奨命名スタイルは `ui_app_xxx.hpp` です。ページクラスは `AppPage` を継承し、コンストラクタでタイトル設定、UI 作成、キーイベントのバインドを行います。 + +最小スケルトン: + +```cpp +#pragma once + +#include "../ui_app_page.hpp" + +class UIMyToolPage : public AppPage +{ +public: + UIMyToolPage() : AppPage() + { + set_page_title("MY TOOL"); + create_ui(); + event_handler_init(); + } + +private: + lv_obj_t *title_ = nullptr; + + void create_ui() + { + lv_obj_t *root = screen(); + lv_obj_set_style_bg_color(root, lv_color_hex(0x101820), LV_PART_MAIN | LV_STATE_DEFAULT); + + UIAppTopBar top("MY TOOL"); + top.create(root); + + title_ = lv_label_create(root); + lv_label_set_text(title_, "Hello APPLaunch"); + lv_obj_center(title_); + } + + void event_handler_init() + { + lv_obj_add_event_cb(screen(), &UIMyToolPage::key_event_cb, LV_EVENT_KEY, this); + } + + static void key_event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + uint32_t key = lv_event_get_key(e); + if (key == LV_KEY_ESC && self->navigate_home) { + self->navigate_home(); + } + } +}; +``` + +注意: + +- ページは `AppPage` を継承する必要があります。`screen()`、`input_group()`、`navigate_home` などの機構を再利用するためです。 +- ホームへ戻るには `navigate_home()` を呼ぶことを推奨します。ホーム画面を直接 load しないでください。そうしないと `Launch` が現在のページオブジェクトを正しく解放できません。 +- ページが LVGL timer、ファイルディスクリプタ、スレッド、周辺機器ハンドルを作成する場合は、デストラクタで解放してください。 +- ページサイズは 320x170 を基準にします。一般的なレイアウトは 20 px のトップバーと 320x150 の本文です。 +- アセットの絶対パスをハードコードしないでください。画像は `img_path("xxx.png")`、音声は `audio_path("xxx.wav")` を使います。 + +### 2.2 ページが include されることを確認 + +`projects/APPLaunch/main/SConstruct` はビルド前にこのスクリプトを実行します。 + +```python +ui/generate_page_app_includes.py +``` + +このスクリプトは `projects/APPLaunch/main/ui/page_app/*.hpp` をスキャンし、`projects/APPLaunch/build/generated/include/generated/page_app.h` を生成します。ほとんどの場合、ファイル拡張子が `.hpp` であればビルド中に自動 include されます。 + +手動確認する場合、`generated/page_app.h` には次が含まれるはずです。 + +```cpp +#include "page_app/ui_app_my_tool.hpp" +``` + +### 2.3 ホームのアプリリストへ登録 + +`projects/APPLaunch/main/ui/launch.cpp` を開き、`Launch::Launch()` を探します。組み込みページは次のように登録します。 + +```cpp +app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); +``` + +Settings ページから表示有無を制御できるように、`APP_ENABLED` 制御セクション内へ置くことを推奨します。 + +```cpp +#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) + +if (APP_ENABLED("MyTool")) + app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); + +#undef APP_ENABLED +``` + +登録ルール: + +- 第 1 引数はホームカルーセルの表示名です。小さい画面で切れないよう短くしてください。 +- 第 2 引数はアイコンパスで、通常 `img_path("xxx_100.png")` です。 +- 第 3 引数 `page_v` は、アプリをクリックしたときに組み込みページが作成されることを意味します。 +- ページがデバイス側ハードウェアだけをサポートする場合、SDL2 ビルド失敗を避けるため `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 内に置いてください。 + +### 2.4 Settings ページのトグルを追加 + +Settings の `Launcher` メニューから新しいページの表示を制御したい場合、`UISetupPage::menu_init()` の `app_keys` と `app_labels` を更新します。 + +例: + +```cpp +static const char *app_keys[] = { + "Python", "Store", "CLI", "Game", "Setting", + "Game", "Math", "MyTool" +}; + +static const char *app_labels[] = { + "Python", "Store", "CLI", "Game", "Setting", + "Game", "Math", "My Tool" +}; +``` + +`save_app_toggle()` はスイッチを `app_` として保存します。例: `app_MyTool=0`。`launch.cpp` では同じキーを読みます。 + +```cpp +cp0_config_get_int("app_MyTool", 1) +``` + +デバイス側の永続化ファイル: + +```text +/var/lib/applaunch/settings +``` + +### 2.5 ビルド確認 + +SDL2 ローカル確認: + +```bash +cd projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed +./dist/M5CardputerZero-APPLaunch +``` + +デバイスクロスコンパイル確認: + +```bash +cd projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed +``` + +デバイス専用ページでは、少なくともクロスビルドを実行してください。ページが開発ホストでも表示できる場合は、まず SDL2 で UI とキーを素早く確認します。 + +## 3. 外部 `.desktop` アプリを追加する + +外部 `.desktop` アプリは、独立した実行ファイル、スクリプト、ターミナルコマンドに適しています。C++ のアプリリストを変更する必要はありません。APPLaunch が `applications` ディレクトリをスキャンし、ホームページへ動的に追加します。 + +### 3.1 `.desktop` ファイルを置く + +開発ツリーのパス: + +```text +projects/APPLaunch/APPLaunch/applications/ +``` + +インストール後のデバイスパス: + +```text +/usr/share/APPLaunch/applications/ +``` + +既存テンプレート: + +```text +projects/APPLaunch/APPLaunch/applications/vim.desktop.temple +``` + +現在のスキャンロジックは `.desktop` で終わるファイル名だけを扱います。`.desktop.temple` はテンプレートであり、読み込まれません。 + +### 3.2 `.desktop` フィールド形式 + +最小例: + +```ini +[Desktop Entry] +Name=Vim +Exec=vim +Terminal=true +Icon=share/images/email.png +Type=Application +``` + +APPLaunch が現在解析するフィールド: + +| Field | Required | Description | +| --- | --- | --- | +| `Name` | Yes | ホームページに表示する名前 | +| `Exec` | Yes | 実行コマンド。絶対パスまたは shell コマンドを指定可能 | +| `Icon` | No | アイコンパス。推奨形式は `share/images/xxx.png`、または LVGL が読める任意パス | +| `Terminal` | No | `true`/`True`/`1` の場合、組み込み `UIConsolePage` 内で実行 | +| `Sysplause` | No | ターミナルアプリのみ。ターミナルコマンド終了後の一時停止動作を制御。既定は true | +| `Type` | No | desktop-file 慣例との互換用。APPLaunch は現在依存していない | +| `TryExec` | No | APPLaunch は現在解析しない。説明用フィールドとしてのみ使用可能 | + +例 1: ターミナルコマンドを起動。 + +```ini +[Desktop Entry] +Name=TOP +Exec=top +Terminal=true +Sysplause=false +Icon=share/images/cli_100.png +Type=Application +``` + +例 2: 独立プログラムを起動。 + +```ini +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/my_app +Terminal=false +Icon=share/images/my_app_100.png +Type=Application +``` + +例 3: スクリプトを起動。 + +```ini +[Desktop Entry] +Name=NetInfo +Exec=/bin/sh /usr/share/APPLaunch/bin/netinfo.sh +Terminal=true +Icon=share/images/ip_panel_100.png +Type=Application +``` + +### 3.3 外部アプリ起動時の挙動 + +`launch.cpp` は 2 種類の外部アプリ起動モードをサポートします。 + +- `Terminal=true`: `UIConsolePage` を作成し、APPLaunch プロセス内に PTY ターミナルを表示して `Exec` を実行します。 +- `Terminal=false`: `cp0_process_exec_blocking()` を呼んで外部プロセスを開始します。APPLaunch は LVGL タイマーと入力グループを一時停止し、子プロセス終了を待ってからホームページを復元します。 + +非ターミナル外部アプリからの復帰は次の挙動に依存します。 + +- 子プロセスが正常終了すると、APPLaunch は `launch_page_->show_home_screen()` を呼んでホーム画面と入力グループを復元します。 +- デバイス上では ESC を約 3 秒押し続けると外部アプリのプロセスグループへ SIGTERM を送ります。さらに 3 秒後も終了していなければ SIGKILL が送られます。 +- `cp0_process_exec_blocking()` は Launcher のキーボードスレッドを一時停止し、外部プログラムが evdev 入力を直接読めるようにします。 + +### 3.4 動的更新 + +APPLaunch は起動時に `applications_load()` を呼んで `.desktop` ファイルをスキャンします。その後、`inotify`/SDL ディレクトリ監視が 3 秒ごとにアプリケーションディレクトリを確認します。`.desktop` ファイルの追加、削除、編集後、通常は Launcher を再起動しなくてもカルーセルが自動更新されます。 + +更新されない場合: + +```bash +# Device side +ls -l /usr/share/APPLaunch/applications +journalctl --user -u APPLaunch.service -f + +# SDL2 development host: confirm APPLaunch/applications exists near the run directory +find projects/APPLaunch -path '*APPLaunch/applications*' -maxdepth 5 -type f +``` + +### 3.5 重複排除ルール + +動的アプリは `Exec` によって重複排除されます。2 つの `.desktop` ファイルが完全に同じ `Exec` を持つ場合、後からスキャンされたファイルはスキップされ、次のメッセージが出力されます。 + +```text +applications_load: skip ... (duplicate Exec) +``` + +## 4. アセットを追加する + +アセットには画像、音声、フォント、外部プログラム/スクリプトが含まれます。開発ツリーでは `projects/APPLaunch/APPLaunch/` 配下に置きます。ビルド時、`main/SConstruct` は `STATIC_FILES += [ADir('../APPLaunch')]` により、このツリーを出力/インストールパッケージへコピーします。 + +### 4.1 アセットディレクトリ + +| Type | Development-tree path | Device path | Recommended access method | +| --- | --- | --- | --- | +| Images | `projects/APPLaunch/APPLaunch/share/images/` | `/usr/share/APPLaunch/share/images/` | `img_path("xxx.png")` または `.desktop` `Icon=share/images/xxx.png` | +| Audio | `projects/APPLaunch/APPLaunch/share/audio/` | `/usr/share/APPLaunch/share/audio/` | `audio_path("xxx.wav")` | +| Fonts | `projects/APPLaunch/APPLaunch/share/font/` | `/usr/share/APPLaunch/share/font/` | `launcher_fonts().get("xxx.ttf", size, style)` | +| External apps | `projects/APPLaunch/APPLaunch/bin/` | `/usr/share/APPLaunch/bin/` | `.desktop` `Exec=/usr/share/APPLaunch/bin/xxx` | +| `.desktop` | `projects/APPLaunch/APPLaunch/applications/` | `/usr/share/APPLaunch/applications/` | 自動スキャン | + +`bin/` ディレクトリやスクリプトを追加する場合は、スクリプトに実行権限を付けるか、`.desktop` ファイルで `/bin/sh script.sh` 経由で呼び出してください。 + +### 4.2 `cp0_file_path()` パスルール + +デバイス側 `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` の主要ルール: + +- `cp0_file_path("applications")` -> `/usr/share/APPLaunch/applications` +- `cp0_file_path("lock_file")` -> `/tmp/M5CardputerZero-APPLaunch_fcntl.lock` +- 画像拡張子 `png/gif/jpg/jpeg/svg` -> `share/images/` +- 音声拡張子 `wav/mp3/ogg` -> `/usr/share/APPLaunch/share/audio/` +- フォント拡張子 `ttf/otf` -> `/usr/share/APPLaunch/share/font/` + +SDL2 では、`sdl_lvgl_file.cpp` が実行ファイルディレクトリ、カレントディレクトリ、`APPLaunch/share` からアセットルートを推定し、開発ホストで扱いやすいようカレントディレクトリ相対のパスへ変換します。 + +### 4.3 画像アセットの推奨事項 + +- ホームカルーセルアイコンには `mytool_100.png` のような 100 px 版を用意してください。 +- ページ内の小さなアイコンには、必要に応じて 80 px 以下の版を用意します。 +- LVGL は画像パスと形式に敏感です。リポジトリ既存の PNG 命名とサイズスタイルを再利用するのが最も安全です。 +- `panel_set_icon()` が `missing/unreadable` を出力する場合、まずファイルがソースディレクトリだけでなく実行時アセットツリーにも存在するか確認します。 + +### 4.4 音声アセットの推奨事項 + +ページのキー音では `UISetupPage` を参考にします。 + +```cpp +std::string snd_enter_ = audio_path("key_enter.wav"); +cp0_signal_audio_api({"PlayFile", snd_enter_}, nullptr); +``` + +デバイス側音声は通常 `/usr/share/APPLaunch/share/audio/xxx.wav` を使います。SDL2 側はパス適応レイヤーで解決されます。 + +### 4.5 フォントアセットの推奨事項 + +フォントは `share/font/` 配下に置きます。ページでは繰り返し作成を避けるため、共有フォントキャッシュを優先してください。 + +```cpp +lv_font_t *font = launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD); +lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); +``` + +フォント追加後は、SDL2 とデバイスビルドの両方で FreeType が有効であることを確認してください。SDL2 設定とクロスビルド設定はいずれも LVGL に FreeType 関連 include/link パラメータを追加します。 + +## 5. Settings トグルを変更する + +Settings ページは `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` に集約されています。現在の設定には Launcher アプリ可視性トグル、Boot、Screen、WiFi、Speaker、Camera、Info、About、Help、ExtPort などがあります。 + +### 5.1 Launcher アプリトグルを追加 + +手順: + +1. `UISetupPage::menu_init()` の `app_keys` に `MyTool` のような内部キーを追加します。 +2. 同じ場所の `app_labels` に `My Tool` のような表示ラベルを追加します。 +3. `launch.cpp` でアプリ登録時に同じキーを使います: `APP_ENABLED("MyTool")`。 +4. Settings ページを開き、`Launcher` メニューへ入り、O/X を切り替えます。 +5. ホームへ戻った後にリストが更新されない場合は APPLaunch を再起動します。現在の固定/組み込みリストは `Launch` 構築時に設定を読みます。 + +### 5.2 通常設定を追加 + +`menu_init()` の対応するグループを探し、`sub_items` に項目を追加します。 + +```cpp +{"My Option", true, cp0_config_get_int("my_option", 1) != 0, [this]() { + bool en = cp0_config_get_int("my_option", 1) == 0; + cp0_config_set_int("my_option", en ? 1 : 0); + cp0_config_save(); +}}, +``` + +値を選択する第 2 階層または第 3 階層ページについては、既存実装を参照してください。 + +- `enter_brightness_adjust()`: 輝度選択。 +- `enter_darktime_adjust()`: 画面オフタイムアウト選択。 +- `enter_volume_adjust()` と `apply_volume()`: 音量保存と適用。 +- `enter_camera_resolution()`: カメラ解像度。 +- `enter_startup_mode()`: 起動モード。 + +### 5.3 設定永続化場所 + +デバイス側設定実装: `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`。 + +- 設定ディレクトリ: `/var/lib/applaunch` +- 設定ファイル: `/var/lib/applaunch/settings` +- 形式: 1 行に 1 つの `key=value` +- 最大エントリ数: `MAX_ENTRIES=32` + +よく使うコマンド: + +```bash +sudo cat /var/lib/applaunch/settings +sudo sed -i 's/^app_Game=.*/app_Game=1/' /var/lib/applaunch/settings +systemctl --user restart APPLaunch.service +``` + +設定項目を多数追加する場合、現在の最大数が 32 エントリであることに注意してください。この上限を超えると `cp0_config_set_*` はすぐ return し、設定は保存されません。 + +## 6. 拡張時の確認チェックリスト + +| Check item | Method | +| --- | --- | +| ファイルが正しいディレクトリだけに置かれている | 組み込みページは `main/ui/page_app/`、アセットは `APPLaunch/share/`、`.desktop` ファイルは `APPLaunch/applications/` | +| SDL2 ビルドが成功する | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | +| デバイスクロスビルドが成功する | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | +| アイコンが正しく表示される | `set panel icon missing/unreadable` ログを確認 | +| ページがホームへ戻れる | 組み込みページは ESC で `navigate_home()` を呼ぶ。外部ページは自分で終了するか、長押し ESC で戻る | +| `.desktop` が読み込まれる | ファイル名が `.desktop` で終わり、`[Desktop Entry]`、`Name`、`Exec` を含む | +| 設定が保存される | 対応キーが `/var/lib/applaunch/settings` に書き込まれているか確認 | diff --git a/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md b/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md new file mode 100644 index 00000000..0faa1e56 --- /dev/null +++ b/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md @@ -0,0 +1,513 @@ +# 11 - デバッグとトラブルシューティング + +この章では、APPLaunch の開発中およびデバイス配備時によくある問題を扱います。基本方針として、UI、アセット、入力ロジックの問題はまず SDL2 ビルドで再現し、その後デバイスログで framebuffer、evdev、権限、systemd の問題を切り分けます。 + +## 1. よく使うデバッグコマンド + +### 1.1 リポジトリとビルド状態を確認 + +```bash +cd /home/nihao/w2T/github/launcher + +git status --short +find docs/launcher工程详细说明 -maxdepth 1 -type f | sort +find projects/APPLaunch/APPLaunch -maxdepth 3 -type f | sort | sed -n '1,160p' +``` + +### 1.2 SDL2 をローカルでビルドして実行 + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed +./dist/M5CardputerZero-APPLaunch +``` + +用途: + +- ホームページ、組み込みページ、カルーセルアニメーション、`.desktop` スキャンを素早く確認する。 +- LVGL オブジェクト作成とアセットパスを確認する。 +- デバイス側 framebuffer、evdev、systemd 権限問題を避ける。 + +### 1.3 デバイス側 / クロスビルド + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed +``` + +デバイス上でネイティブビルドする場合: + +```bash +cd /path/to/launcher/projects/APPLaunch +CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed +``` + +### 1.4 APPLaunch 実行時ログを見る + +systemd で起動している場合: + +```bash +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager +journalctl --user -u APPLaunch.service -f +``` + +デバイスバイナリを手動実行する場合: + +```bash +systemctl --user stop APPLaunch.service +cd /usr/share/APPLaunch +sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch 2>&1 | tee /tmp/applaunch.log +``` + +実際のバイナリパスはパッケージングとインストール方法に依存します。ビルド出力 `projects/APPLaunch/dist/M5CardputerZero-APPLaunch` の場合もあります。 + +### 1.5 実行時アセットを確認 + +```bash +ls -l /usr/share/APPLaunch +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | sort | sed -n '1,120p' +find /usr/share/APPLaunch/share/audio -maxdepth 1 -type f | sort +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort +find /usr/share/APPLaunch/applications -maxdepth 1 -type f | sort +``` + +### 1.6 入力デバイスを確認 + +```bash +ls -l /dev/input/by-path/ +ls -l /dev/input/event* +sudo evtest +``` + +コード内のデフォルトキーボードデバイス: + +```text +/dev/input/by-path/platform-3f804000.i2c-event +``` + +環境変数で上書きできます。 + +```bash +APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +### 1.7 設定ファイルを確認 + +```bash +sudo ls -l /var/lib/applaunch +sudo cat /var/lib/applaunch/settings +``` + +よく使う設定キー: + +- `app_Game`, `app_Math`, `app_File`, `app_Camera` など: Launcher ページ表示トグル。 +- `brightness`: 輝度。 +- `volume`: 音量。 +- `dark_time`: 画面オフタイムアウト。 +- `cam_resolution`: カメラ解像度。 +- `startup_mode`: 起動モード。 +- `extport_usb`, `extport_5vout`: 拡張ポート設定。 +- `run_as_user`: 外部プロセスの権限を下げるときに使うユーザー。 + +## 2. ログキーワード早見表 + +| Keyword | Location | Meaning | +| --- | --- | --- | +| `[BOOT] lv_init() done` | `main.cpp` | LVGL 初期化完了 | +| `[BOOT] cp0_lvgl_init() starting...` | `main.cpp` | プラットフォーム適応層、表示、入力、音声などの初期化開始 | +| `[BOOT] First frame flushed to fb0.` | `main.cpp` | 最初のフレームを表示デバイスへ強制 flush 済み | +| `Entering main loop` | `main.cpp` | メインループ開始 | +| `[LAUNCHER] set panel icon` | `launch.cpp` | ホームアイコン設定成功 | +| `set panel icon missing/unreadable` | `launch.cpp` | アイコンパスが存在しない、または読めない | +| `applications_load: opendir failed` | `launch.cpp` | applications ディレクトリが存在しない、または読めない | +| `missing Name or Exec` | `launch.cpp` | `.desktop` に必須フィールドがない | +| `duplicate Exec` | `launch.cpp` | `.desktop` の Exec が既存アプリと同じ | +| `Launching terminal app` | `launch.cpp` | コマンド実行のため組み込みターミナルページへ入る | +| `Launching external app` | `launch.cpp` | 非ターミナル外部プログラムを開始 | +| `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | 外部アプリ実行中に親プロセスが ESC を読んだ | +| `[cp0] Returned to launcher` | `cp0_app_process.cpp` | 外部アプリが終了し、ホームへ戻る準備中 | +| `[HOME_STATUS] connected=` | `launch.cpp` | ホームステータスバーの WiFi/battery 状態を更新 | + +## 3. 黒画面のトラブルシューティング + +黒画面では、まずプロセスが起動していないのか、LVGL が最初のフレームを flush していないのか、アセット/ページ構築でクラッシュしたのか、外部アプリが framebuffer を占有しているのか、バックライト/輝度が間違っているのかを判断します。 + +### 3.1 プロセス状態を素早く確認 + +```bash +pgrep -a M5CardputerZero-APPLaunch +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager | tail -120 +``` + +プロセスがない場合: + +- systemd unit の `ExecStart` パスが存在するか確認します。 +- バイナリに実行権限があるか確認します。 +- バイナリを手動実行して stderr を確認します。 + +プロセスが繰り返し再起動している場合: + +```bash +journalctl --user -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' +``` + +### 3.2 起動ログの停止段階を確認 + +停止位置により調査方向が変わります。 + +| Stop point | Possible cause | Troubleshooting direction | +| --- | --- | --- | +| No `[BOOT] lv_init()` | プログラムが実行されていない、または非常に早期にクラッシュ | systemd、バイナリパス、動的ライブラリ、権限 | +| Stops at `cp0_lvgl_init() starting` | 表示/入力/音声/ハードウェア初期化で停止 | framebuffer、evdev、音声デバイス、hardware HAL | +| `ui_init done` appears but screen is black | first-frame flush 失敗、バックライト 0、アセットによりオブジェクト不可視 | framebuffer、バックライト、アセットパス | +| Black after entering main loop | ページ描画問題、または外部アプリが表示をロック | ログ、ロックファイル、外部プロセス | + +### 3.3 Framebuffer とバックライトを確認 + +```bash +ls -l /dev/fb0 +id +sudo cat /sys/class/backlight/backlight/brightness +sudo cat /sys/class/backlight/backlight/max_brightness +``` + +輝度を上げてみます。 + +```bash +echo 80 | sudo tee /sys/class/backlight/backlight/brightness +``` + +Settings ページが以前に非常に低い輝度を保存していた場合は確認します。 + +```bash +sudo grep '^brightness=' /var/lib/applaunch/settings +``` + +### 3.4 外部アプリが表示を占有していないか確認 + +APPLaunch が非ターミナル外部アプリを起動すると、自身の LVGL タイマーを停止し、子プロセス終了を待ちます。外部アプリがハングすると、Launcher が黒画面または無反応のように見える場合があります。 + +```bash +ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|Calculator|AppStore|my_app' | grep -v grep +``` + +まず外部アプリのプロセスグループを丁寧に終了するか、ESC を約 3 秒押し続けて `cp0_process_exec_blocking()` の SIGTERM を発火させます。 + +### 3.5 SDL2 で範囲を絞る + +SDL2 ビルドが動くがデバイスビルドが黒い場合は、デバイス HAL、framebuffer、バックライト、evdev、権限、systemd を優先して調べます。SDL2 も黒い場合は、UI 構築、アセットパス、LVGL オブジェクトスタイルを優先します。 + +## 4. アセット欠落のトラブルシューティング + +アセット欠落の典型症状は、空白アイコン、背景欠落、フォントフォールバック、音が出ない、ログの `missing/unreadable` です。 + +### 4.1 ソースと実行時ディレクトリの両方でアセットを確認 + +```bash +# Source tree +find projects/APPLaunch/APPLaunch/share/images -maxdepth 1 -type f | sort | grep my_icon + +# Device side +find /usr/share/APPLaunch/share/images -maxdepth 1 -type f | sort | grep my_icon +``` + +ソースツリーにはあるがデバイスにない場合: + +- 再ビルド、再パッケージ、再インストールします。 +- `projects/APPLaunch/main/SConstruct` に `STATIC_FILES += [ADir('../APPLaunch')]` が残っているか確認します。 +- パッケージングスクリプトが `APPLaunch/` アセットツリーをパッケージへコピーしているか確認します。 + +### 4.2 パス表記を確認 + +組み込みページでの推奨: + +```cpp +img_path("my_icon_100.png") +audio_path("key_enter.wav") +``` + +`.desktop` での推奨: + +```ini +Icon=share/images/my_icon_100.png +``` + +デバイス側ページに `/home/nihao/.../projects/APPLaunch/...` のような開発ホスト絶対パスを書かないでください。インストール後のデバイスアセットルートは `/usr/share/APPLaunch/` です。 + +### 4.3 画像パスの特殊ケースを理解する + +デバイス上では、`cp0_file_path("xxx.png")` は `share/images/xxx.png` を返します。これはカレントディレクトリからの相対パスです。想定外のディレクトリからデバイスバイナリを手動起動すると、画像が見つからないことがあります。作業ディレクトリを `/usr/share/APPLaunch` にして実行するか、正しい systemd `WorkingDirectory` を使ってください。 + +SDL2 では APPLaunch が `APPLaunch/share` を自動探索しますが、それでもビルド出力は `projects/APPLaunch` から実行することを推奨します。 + +### 4.4 フォント欠落 + +フォントファイルを確認: + +```bash +find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort +``` + +フォント追加後にクラッシュする、または表示されない場合: + +- 拡張子が `.ttf` または `.otf` であることを確認します。 +- FreeType ビルド依存関係が利用可能であることを確認します。 +- まず `Montserrat-Bold.ttf` や `AlibabaPuHuiTi-3-55-Regular.ttf` など既存フォントでページロジックを検証します。 + +## 5. 入力が動かない + +入力不具合には、ホームページが反応しない、組み込みページが反応しない、外部アプリが反応しない、キーコードマッピングが間違っている、などがあります。 + +### 5.1 ホームページまたは組み込みページが反応しない + +正しい入力グループがバインドされているか確認します。 + +- ホームページ: `UILaunchPage::bind_home_input_group()`。 +- 組み込みページ: ページ作成後、`launch.cpp` が `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())` を呼びます。 +- ホームへ戻る: `Launch::lv_go_back_home()` がホーム入力グループを再バインドします。 + +組み込みページにイベントを追加する場合、イベントが正しいオブジェクトに付けられており、そのオブジェクトがページ入力グループに属していることを確認してください。既存ページの `event_handler_init()` 実装を参照します。 + +### 5.2 デバイス evdev にイベントがあるか確認 + +```bash +ls -l /dev/input/by-path/platform-3f804000.i2c-event +sudo evtest /dev/input/by-path/platform-3f804000.i2c-event +``` + +デフォルトパスが存在しない場合、一時的に上書きします。 + +```bash +APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch +``` + +### 5.3 キーコードマッピング問題 + +関連ファイル: + +| File | Purpose | +| --- | --- | +| `ext_components/cp0_lvgl/include/compat/input_keys.h` | 互換入力キー定義 | +| `ext_components/cp0_lvgl/include/keyboard_input.h` | APPLaunch private input header | +| `ext_components/cp0_lvgl/include/keyboard_input.h` | cp0_lvgl input interface | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | デバイス側キーボード入力実装 | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | SDL2 キーボード入力実装 | + +調査方法: + +- SDL2 でまず矢印キー、Enter、Esc が期待どおり動くか確認します。 +- デバイスでは `evtest` で生のキーコードを読みます。 +- `LV_KEY_*` とプロジェクト独自キー値を比較します。 +- 外部アプリ実行中は `[CP0-APP] evdev code=... value=...` ログを確認します。 + +### 5.4 外部アプリの入力が動かない + +非ターミナル外部アプリ起動後、APPLaunch は `keyboard_pause()` で自身のキーボードスレッドを一時停止しますが、デバイスを EVIOCGRAB はしません。親子両方のプロセスが同じ evdev デバイスを読めます。外部アプリに入力がない場合: + +- 外部アプリが同じ `/dev/input/event*` を読んでいるか確認します。 +- 実行時ユーザーにその入力デバイスを読む権限があるか確認します。外部アプリは既定で root から通常ユーザーへ下げられる場合があります。 +- `run_as_user` 設定を確認するか、`run_as_root` を使う固定組み込み登録を使います。 +- まず `Terminal=true` で、コマンドが PTY 内でキーボード入力を受け取れるか確認します。 + +## 6. 外部アプリから戻れない + +外部アプリから戻れない原因は、多くの場合、子プロセスが終了しない、プロセスグループが kill されない、ESC が入力デバイスから読めない、またはアプリが表示を奪ったまま復元しないことです。 + +### 6.1 通常の復帰経路 + +`launch.cpp` の `launch_Exec()`: + +1. Loading を表示します。 +2. `LVGL_RUN_FLAGE = 0` を設定します。 +3. LVGL 入力グループを解除します。 +4. `lv_timer_enable(false)` で LVGL タイマーを一時停止します。 +5. `cp0_process_exec_blocking(exec, &LVGL_HOME_KEY_FLAG, keep_root)` を呼びます。 +6. 子プロセス終了後、タイマーを再有効化し、ホーム入力グループをバインドし、`launch_page_->show_home_screen()` でホーム画面を復元し、Loading を非表示にします。 + +### 6.2 まず子プロセスがまだ実行中か確認 + +```bash +ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|my_app|sh -c' | grep -v grep +``` + +子プロセスがまだ動いている場合: + +- アプリ自身に終了させます。 +- ESC を約 3 秒押し続けます。 +- ログに `[cp0] ESC held ... SIGTERM pgid ...` が含まれるか確認します。 + +### 6.3 ESC 長押しが効かない + +確認: + +```bash +journalctl --user -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' +``` + +`[CP0-APP] evdev` ログがない場合: + +- デフォルトキーボードパスが間違っている可能性があります。 +- 親プロセスに入力デバイス権限がない可能性があります。 +- 外部アプリまたは別プロセスが入力デバイスを排他的に使用している可能性があります。 + +ESC DOWN はあるが SIGTERM がない場合: + +- 長押し時間が足りません。現在のしきい値は 3 秒です。 +- キーコードが `KEY_ESC` ではありません。キーボードマッピングを確認します。 + +### 6.4 子は終了したがホームページが復元しない + +ログに次が含まれるか確認します。 + +```text +[cp0] Returned to launcher +App ... exited with code ... +``` + +復帰ログがあるのに画面が異常な場合: + +- 外部アプリが framebuffer 状態または terminal mode を変更した可能性があります。 +- APPLaunch はホーム切替後に `lv_obj_invalidate()` と `lv_refr_now()` で強制更新を試みています。それでも表示されない場合は framebuffer/backlight を確認します。 +- 外部アプリがロックやバックグラウンドプロセスを残し、表示を占有し続けていないか確認します。 + +## 7. ビルド失敗のトラブルシューティング + +### 7.1 SCons が SDK またはコンポーネントを見つけられない + +症状: `project.py`、components、headers が見つからない。 + +調査: + +```bash +cd /home/nihao/w2T/github/launcher/projects/APPLaunch +python3 - <<'PY' +from pathlib import Path +p = Path.cwd() +print('cwd=', p) +print('SDK=', p.parent.parent / 'SDK') +print('ext=', p.parent.parent / 'ext_components') +PY +ls ../../SDK/tools/scons/project.py +ls ../../ext_components/cp0_lvgl +``` + +APPLaunch の `SConstruct` は自動的に次を設定します。 + +```python +SDK_PATH = ../../SDK +EXT_COMPONENTS_PATH = ../../ext_components +``` + +### 7.2 SDL2 / FreeType / libinput 依存関係不足 + +SDL2 設定は `pkg-config` で `sdl2` と `freetype2` を探し、`input`、`xkbcommon`、`udev` をリンクします。 + +確認: + +```bash +pkg-config --cflags --libs sdl2 freetype2 +ldconfig -p | grep -E 'libinput|libxkbcommon|libudev' +``` + +一般的な Ubuntu/Debian 依存パッケージ名: + +```bash +sudo apt-get install scons pkg-config libsdl2-dev libfreetype-dev libinput-dev libxkbcommon-dev libudev-dev +``` + +### 7.3 クロスコンパイル sysroot 不足 + +`linux_x86_cross_cp0_config_defaults.mk` は `SDK/github_source/static_lib_v0.0.4` を sysroot として使います。存在しない場合、`SConstruct` は `sdk_bsp.tar.gz` のダウンロードを試みます。ネットワークアクセスが制限されていると失敗します。 + +確認: + +```bash +ls -l ../../SDK/github_source/static_lib_v0.0.4 +cat ../../SDK/github_source/static_lib_v0.0.4/version 2>/dev/null || true +``` + +ダウンロードに失敗する場合は、事前に sysroot を用意するか、ネットワークが利用できる環境で一度ビルドしてキャッシュを作成してください。 + +### 7.4 ページ追加後にビルドが失敗する + +よくある原因: + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `PageT not declared` | ページクラス名と登録名が一致しない、または `.hpp` が `generated/page_app.h` に include されていない | `generated/page_app.h` を確認し、scons を再実行 | +| SDL2 build cannot find Linux headers | ページがデバイス専用ヘッダーを直接 include している | デバイス専用コードを `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` で囲む | +| Linker cannot find symbols | 新しいページが呼ぶ関数がコンポーネント依存関係に追加されていない | `main/SConstruct` の `REQUIREMENTS`/`LDFLAGS` を確認 | +| Duplicate definition | header-only ページが非 inline のグローバル変数/関数を定義している | クラスメンバー、`static`、`inline` にするか `.cpp` へ移動 | + +### 7.5 `generated/page_app.h` 自動生成で作業ツリーが変わる + +`generate_page_app_includes.py` はファイル名順に `generated/page_app.h` を生成します。`page_app/*.hpp` を追加または削除した後にビルドすると、このファイルが変更される場合があります。これは期待される挙動ですが、コミット前に diff が意図した include-list 変更だけであることを確認してください。 + +## 8. `.desktop` 読み込み失敗のトラブルシューティング + +### 8.1 ファイルがスキャンされない + +確認: + +```bash +ls -l /usr/share/APPLaunch/applications +``` + +要件: + +- ファイル名が `.desktop` で終わる。 +- 内容に `[Desktop Entry]` を含む。 +- 少なくとも `Name=` と `Exec=` が必要。 +- 空行と `#` または `;` で始まるコメントはスキップされる。 + +### 8.2 重複排除でアプリがスキップされた + +ログに次が含まれる場合: + +```text +applications_load: skip ... (duplicate Exec) +``` + +既存の固定アプリまたは別の `.desktop` が同じ `Exec` を使っています。`Exec` を一意のコマンドに変更してください。 + +### 8.3 アイコンが表示されない + +`.desktop` の `Icon` フィールドは自動的に `img_path()` を呼ばず、そのまま `panel_set_icon()` へ渡されます。そのため、次を使います。 + +```ini +Icon=share/images/my_app_100.png +``` + +絶対パスを使う場合も、デバイス上にファイルが存在し読めることを確認してください。 + +### 8.4 コマンド実行に失敗した + +ターミナルアプリでは、まずコマンドラインで確認します。 + +```bash +which vim +vim --version +``` + +非ターミナルアプリでは次を確認します。 + +```bash +ls -l /usr/share/APPLaunch/bin/my_app +ldd /usr/share/APPLaunch/bin/my_app +sudo -u pi /usr/share/APPLaunch/bin/my_app +``` + +APPLaunch が root として起動する場合、外部アプリは通常、通常ユーザーへ権限を下げようとします。root が必要なアプリは `run_as_root` を使う固定組み込みエントリとして登録するか、不要な root 権限を避けられるようプログラム権限/グループ権限を調整してください。 + +## 9. 推奨される問題切り分け順序 + +1. `git status --short` を実行し、現在の変更範囲を確認します。 +2. SDL2 をビルドして実行し、基本的な UI/構文問題を除外します。 +3. アセットが `projects/APPLaunch/APPLaunch` とデバイス `/usr/share/APPLaunch` の両方に存在するか確認します。 +4. `journalctl --user -u APPLaunch.service -f` を見て起動段階を特定します。 +5. `evtest` で入力デバイスとキーコードを確認します。 +6. `ps` で外部アプリとプロセスグループを確認します。 +7. `/var/lib/applaunch/settings` を確認し、設定トグル、輝度、実行時ユーザー問題を除外します。 +8. 最後に `ext_components/cp0_lvgl/src/cp0/` 配下の HAL 層を確認します。framebuffer、keyboard、process、settings、audio 実装が対象です。 diff --git a/docs/launcher-project-guide-ja/12-common-modification-entry-points.md b/docs/launcher-project-guide-ja/12-common-modification-entry-points.md new file mode 100644 index 00000000..29fbd948 --- /dev/null +++ b/docs/launcher-project-guide-ja/12-common-modification-entry-points.md @@ -0,0 +1,215 @@ +# 12 - よく使う変更入口 + +この章では、「何を変更したいか」に応じて APPLaunch の一般的な入口を整理します。変更前には、他の agent の変更を上書きしないよう、まず現在の作業ツリー状態を確認してください。 + +```bash +cd /home/nihao/w2T/github/launcher +git status --short +``` + +## 1. 高頻度タスク入口表 + +| Task | Main files/directories | Key points | Verification | +| --- | --- | --- | --- | +| 組み込みページを追加 | `projects/APPLaunch/main/ui/page_app/` | `ui_app_xxx.hpp` を作成し、`AppPage` を継承 | SDL2 でビルドし、ページを開く | +| ホームに組み込みページを登録 | `projects/APPLaunch/main/ui/launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | ホームカルーセルにアイコンが表示される | +| 組み込みページ表示トグルを制御 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/launch.cpp` | Settings ページが `app_Key` を書き、Launcher が `APP_ENABLED("Key")` を読む | Settings で切替後、再起動またはホーム更新 | +| 外部 `.desktop` アプリを追加 | `projects/APPLaunch/APPLaunch/applications/` | ファイル名は `.desktop` で終わり、`Name` と `Exec` を含む必要がある | skip ログなしでホームに表示される | +| アイコンを追加 | `projects/APPLaunch/APPLaunch/share/images/` | 組み込みページは `img_path()`、`.desktop` は `Icon=share/images/xxx.png` を使う | `missing/unreadable` ログがない | +| 効果音を追加 | `projects/APPLaunch/APPLaunch/share/audio/` | ページは `audio_path()` と `cp0_signal_audio_api()` を使う | デバイスで音が鳴る | +| フォントを追加 | `projects/APPLaunch/APPLaunch/share/font/` | `launcher_fonts().get()` を使い、FreeType 依存を確認 | ページテキストが新しいフォントを使う | +| ホームカルーセルレイアウトを変更 | `projects/APPLaunch/main/ui/ui_launch_page.cpp`, `projects/APPLaunch/main/ui/ui_launch_page.h` | 5 slots、左右切替、中央カード | SDL2 でアニメーションと入力を確認 | +| カルーセルアニメーションを変更 | `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | カード移動、scale、opacity などのアニメーション | SDL2 で左右切替を繰り返す | +| ホームステータスバーを変更 | `projects/APPLaunch/main/ui/launch.cpp`, `projects/APPLaunch/main/ui/ui.cpp` | `update_home_status_bar()` が WiFi/time/battery を更新 | `[HOME_STATUS]` ログを確認 | +| Settings メニューを変更 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `menu_init()` に `MenuItem`/`SubItem` を追加 | SETTING ページに入りテスト | +| 設定保存ロジックを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 現在は `/var/lib/applaunch/settings` に保存、最大 32 エントリ | settings ファイルを確認 | +| アセットパスルールを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | デバイスと SDL2 の整合性を考慮 | SDL2 とデバイスの両方でアセット確認 | +| 外部アプリ起動/復帰を変更 | `projects/APPLaunch/main/ui/launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | 外部アプリ起動、ESC で戻る | +| ターミナルアプリを変更 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、コマンド実行、入出力 | `Terminal=true` アプリで確認 | +| 入力マッピングを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | デバイスと SDL2 の入力差分 | `evtest` + SDL2 keyboard | +| 起動フローを変更 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、main loop | `[BOOT]` ログを確認 | +| ビルド依存関係を変更 | `projects/APPLaunch/main/SConstruct` | `SRCS`, `INCLUDE`, `REQUIREMENTS`, `STATIC_FILES` | scons build | +| ビルド設定を変更 | `projects/APPLaunch/*.mk` | SDL2、device、cross build ごとに異なる設定 | 特定の `CONFIG_DEFAULT_FILE` でビルド | +| パッケージ内容を変更 | `scripts/debian_packager.py`, `projects/APPLaunch/APPLaunch/` | アセットツリーとインストールパス | パッケージ作成後のファイル一覧を確認 | +| platform HAL を変更 | `ext_components/cp0_lvgl/src/cp0/`, `ext_components/cp0_lvgl/include/hal/` | framebuffer、audio、network、settings、process など | デバイスでテスト | + +## 2. ソースディレクトリ早見表 + +| Path | Purpose | +| --- | --- | +| `projects/APPLaunch/main/src/main.cpp` | APPLaunch プロセス入口、初期化順序、メインループ、外部アプリロック検出 | +| `projects/APPLaunch/main/ui/ui.cpp` | グローバル LVGL UI オブジェクトを作成。多くの `ui_*` グローバルはここから来る | +| `projects/APPLaunch/main/ui/ui.cpp` | C++ UI 初期化ブリッジ | +| `projects/APPLaunch/main/ui/ui.h` | UI グローバル宣言と C/C++ 共通インターフェース | +| `projects/APPLaunch/main/ui/launch.cpp` | アプリモデル、アプリリスト、起動ロジック、動的 `.desktop` 読み込み、ステータスバー更新 | +| `projects/APPLaunch/main/ui/launch.h` | `Launch` の public wrapper class | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | ホーム画面、カルーセルスロット、入力イベント、ホームページ挙動 | +| `projects/APPLaunch/main/ui/ui_launch_page.h` | ホームクラスインターフェース。panel/label/input group accessor を含む | +| `projects/APPLaunch/main/ui/ui_loading.cpp` | Loading オーバーレイ表示/非表示 | +| `projects/APPLaunch/main/ui/ui_global_hint.cpp` | グローバルヒントオーバーレイ | +| `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp` | LVGL OS/thread helpers | +| `projects/APPLaunch/main/ui/animation/` | ホームカルーセルアニメーション実装 | +| `projects/APPLaunch/main/ui/ui_app_page.hpp` | 組み込みページ基底クラス、トップバー、共有アセットパス helper | +| `projects/APPLaunch/build/generated/include/generated/page_app.h` | 自動生成される組み込みページ include 集約 | +| `projects/APPLaunch/main/ui/page_app/` | 組み込みページ実装ディレクトリ | +| `ext_components/cp0_lvgl/include/` | 共有 CP0/LVGL ヘッダー。keyboard と互換 input ヘッダーを含む | + +## 3. 組み込みページ入口表 + +| Page/feature | File | Registered name or icon | Description | +| --- | --- | --- | --- | +| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 組み込みゲームエントリ | +| SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings ページ。app toggles、brightness、volume、WiFi、camera などを含む | +| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 組み込みゲームエントリ | +| Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass ページ | +| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_ip_panel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP 情報パネル。デバイスで有効 | +| FILE | `projects/APPLaunch/main/ui/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | File ページ。デバイスで有効 | +| SSH | `projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH ページ。デバイスで有効 | +| MESH | `projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh ページ。デバイスで有効 | +| REC | `projects/APPLaunch/main/ui/page_app/ui_app_rec.hpp` | `REC` / `rec_100.png` | 録音ページ。デバイスで有効 | +| CAMERA | `projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | Camera ページ。デバイスで有効 | +| LORA | `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa ページ。デバイスで有効 | +| TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game。デバイスで有効 | +| CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`。bash、python、`Terminal=true` アプリで使用 | + +`Launch::Launch()` の固定登録エントリ: + +```cpp +app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); +app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); +app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +``` + +## 4. 外部アプリ入口表 + +| Item | Path/function | Description | +| --- | --- | --- | +| `.desktop` directory | `projects/APPLaunch/APPLaunch/applications/` | 開発ツリー。`/usr/share/APPLaunch/applications/` としてパッケージ化 | +| Template | `projects/APPLaunch/APPLaunch/applications/vim.desktop.temple` | サンプルテンプレート。サフィックスが `.desktop` ではないためスキャンされない | +| Scan function | `Launch::applications_load()` in `projects/APPLaunch/main/ui/launch.cpp` | `[Desktop Entry]`、`Name`、`Icon`、`Exec`、`Terminal`、`Sysplause` を解析 | +| Directory watching | `Launch::inotify_init_watch()`, `app_dir_watch_cb()` | アプリケーション変更を監視し、動的アプリリストを更新 | +| Dynamic refresh | `Launch::applications_reload()` | 固定アプリを保持し、動的アプリを削除してから再スキャン | +| Terminal launch | `Launch::launch_Exec_in_terminal()` | `UIConsolePage` を作成してコマンドを実行 | +| Non-terminal launch | `Launch::launch_Exec()` | LVGL を一時停止し、`cp0_process_exec_blocking()` を呼ぶ | +| Device-side process execution | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | fork、権限降格、長押し ESC による終了、キーボード復元 | +| PTY execution | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | ターミナルページのコマンド実行とユーザー選択 | + +最小 `.desktop` テンプレート: + +```ini +[Desktop Entry] +Name=MyApp +Exec=/usr/share/APPLaunch/bin/my_app +Terminal=false +Icon=share/images/my_app_100.png +Type=Application +``` + +## 5. アセット入口表 + +| Asset | Development path | Access method | Common issue | +| --- | --- | --- | --- | +| Home/app icons | `projects/APPLaunch/APPLaunch/share/images/` | `img_path("xxx.png")` | デバイス側相対パスは作業ディレクトリに依存。`.desktop` は `share/images/xxx.png` を使うべき | +| Page images | `projects/APPLaunch/APPLaunch/share/images/` | `img_path("xxx.png")` または `cp0_file_path_c("xxx.png")` | ファイル名の大文字小文字は完全一致が必要 | +| Audio | `projects/APPLaunch/APPLaunch/share/audio/` | `audio_path("xxx.wav")` | デバイス側音声パスは絶対パス `/usr/share/APPLaunch/share/audio/` | +| Fonts | `projects/APPLaunch/APPLaunch/share/font/` | `launcher_fonts().get("xxx.ttf", size, style)` | FreeType が必要。font object はキャッシュして再利用すべき | +| External binaries/scripts | `projects/APPLaunch/APPLaunch/bin/` | `.desktop` `Exec=/usr/share/APPLaunch/bin/xxx` | 実行権限と動的ライブラリ依存に注意 | +| External app descriptors | `projects/APPLaunch/APPLaunch/applications/` | `.desktop` を自動スキャン | `.desktop.temple` はスキャンされない | +| Packaged libraries | `projects/APPLaunch/APPLaunch/lib/` | プログラムまたはスクリプトから読み込み | 実行時の `LD_LIBRARY_PATH` または rpath に注意 | + +パス解決コード: + +| Platform | File | Focus | +| --- | --- | --- | +| Device | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | アセットルートは `/usr/share/APPLaunch` 固定。画像は相対 `share/images/` パスを返す | +| SDL2 | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | 実行ファイルディレクトリ、カレントディレクトリ、親ディレクトリから `APPLaunch/share` を推定 | +| C interface | `cp0_file_path_c()` | thread-local にキャッシュされた `const char *` を返し、LVGL style API に適する | +| C++ interface | `cp0_file_path()` | `std::string` を返す。ページ内で推奨 | + +## 6. Settings と永続化入口表 + +| Setting item | UI entry | Configuration key | Implementation location | +| --- | --- | --- | --- | +| App visibility toggle | SETTING -> Launcher | `app_` | `ui_app_setup.hpp` の `save_app_toggle()`、`launch.cpp` の `APP_ENABLED()` | +| Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | +| Screen-off timeout | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | +| Volume | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`, `cp0_volume_read/write()` | +| Camera resolution | SETTING -> Camera -> Resolution | `cam_resolution` | `ui_app_setup.hpp`。camera page が読み取る | +| Startup mode | Settings page の関連選択 | `startup_mode` | `ui_app_setup.hpp` | +| USB extension port | SETTING -> ExtPort | `extport_usb` | `ui_app_setup.hpp` | +| 5V output | SETTING -> ExtPort | `extport_5vout` | `ui_app_setup.hpp` | +| External app runtime user | 手動設定 | `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | + +設定実装: + +| File | Description | +| --- | --- | +| `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` | `cp0_config_get_int/set_int/get_str/set_str/save` の宣言 | +| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | デバイス側設定 read/write。`/var/lib/applaunch/settings` に保存 | +| `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` | SDL2 互換実装 | +| `ext_components/cp0_lvgl/src/commount.c` | 起動時に保存済み輝度と音量を適用 | + +## 7. ビルド入口表 + +| Scenario | Command/file | Description | +| --- | --- | --- | +| Default SDL2 build | `projects/APPLaunch/SConstruct` が自動的に `linux_x86_sdl2_config_defaults.mk` を選択 | x86_64 開発ホストの既定設定 | +| Explicit SDL2 build | `CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | ローカル開発確認に推奨 | +| Cross build | `CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | x86 Linux から AArch64 デバイスへ | +| Native device build | `CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed` | デバイス側 framebuffer/evdev 設定 | +| macOS cross build | `CONFIG_DEFAULT_FILE=mac_cross_cp0_config_defaults.mk scons ...` | macOS からデバイスへ | +| macOS/Darwin | `darwin_config_defaults.mk` | Darwin/SDL 関連設定 | +| Main build script | `projects/APPLaunch/SConstruct` | SDK、EXT_COMPONENTS、sysroot download を設定 | +| Component build script | `projects/APPLaunch/main/SConstruct` | sources、dependencies、static files、git commit macro | +| APPLaunch configuration | `projects/APPLaunch/main/Kconfig` | メインプロジェクト Kconfig | +| cp0_lvgl configuration | `ext_components/cp0_lvgl/Kconfig` | プラットフォーム適応コンポーネント設定 | + +## 8. プラットフォーム適応入口表 + +| Capability | Header | Device implementation | SDL2/compat implementation | +| --- | --- | --- | --- | +| LVGL initialization | `ext_components/cp0_lvgl/include/hal_lvgl_bsp.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl.c` | +| framebuffer/display | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c` | +| Keyboard input | `ext_components/cp0_lvgl/include/keyboard_input.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | +| File paths | `ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | +| Process | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp` | +| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp` | +| Audio | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c` | +| Settings/brightness/volume | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp` | +| Network/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp` | +| Screenshot | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp` | +| Camera | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp` | Device camera | `ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp` | + +## 9. デバッグコマンド早見表 + +| Purpose | Command | +| --- | --- | +| 現在の変更を見る | `git status --short` | +| SDL2 build | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | +| SDL2 run | `cd projects/APPLaunch && ./dist/M5CardputerZero-APPLaunch` | +| Cross build | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | +| systemd status を見る | `systemctl --user status APPLaunch.service --no-pager` | +| ログを追跡 | `journalctl --user -u APPLaunch.service -f` | +| boot logs を見る | `journalctl --user -u APPLaunch.service -b --no-pager` | +| アセット確認 | `find /usr/share/APPLaunch -maxdepth 3 -type f | sort` | +| `.desktop` ファイル確認 | `find /usr/share/APPLaunch/applications -maxdepth 1 -type f -name '*.desktop' -print -exec sed -n '1,80p' {} \;` | +| settings 確認 | `sudo cat /var/lib/applaunch/settings` | +| 入力デバイス確認 | `ls -l /dev/input/by-path/ && sudo evtest` | +| 外部アプリプロセス確認 | `ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|sh -c|M5CardputerZero'` | +| 動的ライブラリ確認 | `ldd /usr/share/APPLaunch/bin/my_app` | +| アイコンログ確認 | `journalctl --user -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | + +## 10. 変更前後チェックリスト + +| Stage | Check item | +| --- | --- | +| Before change | `git status --short` を実行し、他者の既存変更がどのファイルにあるか確認 | +| After adding a page | `.hpp` ファイルが `page_app/` にあり、クラス名が `launch.cpp` の登録と一致することを確認 | +| After adding assets | ファイルがソースツリーとデバイス `/usr/share/APPLaunch` の両方で見つかることを確認 | +| After adding `.desktop` | ファイルサフィックスが `.desktop` で、`[Desktop Entry]`、`Name`、`Exec` を含む | +| After changing settings | `/var/lib/applaunch/settings` に正しいキーが含まれ、設定エントリ上限を超えていない | +| After build | SDL2 またはクロスビルドが成功し、予期しない自動生成 diff がない | +| After running on device | `journalctl` に missing、skip、segfault、permission denied メッセージがない | +| After external app changes | アプリが正常終了する、または長押し ESC でホームへ戻る | diff --git a/docs/launcher-project-guide-ja/launcher-project-guide.md b/docs/launcher-project-guide-ja/launcher-project-guide.md new file mode 100644 index 00000000..26b07404 --- /dev/null +++ b/docs/launcher-project-guide-ja/launcher-project-guide.md @@ -0,0 +1,29 @@ +# Launcher プロジェクトガイド + +このドキュメントセットは `launcher` リポジトリ、特に `projects/APPLaunch` ランチャープロジェクトを中心に、アーキテクチャ、ソースフレームワーク、ビルドフロー、実行時の動作、パッケージング、デプロイ、拡張ポイント、トラブルシューティングを説明します。 + +推奨する読み進め方: + +1. [00 - Overview and Reading Path](00-overview-and-reading-path.md) +2. [01 - Project Layout and Module Responsibilities](01-project-layout-and-module-responsibilities.md) +3. [02 - Runtime Framework and Boot Flow](02-runtime-framework-and-boot-flow.md) +4. [03 - UI Framework and Home Carousel](03-ui-framework-and-home-carousel.md) +5. [04 - Application Model and Launch Mechanism](04-application-model-and-launch-mechanism.md) +6. [05 - Built-in Page Framework](05-built-in-page-framework.md) +7. [06 - Resources and Configuration](06-resources-and-configuration.md) +8. [07 - Input System and Key Mapping](07-input-system-and-key-mapping.md) +9. [08 - Build and Compilation Guide](08-build-and-compilation-guide.md) +10. [09 - Packaging, Deployment, and systemd](09-packaging-deployment-and-systemd.md) +11. [10 - Extension Development Guide](10-extension-development-guide.md) +12. [11 - Debugging and Troubleshooting](11-debugging-and-troubleshooting.md) +13. [12 - Common Modification Entry Points](12-common-modification-entry-points.md) + +## Quick Links + +- ビルドして実行するには: [08 - Build and Compilation Guide](08-build-and-compilation-guide.md) を参照してください。 +- アプリの起動方法を理解するには: [04 - Application Model and Launch Mechanism](04-application-model-and-launch-mechanism.md) を参照してください。 +- ホーム UI を変更するには: [03 - UI Framework and Home Carousel](03-ui-framework-and-home-carousel.md) を参照してください。 +- 組み込みページを追加するには: [10 - Extension Development Guide](10-extension-development-guide.md) を参照してください。 +- 黒画面や起動失敗をデバッグするには: [11 - Debugging and Troubleshooting](11-debugging-and-troubleshooting.md) を参照してください。 + +English version: [Launcher Project Guide](../launcher-project-guide.md) | 中文版: [Launcher 工程详细说明](../launcher工程详细说明.md) diff --git a/docs/launcher-project-guide/00-overview-and-reading-path.md b/docs/launcher-project-guide/00-overview-and-reading-path.md index 3cf3c668..f8cb8a18 100644 --- a/docs/launcher-project-guide/00-overview-and-reading-path.md +++ b/docs/launcher-project-guide/00-overview-and-reading-path.md @@ -68,8 +68,8 @@ If you only want to complete a specific task: | --- | --- | | `projects/APPLaunch` | Main launcher project | | `projects/APPLaunch/main/src/main.cpp` | APPLaunch entry point and LVGL main loop | -| `projects/APPLaunch/main/ui/Launch.cpp` | Application list, launch logic, status bar refresh | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home UI, carousel, home key handling | +| `projects/APPLaunch/main/ui/launch.cpp` | Application list, launch logic, status bar refresh | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Home UI, carousel, home key handling | | `projects/APPLaunch/main/ui/page_app` | Built-in page implementations | | `projects/APPLaunch/APPLaunch` | Resource tree packaged into the runtime environment | | `ext_components/cp0_lvgl` | Platform adaptation layer that wraps file, process, input, and system interfaces | diff --git a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md index 8851e1e3..f9b590aa 100644 --- a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md +++ b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md @@ -167,27 +167,27 @@ projects/APPLaunch/main/ ```text main/ui/ ├── ui.cpp / ui.h -├── Launch.cpp / Launch.h -├── UILaunchPage.cpp / UILaunchPage.h +├── launch.cpp / launch.h +├── ui_launch_page.cpp / ui_launch_page.h ├── ui_app_page.hpp -├── page_app.h +├── generated/page_app.h ├── generate_page_app_includes.py ├── ui_loading.* ├── ui_global_hint.* -├── zero_lvgl_os.* -├── Animation/ +├── LauncherUiRuntime.* +├── animation/ └── page_app/ ``` | File/Directory | Role | | --- | --- | | `ui.c` / `ui.cpp` / `ui.h` | UI initialization, global objects, and the C/C++ bridge | -| `Launch.cpp` | Application manager; implements application list, launch, status bar refresh, and directory watching | -| `UILaunchPage.cpp` | Home UI creation, carousel slots, key handling, and startup animation | +| `launch.cpp` | Application manager; implements application list, launch, status bar refresh, and directory watching | +| `ui_launch_page.cpp` | Home UI creation, carousel slots, key handling, and startup animation | | `ui_loading.cpp` | Loading overlay | | `ui_global_hint.cpp` | Global hints | -| `zero_lvgl_os.cpp` | LVGL OS/thread-related helpers | -| `Animation/` | Home carousel animation implementation | +| `LauncherUiRuntime.cpp` | LVGL OS/thread-related helpers | +| `animation/` | Home carousel animation implementation | | `components/` | Page base classes, components, and custom pages | ### 2.5 `components/page_app/` Built-In Page Directory @@ -201,12 +201,12 @@ main/ui/page_app/ ├── ui_app_game.hpp ├── ui_app_lora.hpp ├── ui_app_mesh.hpp -├── ui_app_music.hpp +├── ui_app_game.hpp ├── ui_app_rec.hpp ├── ui_app_setup.hpp ├── ui_app_ssh.hpp ├── ui_app_tank_battle.hpp -└── ui_app_IpPanel.hpp +└── ui_app_ip_panel.hpp ``` These pages are usually implemented header-only so they can be automatically included by `generate_page_app_includes.py`. @@ -228,7 +228,7 @@ ui_init() ├── ui_loading └── page_app/* -LaunchImpl +Launch ├── UILaunchPage::panel()/label() ├── page_v ├── cp0_file_path() diff --git a/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md b/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md index 6a946d99..96b720f8 100644 --- a/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md +++ b/docs/launcher-project-guide/02-runtime-framework-and-boot-flow.md @@ -1,6 +1,6 @@ # 02 - Runtime Framework and Boot Flow -This chapter explains the full path from the APPLaunch process entry point to the first frame of the home screen. Key references are `projects/APPLaunch/main/src/main.cpp`, `projects/APPLaunch/main/ui/ui.cpp`, `projects/APPLaunch/main/ui/zero_lvgl_os.cpp`, and `projects/APPLaunch/main/ui/UILaunchPage.cpp`. +This chapter explains the full path from the APPLaunch process entry point to the first frame of the home screen. Key references are `projects/APPLaunch/main/src/main.cpp`, `projects/APPLaunch/main/ui/ui.cpp`, `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp`, and `projects/APPLaunch/main/ui/ui_launch_page.cpp`. ## 1. Runtime Framework Overview @@ -18,17 +18,17 @@ APPLaunch process │ ├── lv_timer_handler() │ └── usleep(5000) └── ui_init() - └── zero_lvgl_os() - ├── creat_display() + └── LauncherUiRuntime() + ├── create_display() ├── Create Launch / UILaunchPage bound objects - └── create_launcher_home() + └── build_launcher_home() ``` Core characteristics: - LVGL initialization and platform adaptation initialization are executed only once in `main()`. -- The home UI is created under the control of `zero_lvgl_os`; the actual objects are created in `UILaunchPage::create_screen()`. -- `Launch` / `LaunchImpl` is responsible for the application list, launch modes, status bar refresh, and dynamic application directory watching. +- The home UI is created under the control of `LauncherUiRuntime`; the actual objects are created in `UILaunchPage::create_screen()`. +- `Launch` / `Launch` is responsible for the application list, launch modes, status bar refresh, and dynamic application directory watching. - Immediately after `ui_init()`, the first home frame is forced to refresh through `lv_obj_invalidate()` + `lv_refr_now(NULL)`, avoiding a black screen while waiting for the next natural refresh after startup. ## 2. Entry Files and Key Source Paths @@ -36,10 +36,10 @@ Core characteristics: | Path | Role | | --- | --- | | `projects/APPLaunch/main/src/main.cpp` | Process entry point, LVGL main loop, and external-application runtime lock detection | -| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`, creates the global `zero_lvgl_os home` | -| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | Sets the LVGL theme, creates the home screen, and creates Launch bound objects | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home screen, startup GIF, home loading, and input group | -| `projects/APPLaunch/main/ui/Launch.cpp` | Application manager; launches external/terminal/built-in pages and owns the status bar timer | +| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`, creates the global `LauncherUiRuntime home` | +| `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp` | Sets the LVGL theme, creates the home screen, and creates Launch bound objects | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Home screen, startup GIF, home loading, and input group | +| `projects/APPLaunch/main/ui/launch.cpp` | Application manager; launches external/terminal/built-in pages and owns the status bar timer | | `ext_components/cp0_lvgl` | Wrappers for `cp0_lvgl_init()`, file paths, input, processes, and system capabilities | ## 3. `main()` Boot Flow @@ -110,41 +110,41 @@ Each loop iteration `ui_init()` is located in `projects/APPLaunch/main/ui/ui.cpp`: ```cpp -std::unique_ptr home; +std::unique_ptr home; void ui_init(void) { - home = std::make_unique(); + home = std::make_unique(); } ``` -The `zero_lvgl_os` constructor continues with: +The `LauncherUiRuntime` constructor continues with: ```cpp -zero_lvgl_os::zero_lvgl_os() +LauncherUiRuntime::LauncherUiRuntime() { - creat_display(); + create_display(); launch_ = std::make_shared(); launch_page_ = std::make_shared(launch_); launch_->set_launch_page(launch_page_); - create_launcher_home(); + build_launcher_home(); } ``` Pay attention to the order here: -1. `creat_display()` first creates the font manager and sets the LVGL theme. +1. `create_display()` first creates the font manager and sets the LVGL theme. 2. It constructs `Launch` and `UILaunchPage`, then establishes the two-way collaboration relationship through `Launch::set_launch_page()`. -3. `create_launcher_home()` creates the home screen, calls `Launch::bind_ui()` to build the application list, initializes the input group, and displays either the home screen or the startup GIF. +3. `build_launcher_home()` creates the home screen, calls `Launch::bind_ui()` to build the application list, initializes the input group, and displays either the home screen or the startup GIF. ## 5. Display / Theme Initialization -The core code of `zero_lvgl_os::creat_display()`: +The core code of `LauncherUiRuntime::create_display()`: ```cpp -void zero_lvgl_os::creat_display() +void LauncherUiRuntime::create_display() { fonts_ = std::make_shared(); @@ -163,14 +163,14 @@ Notes: - `LauncherFonts` is the FreeType font cache shared by the home screen and pages. Its entry function is `launcher_fonts()`. - `lv_disp_get_default()` depends on `cp0_lvgl_init()` having already registered the display device. -- The theme is only the base theme. Most home controls still have their sizes, colors, background images, and fonts set manually in `UILaunchPage.cpp`. +- The theme is only the base theme. Most home controls still have their sizes, colors, background images, and fonts set manually in `ui_launch_page.cpp`. ## 6. Home Creation and Display Flow -`zero_lvgl_os::create_launcher_home()` is the main entry point for displaying the home screen: +`LauncherUiRuntime::build_launcher_home()` is the main entry point for displaying the home screen: ```cpp -void zero_lvgl_os::create_launcher_home() +void LauncherUiRuntime::build_launcher_home() { LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); @@ -273,21 +273,21 @@ main() -> cp0_lvgl_init() -> register LV_EVENT_KEYBOARD -> ui_init() - -> new zero_lvgl_os - -> creat_display() + -> new LauncherUiRuntime + -> create_display() -> new LauncherFonts -> lv_disp_get_default() -> lv_theme_default_init() -> new Launch -> new UILaunchPage(Launch) -> Launch::set_launch_page() - -> create_launcher_home() + -> build_launcher_home() -> register LV_EVENT_GET_COMP_CHILD -> launch_page_->create_screen() -> home_base::creat_Top_UI() -> create_app_container(content_container()) -> launch_->bind_ui() - -> new LaunchImpl + -> new Launch -> Register fixed/dynamic applications and write them into home slots -> Create status bar and application directory watch timers -> launch_page_->init_input_group() diff --git a/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md b/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md index 8796c38b..74d69d6a 100644 --- a/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md +++ b/docs/launcher-project-guide/03-ui-framework-and-home-carousel.md @@ -1,6 +1,6 @@ # 03 - UI Framework and Home Carousel -This chapter explains how the APPLaunch home UI is organized, how data flows through the carousel cards, and how key events are handled. Key references are `projects/APPLaunch/main/ui/UILaunchPage.cpp`, `projects/APPLaunch/main/ui/UILaunchPage.h`, `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp`, and `projects/APPLaunch/main/ui/Launch.cpp`. +This chapter explains how the APPLaunch home UI is organized, how data flows through the carousel cards, and how key events are handled. Key references are `projects/APPLaunch/main/ui/ui_launch_page.cpp`, `projects/APPLaunch/main/ui/ui_launch_page.h`, `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp`, and `projects/APPLaunch/main/ui/launch.cpp`. ## 1. UI Framework Overview @@ -22,16 +22,16 @@ UILaunchPage : home_base └── 5 page dots ``` -Home uses the common `home_base` / `AppPageRoot` page framework for the root screen, status bar, and input group. `UILaunchPage.cpp` fills the inherited content container with the carousel and wires the LVGL callbacks. +Home uses the common `home_base` / `AppPageRoot` page framework for the root screen, status bar, and input group. `ui_launch_page.cpp` fills the inherited content container with the carousel and wires the LVGL callbacks. ## 2. Key Source Paths | Path | Description | | --- | --- | -| `projects/APPLaunch/main/ui/UILaunchPage.h` | Home class definition, carousel element enum, and `carousel_elements` array | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home screen creation, carousel switching, keyboard events, startup GIF, and font cache | -| `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | Carousel left/right switch animation | -| `projects/APPLaunch/main/ui/Launch.cpp` | Fills new card content after switching, launches the current application, and refreshes the status bar | +| `projects/APPLaunch/main/ui/ui_launch_page.h` | Home class definition, carousel element enum, and `carousel_elements` array | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Home screen creation, carousel switching, keyboard events, startup GIF, and font cache | +| `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | Carousel left/right switch animation | +| `projects/APPLaunch/main/ui/launch.cpp` | Fills new card content after switching, launches the current application, and refreshes the status bar | | `projects/APPLaunch/main/ui/ui.h` | Home layout constants such as `LABEL_Y_CENTER` and `BORDER_COLOR_CENTER` | ## 3. Responsibilities of `UILaunchPage` @@ -84,8 +84,8 @@ private: It has two categories of responsibilities: -- Static compatibility responsibilities: keep the shared `carousel_elements` array, maintain the home input group bridge, and provide `panel()` / `label()` accessors used by `Launch.cpp`. -- Instance responsibilities: hold the `Launch` pointer, own per-page UI state, handle LVGL events, and forward carousel updates / app launches to `LaunchImpl`. +- Static compatibility responsibilities: keep the shared `carousel_elements` array, maintain the home input group bridge, and provide `panel()` / `label()` accessors used by `launch.cpp`. +- Instance responsibilities: hold the `Launch` pointer, own per-page UI state, handle LVGL events, and forward carousel updates / app launches to `Launch`. LVGL still requires C-style static callbacks, but the current code no longer relies on global state for normal event dispatch. Each callback receives the owning page instance through LVGL user data: @@ -122,7 +122,7 @@ std::array UILaunchPage::carousel_elements = {}; ``` -The enum is defined in `UILaunchPage.h`: +The enum is defined in `ui_launch_page.h`: ```cpp enum LauncherCarouselElement : size_t { @@ -171,7 +171,7 @@ Therefore, `panel(2)` is the center card, and `label(2)` is the center title. ## 5. Standard Slot Layout -`UILaunchPage.cpp` uses `CarouselSlot` to describe the static carousel layout: +`ui_launch_page.cpp` uses `CarouselSlot` to describe the static carousel layout: ```cpp struct CarouselSlot { @@ -228,7 +228,7 @@ The top status bar comes from `home_base::creat_Top_UI()` and contains: - `ui_Panel1`, the time background image `status_time_background.png`, and `ui_timeLabel`. - `ui_batteryPanel`, the battery background image `status_battery_background.png`, `ui_Bar1`, and `ui_powerLabel`. -Status bar data is not refreshed in `UILaunchPage`, but in `LaunchImpl::update_home_status_bar()`: +Status bar data is not refreshed in `UILaunchPage`, but in `Launch::update_home_status_bar()`: ```cpp cp0_wifi_status_t wifi = cp0_wifi_get_status(); @@ -236,7 +236,7 @@ cp0_time_str(time_buf, sizeof(time_buf)); cp0_battery_info_t bat = cp0_battery_read(); ``` -`LaunchImpl` creates a 5-second timer during construction: +`Launch` creates a 5-second timer during construction: ```cpp status_timer = lv_timer_create(home_status_timer_cb, 5000, this); @@ -263,7 +263,7 @@ It then creates, in order: - 5 cards: the center is 100x100, left/right are 80x80, and far-side cards are 61x61 and hidden. - Left/right buttons: background images `carousel_left_arrow.png` / `carousel_right_arrow.png`. -The default titles are only UI placeholders. Real content is written by `LaunchImpl` after it initializes the application list. +The default titles are only UI placeholders. Real content is written by `Launch` after it initializes the application list. ## 7. Carousel Switch Flow @@ -374,7 +374,7 @@ It solves two problems: ## 9. How Application Data Is Written into the Carousel -`LaunchImpl` maintains `current_app` and `app_list`. During a switch, `UILaunchPage` only passes in the panel/label to be reused; `LaunchImpl` calculates which application should be displayed. +`Launch` maintains `current_app` and `app_list`. During a switch, `UILaunchPage` only passes in the panel/label to be reused; `Launch` calculates which application should be displayed. Fill the new right end after switching left: @@ -512,5 +512,5 @@ User presses ENTER - The names `switch_left()` / `switch_right()` describe animation direction and are not necessarily identical to user key direction. Currently, `KEY_LEFT` calls `switch_right()`, and `KEY_RIGHT` calls `switch_left()`. - During animation, only one `pending_switch_` enum value is recorded, so rapid repeated key presses do not create an unbounded queue. - Home card click events are bound to `on_app_clicked()`, which bridges to `launch_selected_app()`, but normal interaction mainly uses center selection + Enter launch. If mouse/touch interaction is enabled, confirm whether clicking a non-center card matches expectations. -- Status bar objects are created by `UILaunchPage`, but the refresh timer is created during `LaunchImpl` construction. If the home screen is created without executing `Launch::bind_ui()`, the application list and status bar refresh will not start. +- Status bar objects are created by `UILaunchPage`, but the refresh timer is created during `Launch` construction. If the home screen is created without executing `Launch::bind_ui()`, the application list and status bar refresh will not start. - When adding or adjusting carousel slots, update `CAROUSEL_SLOTS`, the initial positions in `create_app_container()`, and the slot definitions in the animation file together to avoid jumps after animation completion. diff --git a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md index 0b50f10d..7ebefe4d 100644 --- a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md +++ b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md @@ -1,6 +1,6 @@ # 04 - Application Model and Launch Mechanism -This chapter explains how APPLaunch unifies built-in pages, terminal commands, and external standalone programs into one application list, and how an application is launched after the user presses Enter. Key references are `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/Launch.h`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/page_app/*`. +This chapter explains how APPLaunch unifies built-in pages, terminal commands, and external standalone programs into one application list, and how an application is launched after the user presses Enter. Key references are `projects/APPLaunch/main/ui/launch.cpp`, `projects/APPLaunch/main/ui/launch.h`, `projects/APPLaunch/main/ui/ui_launch_page.cpp`, and `projects/APPLaunch/main/ui/page_app/*`. ## 1. Application Model Overview @@ -11,7 +11,7 @@ app ├── Name display title ├── Icon icon path ├── Exec external command; can be empty for built-in pages -└── launch(LaunchImpl*) launch action +└── launch(Launch*) launch action ``` After this unification, the home carousel does not need to care about application type. It only displays `Name` and `Icon`; when Enter is pressed, it simply calls the current `app.launch()`. @@ -19,7 +19,7 @@ After this unification, the home carousel does not need to care about applicatio ```text Home center card -> Launch::launch_app() - -> LaunchImpl::launch_app() + -> Launch::launch_app() -> app.launch(this) ├── Built-in page: new PageT + lv_disp_load_scr() ├── Terminal app: UIConsolePage + PTY exec() @@ -30,67 +30,34 @@ Home center card | Path | Description | | --- | --- | -| `projects/APPLaunch/main/ui/Launch.h` | Public facade for `Launch`, hiding `LaunchImpl` | -| `projects/APPLaunch/main/ui/Launch.cpp` | `app`, `LaunchImpl`, application list, launch logic, `.desktop` scanning | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Forwards Enter / click events to `Launch::launch_app()` | +| `projects/APPLaunch/main/ui/launch.h` | Public `Launch` interface and app model declarations | +| `projects/APPLaunch/main/ui/launch.cpp` | `app`, `Launch`, application list, launch logic, `.desktop` scanning | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Forwards Enter / click events to `Launch::launch_app()` | | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | Terminal page `UIConsolePage` | -| `projects/APPLaunch/main/ui/page_app/*.hpp` | Built-in pages such as settings, music, file, camera, and LoRa | +| `projects/APPLaunch/main/ui/page_app/*.hpp` | Built-in pages such as settings, game, file, camera, and LoRa | | `projects/APPLaunch/APPLaunch/applications/` | Runtime `.desktop` application descriptor directory | | `ext_components/cp0_lvgl` | Lower-level capabilities such as process launch, PTY, directory watching, and path resolution | -## 3. `Launch` and `LaunchImpl` Layers - -`Launch.h` exposes only a small public surface: - -```cpp -class Launch -{ -public: - void bind_ui(); - void set_launch_page(std::shared_ptr launch_page); - void update_left_slot(lv_obj_t *panel, lv_obj_t *label); - void update_right_slot(lv_obj_t *panel, lv_obj_t *label); - void launch_app(); - -private: - std::unique_ptr impl_; - std::shared_ptr launch_page_; -}; -``` - -The real logic lives in `LaunchImpl`: +## 3. `Launch` Runtime State -```cpp -class LaunchImpl -{ -private: - int current_app = 2; - cp0_watcher_t dir_watcher = NULL; - lv_timer_t *watch_timer = nullptr; - lv_timer_t *status_timer = nullptr; - int fixed_count; - -public: - std::list app_list; - std::shared_ptr app_Page; - std::shared_ptr home_Page; -}; -``` +`launch.h` exposes the `Launch` class directly. Current code no longer has a separate `LaunchImpl` layer; the application list, directory watcher, current page holder, and carousel helpers all live in `Launch`. -Field meanings: +Important private state includes: | Field | Description | | --- | --- | +| `launch_page_` | Weak reference to the home `UILaunchPage` | | `current_app` | Application index corresponding to the current center card. Defaults to `2`, so the initial center card is CLI | -| `app_list` | All fixed applications and dynamic `.desktop` applications | -| `fixed_count` | Number of fixed applications. Dynamic reload keeps the elements before this point | +| `dir_watcher_` / `watch_timer_` | Watches the `applications/` directory and reloads dynamic apps | +| `fixed_count` | Number of built-in/fixed applications. Dynamic reload keeps the elements before this point | +| `app_list` | Built-in entries plus dynamic `.desktop` entries | | `app_Page` | Lifetime holder for the current built-in page or terminal page | -| `dir_watcher` / `watch_timer` | Watches the `applications/` directory for changes and reloads dynamic apps | -| `status_timer` | Timer that refreshes the home status bar | + +`Launch::bind_ui()` builds the initial list, loads dynamic `.desktop` files, starts the directory watcher timer, and registers the app-registry change callback. ## 4. `app` Structure and Three Launch Modes -`app` is defined in `Launch.cpp`: +`app` is defined in `launch.cpp`: ```cpp struct app @@ -99,7 +66,7 @@ struct app std::string Icon; std::string Exec; - std::function launch; + std::function launch; app(std::string name, std::string icon, std::string exec, bool terminal); app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause); @@ -114,32 +81,32 @@ Three application categories: | Type | Construction | Launch function | Examples | | --- | --- | --- | --- | -| Built-in page | `page_v` | Constructs a page and calls `lv_disp_load_scr()` | `GAME`, `SETTING`, `MUSIC` | +| Built-in page | `page_v` | Constructs a page and calls `lv_disp_load_scr()` | `GAME`, `SETTING`, `Compass` | | Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`, `CLI` | | External process | `exec, terminal=false` | `launch_Exec()` | AppStore, Calculator | ## 5. Fixed Application Registration -The `LaunchImpl` constructor first registers fixed entries: +Built-in entries are declared in `launch.cpp` as `kBuiltinApps[]`. Each entry carries an `AppDescriptor` with the label, icon, config key, whether it is configurable in Settings, and whether it is always on. -```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", - false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); -``` - -Then it writes the first 5 applications into the 5 home-screen slots: +Representative entries: ```cpp -lv_label_set_text(UILaunchPage::label(0), it->Name.c_str()); -panel_set_icon(UILaunchPage::panel(0), it->Icon.c_str()); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, + {{"MATH", "math_100.png", "app_Math", true, false}, + "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false, true, false, nullptr}, +}; ``` -Initial state: +`Launch::rebuild_builtin_apps()` clears the list, appends enabled built-ins by calling `launcher_app_registry_is_enabled()`, and updates `fixed_count`. Settings changes are saved through `launcher_app_registry_set_enabled()` and then trigger `Launch::applications_reload()`. + +The first five entries initialize the 5-slot home carousel: ```text slot 0 far-left : Python @@ -150,23 +117,6 @@ slot 4 far-right: SETTING current_app : 2 ``` -After that, optional applications are appended according to settings and platform conditions: - -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("Music")) - app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); - -if (APP_ENABLED("Math")) - app_list.emplace_back("MATH", img_path("math_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); - -app_list.emplace_back("Compass", img_path("compass_needle_80.png"), page_v); -``` - -On Linux device builds, pages such as `IP_PANEL`, `FILE`, `SSH`, `MESH`, `REC`, `CAMERA`, `LORA`, and `TANK` are also appended according to configuration. - ## 6. Built-in Page Launch Mechanism Built-in pages are constructed through the template constructor: @@ -176,7 +126,7 @@ template app::app(std::string name, std::string icon, page_t) : Name(std::move(name)), Icon(std::move(icon)) { - launch = [](LaunchImpl *self) + launch = [](Launch *self) { ui_loading_show("Loading..."); lv_refr_now(NULL); @@ -185,7 +135,7 @@ app::app(std::string name, std::string icon, page_t) self->app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); - p->navigate_home = std::bind(&LaunchImpl::go_back_home, self); + p->navigate_home = std::bind(&Launch::go_back_home, self); ui_loading_hide(); }; @@ -203,14 +153,14 @@ Launch sequence: ```text Enter - -> app.launch(LaunchImpl*) + -> app.launch(Launch*) -> ui_loading_show("Loading...") -> lv_refr_now(NULL) -> make_shared() -> app_Page = p keeps the lifetime -> lv_disp_load_scr(p->screen()) -> Input device switches to p->input_group() - -> p->navigate_home = LaunchImpl::go_back_home + -> p->navigate_home = Launch::go_back_home -> ui_loading_hide() ``` @@ -228,7 +178,7 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); - p->navigate_home = std::bind(&LaunchImpl::go_back_home, this); + p->navigate_home = std::bind(&Launch::go_back_home, this); p->terminal_sysplause = sysplause; ui_loading_hide(); @@ -331,7 +281,7 @@ void go_back_home() static void lv_go_back_home(void *arg) { - auto self = (LaunchImpl *)arg; + auto self = (Launch *)arg; lv_timer_enable(true); if (self->launch_page_) self->launch_page_->show_home_screen(); @@ -406,7 +356,7 @@ Note: dynamic applications are deduplicated by `Exec`; if `Exec` matches a fixed ## 11. Dynamic Application Directory Watching and Reloading -At the end of the `LaunchImpl` constructor: +At the end of the `Launch` constructor: ```cpp fixed_count = app_list.size(); @@ -478,7 +428,7 @@ User releases ENTER -> launch_->launch_app() -> Launch::launch_app() -> impl_->launch_app() - -> LaunchImpl::launch_app() + -> Launch::launch_app() -> auto it = std::next(app_list.begin(), current_app) -> it->launch(this) -> Enter built-in page / terminal page / external process based on app type @@ -486,7 +436,7 @@ User releases ENTER ## 14. Notes -- `Launch::bind_ui()` must be called before `LaunchImpl` is created. Otherwise, the home screen may be displayed, but application list updates, the status-bar timer, directory watching, and launch logic will not work. +- `Launch::bind_ui()` must be called before `Launch` is created. Otherwise, the home screen may be displayed, but application list updates, the status-bar timer, directory watching, and launch logic will not work. - `current_app` defaults to `2`. The order of the first 5 fixed entries affects the initial center card; consider the initial home experience when changing this order. - If built-in page construction can take a long time, keep `ui_loading_show()` + `lv_refr_now()` so the user sees immediate feedback. - Launching an external application pauses APPLaunch LVGL timers and input group. The external program must exit normally or respond to the HOME logic, otherwise the user will feel stuck in the external UI. diff --git a/docs/launcher-project-guide/05-built-in-page-framework.md b/docs/launcher-project-guide/05-built-in-page-framework.md index 09134115..cc04d345 100644 --- a/docs/launcher-project-guide/05-built-in-page-framework.md +++ b/docs/launcher-project-guide/05-built-in-page-framework.md @@ -1,15 +1,15 @@ # 05 - Built-in Page Framework -This chapter explains the class hierarchy, lifecycle, page list, page registration method, and conventions for adding built-in APPLaunch pages. Key source files are `projects/APPLaunch/main/ui/ui_app_page.hpp`, `projects/APPLaunch/main/ui/page_app/*.hpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/UILaunchPage.cpp`. +This chapter explains the class hierarchy, lifecycle, page list, page registration method, and conventions for adding built-in APPLaunch pages. Key source files are `projects/APPLaunch/main/ui/ui_app_page.hpp`, `projects/APPLaunch/main/ui/page_app/*.hpp`, `projects/APPLaunch/main/ui/launch.cpp`, and `projects/APPLaunch/main/ui/ui_launch_page.cpp`. ## 1. What a Built-in Page Is A built-in page is an LVGL page class compiled into the APPLaunch process. It is different from an external `.desktop` application: - A built-in page directly creates an `lv_obj_t *root_screen_` and switches to its own screen through `lv_disp_load_scr(page->screen())`. -- The page object is stored in `LaunchImpl::app_Page`, and is released asynchronously by the `navigate_home` callback when exiting. +- The page object is stored in `Launch::app_Page`, and is released asynchronously by the `navigate_home` callback when exiting. - The page shares the APPLaunch process, LVGL main loop, input thread, resource resolution, and `cp0_lvgl_app.h` system interfaces with the home screen. -- Pages are usually header-only and placed under `projects/APPLaunch/main/ui/page_app/`, then aggregated by `components/page_app.h`. +- Pages are usually header-only and placed under `projects/APPLaunch/main/ui/page_app/`, then aggregated by `build/generated/include/generated/page_app.h`. Simplified relationship: @@ -17,7 +17,7 @@ Simplified relationship: UILaunchPage home carousel | v -LaunchImpl::launch_app() +Launch::launch_app() | +-- External command: cp0_process_exec_blocking() +-- Terminal command: UIConsolePage + PTY @@ -62,7 +62,7 @@ Key points: - `root_screen_` is the page's own top-level screen, not a child of the home `UILaunchPage::screen()`. - By default, `input_group_` only contains `root_screen_`. When the page is launched, it is bound to the current `lv_indev_t`. -- `navigate_home` is injected by `LaunchImpl`; a page calls it to return home after ESC or after finishing a task. +- `navigate_home` is injected by `Launch`; a page calls it to return home after ESC or after finishing a task. - The destructor deletes `root_screen_` and `input_group_`, so LVGL child objects created inside the page are released with the screen. ### 2.2 Top Bar, Content Area, and Bottom Bar Regions @@ -123,31 +123,31 @@ Note: ordinary built-in pages have their own status refresh timer. A page must r ### 4.1 Launching a Built-in Page from Home -`Launch.cpp` constructs a built-in page app descriptor through a template: +`launch.cpp` constructs a built-in page app descriptor through a template: ```cpp template app::app(std::string name, std::string icon, page_t) : Name(std::move(name)), Icon(std::move(icon)) { - launch = [](LaunchImpl *ctx) { + launch = [](Launch *ctx) { auto p = std::make_shared(); ctx->app_Page = p; - p->navigate_home = std::bind(&LaunchImpl::go_back_home, ctx); + p->navigate_home = std::bind(&Launch::go_back_home, ctx); lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); }; } ``` -In the actual code in `projects/APPLaunch/main/ui/Launch.cpp`, the core flow is: +In the actual code in `projects/APPLaunch/main/ui/launch.cpp`, the core flow is: 1. After the user releases ENTER on the home screen, `UILaunchPage::handle_home_key()` calls `launch_selected_app()`. 2. `UILaunchPage::launch_selected_app()` forwards to `Launch::launch_app()`. -3. `LaunchImpl::launch_app()` finds the current app and executes that app's `launch` function. +3. `Launch::launch_app()` finds the current app and executes that app's `launch` function. 4. The built-in page object is created, the screen is loaded, and the input group is switched. 5. The page calls `navigate_home()` after ESC or after completing its business logic. -6. `LaunchImpl::go_back_home()` uses `lv_async_call()` to return to the home screen, rebinds the home input group, and resets `app_Page`. +6. `Launch::go_back_home()` uses `lv_async_call()` to return to the home screen, rebinds the home input group, and resets `app_Page`. ### 4.2 Returning Home @@ -158,7 +158,7 @@ if (navigate_home) navigate_home(); ``` -`LaunchImpl::lv_go_back_home()` will: +`Launch::lv_go_back_home()` will: - `lv_timer_enable(true)` to restore LVGL timers. - `UILaunchPage::bind_home_input_group()` to bind the home input group. @@ -168,7 +168,7 @@ if (navigate_home) Notes: - A page destructor must stop any `lv_timer_t`, background thread, file watcher, PTY, or audio resource that the page created. -- Do not directly `delete this` from a keyboard event callback stack; use `navigate_home` and let `LaunchImpl` handle it asynchronously. +- Do not directly `delete this` from a keyboard event callback stack; use `navigate_home` and let `Launch` handle it asynchronously. - If a page temporarily switches to a child or nested page, it must restore the correct input group. ## 5. Current Built-in Page List @@ -180,9 +180,9 @@ Page implementations are concentrated in `projects/APPLaunch/main/ui/page_app/`. | `UIConsolePage` | `ui_app_console.hpp` | `CLI` or terminal external command | `AppPage` | Terminal emulator, PTY read/write, supports ANSI/VT sequences and keyboard escape sequences | | `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | Snake game, full-screen custom drawing, driven by an LVGL timer | | `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | System settings, application toggles, brightness, volume, WiFi, camera resolution, and more | -| `UIMusicPage` | `ui_app_music.hpp` | `MUSIC` | `AppPage` | Music player, directory browsing, playlist, audio callbacks | +| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPage` | Built-in game entry | | `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | Compass page, sensor thread + UI timer | -| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | `IP_PANEL` | `AppPage` | Network interface/IP information list, refreshed every second | +| `UIIpPanelPage` | `ui_app_ip_panel.hpp` | `IP_PANEL` | `AppPage` | Network interface/IP information list, refreshed every second | | `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | File browser, directory list and enter/back navigation | | `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH parameter input, embeds `UIConsolePage` after connection | | `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh message list, input overlay, send/refresh | @@ -195,7 +195,7 @@ Page implementations are concentrated in `projects/APPLaunch/main/ui/page_app/`. ## 6. Page Registration and Display Order -Built-in pages are inserted into `app_list` in `LaunchImpl::LaunchImpl()`. The first 5 fixed applications initialize the 5 home carousel slots first: +Built-in pages are inserted into `app_list` in `Launch::Launch()`. The first 5 fixed applications initialize the 5 home carousel slots first: ```cpp app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); @@ -205,22 +205,12 @@ app_list.emplace_back("GAME", img_path("game_100.png"), page_v); app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); ``` -Optional built-in pages are then added according to setting toggles: - -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("Music")) - app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); - -if (APP_ENABLED("IP_Panel")) - app_list.emplace_back("IP_PANEL", img_path("ip_panel_100.png"), page_v); -``` +Built-in page visibility is now driven by `kBuiltinApps[]` and `AppDescriptor.config_key`. `Launch::rebuild_builtin_apps()` calls `launcher_app_registry_is_enabled()` before appending each descriptor, and Settings changes call `launcher_app_registry_set_enabled()` followed by `Launch::applications_reload()`. Conventions: - `Store`, `CLI`, `Game`, and `Setting` are always-on in the settings page and cannot be disabled. -- `Compass` is currently added unconditionally in `Launch.cpp` and is not controlled by the Launcher toggle list in `UISetupPage`. +- `Compass` is currently added unconditionally in `launch.cpp` and is not controlled by the Launcher toggle list in `UISetupPage`. - Pages such as `IP_PANEL`, `FILE`, `SSH`, `MESH`, `REC`, `CAMERA`, `LORA`, and `TANK` are added only in Linux device builds; SDL builds are limited by `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)`. - Dynamic `.desktop` applications are scanned and added after built-in pages. Directory changes are checked by a watcher every 3 seconds. @@ -315,13 +305,13 @@ Special care is required for this type of page: ## 10. Relationship with the Home Carousel -The home carousel itself is managed by `UILaunchPage.cpp`: +The home carousel itself is managed by `ui_launch_page.cpp`: - `carousel_elements` stores 5 cards, 5 titles, and 5 page dots. -- When switching left/right, `switch_left()` / `switch_right()` are called. After the animation finishes, the array is rotated and `LaunchImpl` updates the far-side slot content. +- When switching left/right, `switch_left()` / `switch_right()` are called. After the animation finishes, the array is rotated and `Launch` updates the far-side slot content. - ENTER triggers `UILaunchPage::launch_selected_app()`, which ultimately calls the current app's `launch()`. -Built-in pages do not directly manipulate the home carousel. After returning home, the carousel state is preserved by `LaunchImpl`. +Built-in pages do not directly manipulate the home carousel. After returning home, the carousel state is preserved by `Launch`. ## 11. Common Notes @@ -330,4 +320,4 @@ Built-in pages do not directly manipulate the home carousel. After returning hom - Do not directly access home global objects from a page unless it is clearly a home-screen feature. - For page titles, call `set_page_title()` instead of modifying the internal top-bar label directly. - Every page that can exit should support `KEY_ESC` and call `navigate_home` or return to the previous view. -- Page toggle keys must stay consistent with `UISetupPage::save_app_toggle()` and `APP_ENABLED()` in `Launch.cpp`. +- Page toggle keys must stay consistent with `UISetupPage::save_app_toggle()` and `APP_ENABLED()` in `launch.cpp`. diff --git a/docs/launcher-project-guide/06-resources-and-configuration.md b/docs/launcher-project-guide/06-resources-and-configuration.md index 0477a941..36903428 100644 --- a/docs/launcher-project-guide/06-resources-and-configuration.md +++ b/docs/launcher-project-guide/06-resources-and-configuration.md @@ -1,13 +1,13 @@ # 06 - Resources and Configuration System -This chapter explains APPLaunch runtime resource directories, path resolution rules, `.desktop` dynamic application files, configuration APIs, settings-page configuration keys, and resource usage notes. Key source files are `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`, `projects/APPLaunch/main/ui/Launch.cpp`, and `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`. +This chapter explains APPLaunch runtime resource directories, path resolution rules, `.desktop` dynamic application files, configuration APIs, settings-page configuration keys, and resource usage notes. Key source files are `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`, `projects/APPLaunch/main/ui/launch.cpp`, and `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`. ## 1. Resource System Overview APPLaunch pages should not manually concatenate runtime paths. Instead, use `cp0_file_path()` / `cp0_file_path_c()` for unified resolution. ```text -Page code / Launch.cpp +Page code / launch.cpp | v img_path(), audio_path(), cp0_file_path_c() @@ -66,7 +66,7 @@ After installation on a device, it usually maps to: | Directory | Contents | Used by | | --- | --- | --- | -| `applications/` | `.desktop` application descriptors | `LaunchImpl::applications_load()` | +| `applications/` | `.desktop` application descriptors | `Launch::applications_load()` | | `share/images/` | Icons, status-bar backgrounds, page images, GIFs | Home screen, top bar, built-in pages | | `share/audio/` | `startup.mp3`, `switch.wav`, `enter.wav`, page key sounds | Home sound effects, settings page, page sound effects | | `share/font/` | TTF/OTF fonts | `LauncherFonts`, page custom fonts | @@ -145,14 +145,14 @@ Therefore, the returned `const char *` is stable within the thread and can be pa Common home-screen and built-in-page usage: ```cpp -app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); lv_obj_set_style_bg_img_src(time_panel_, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); ``` -Home card icons are set by `Launch.cpp::panel_set_icon()`: +Home card icons are set by `launch.cpp::panel_set_icon()`: ```cpp static void panel_set_icon(lv_obj_t *panel, const char *src) @@ -203,7 +203,7 @@ Font paths are ultimately resolved by `cp0_file_path()` into `share/font/`. If f ## 5. `.desktop` Dynamic Applications -Dynamic application files are placed in the directory pointed to by `cp0_file_path("applications")`. `LaunchImpl::applications_load()` only processes `*.desktop` files and parses the `[Desktop Entry]` section. +Dynamic application files are placed in the directory pointed to by `cp0_file_path("applications")`. `Launch::applications_load()` only processes `*.desktop` files and parses the `[Desktop Entry]` section. Supported keys: @@ -273,13 +273,13 @@ The `Launcher` menu in `UISetupPage` saves `app_`: | Configuration key | Default | Meaning | Notes | | --- | --- | --- | --- | -| `app_Python` | `1` | Python entry display toggle | Visible in settings, but Python is fixed in `Launch.cpp`; currently this toggle does not affect fixed entries | +| `app_Python` | `1` | Python entry display toggle | Visible in settings, but Python is fixed in `launch.cpp`; currently this toggle does not affect fixed entries | | `app_Store` | `1` | Store entry | always-on, cannot be disabled | | `app_CLI` | `1` | CLI entry | always-on, cannot be disabled | | `app_Game` | `1` | GAME entry | always-on, cannot be disabled | | `app_Setting` | `1` | SETTING entry | always-on, cannot be disabled | -| `app_Music` | `1` | MUSIC built-in page | Read by `Launch.cpp` | -| `app_Math` | `1` | Calculator external application | Read by `Launch.cpp` | +| `app_Game` | `1` | GAME built-in page | Read by `launch.cpp` | +| `app_Math` | `1` | Calculator external application | Read by `launch.cpp` | | `app_IP_Panel` | `1` | IP_PANEL built-in page | Read under Linux non-SDL builds | | `app_File` | `1` | FILE built-in page | Read under Linux non-SDL builds | | `app_SSH` | `1` | SSH built-in page | Read under Linux non-SDL builds | @@ -289,7 +289,7 @@ The `Launcher` menu in `UISetupPage` saves `app_`: | `app_LoRa` | `1` | LORA built-in page | Read under Linux non-SDL builds | | `app_Tank` | `1` | TANK built-in page | Read under Linux non-SDL builds | -Note: `Compass` currently has no corresponding `app_Compass` setting and is added unconditionally by `Launch.cpp`. +Note: `Compass` currently has no corresponding `app_Compass` setting and is added unconditionally by `launch.cpp`. ### 7.2 System and Page Configuration @@ -339,12 +339,12 @@ When changing configuration keys, check all of the following in sync: - `app_keys` / `app_labels` in `UISetupPage::menu_init()`. - The `app_keys` and always-on list in `UISetupPage::save_app_toggle()`. -- `APP_ENABLED("...")` in `Launch.cpp`. +- `APP_ENABLED("...")` in `launch.cpp`. - Documentation and default configuration. ## 9. Resource Naming Recommendations -- Name home icons as `_100.png`, such as `music_100.png` and `setting_100.png`. +- Name home icons as `_100.png`, such as `game_100.png` and `setting_100.png`. - Name small icons or status backgrounds by function, such as `status_time_background.png` and `status_battery_background.png`. - Use a page prefix for page-specific resources, such as `setting_ok.png` and `setting_cross.png`. - Use short names for sound effects, such as `switch.wav`, `enter.wav`, and `key_back.wav`. @@ -357,5 +357,5 @@ When changing configuration keys, check all of the following in sync: - The `.desktop` `Icon` value does not automatically call `cp0_file_path()`; use a path that LVGL can read directly, or keep it consistent with existing templates. - If a new resource is used on the device side, confirm that packaging scripts include `projects/APPLaunch/APPLaunch/share/...` in the install package. - If `cp0_config_save()` is forgotten after writing configuration, the value will be lost after reboot. -- `app_*` toggles affect the list the next time `LaunchImpl` is constructed; changing them at runtime may not immediately update the fixed home list, depending on whether a rebuild/restart is triggered. +- `app_*` toggles affect the list the next time `Launch` is constructed; changing them at runtime may not immediately update the fixed home list, depending on whether a rebuild/restart is triggered. - `run_as_user` affects the execution identity of external processes and PTY commands. Check this setting when debugging permission issues. diff --git a/docs/launcher-project-guide/07-input-system-and-key-mapping.md b/docs/launcher-project-guide/07-input-system-and-key-mapping.md index 70114213..b4340471 100644 --- a/docs/launcher-project-guide/07-input-system-and-key-mapping.md +++ b/docs/launcher-project-guide/07-input-system-and-key-mapping.md @@ -1,6 +1,6 @@ # 07 - Input System and Key Mapping -This chapter explains APPLaunch's keyboard input thread, the `key_item` event structure, LVGL event dispatch, key mappings on the home screen and built-in pages, terminal input escaping, and debugging notes. The key source files are `projects/APPLaunch/main/include/keyboard_input.h`, `projects/APPLaunch/main/ui/ui.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`, `projects/APPLaunch/main/ui/UILaunchPage.cpp`, and `projects/APPLaunch/main/ui/page_app/*.hpp`. +This chapter explains APPLaunch's keyboard input thread, the `key_item` event structure, LVGL event dispatch, key mappings on the home screen and built-in pages, terminal input escaping, and debugging notes. The key source files are `ext_components/cp0_lvgl/include/keyboard_input.h`, `projects/APPLaunch/main/ui/ui.h`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`, `projects/APPLaunch/main/ui/ui_launch_page.cpp`, and `projects/APPLaunch/main/ui/page_app/*.hpp`. ## 1. Input System Overview @@ -43,7 +43,7 @@ if (LV_EVENT_KEYBOARD == 0) ## 2. `key_item` Data Structure -`projects/APPLaunch/main/include/keyboard_input.h` defines input events: +`ext_components/cp0_lvgl/include/keyboard_input.h` defines input events: ```c struct key_item { @@ -226,11 +226,11 @@ Note: `elm` is freed after the callback returns, so pages must not keep the poin If a page handles `LV_EVENT_KEYBOARD` directly, it usually uses the raw `KEY_*` values. If a page delegates to LVGL's widget focus mechanism, it relies on `data->key`. -`projects/APPLaunch/main/include/compat/input_keys.h` includes `` on Linux and provides common compatible `KEY_*` definitions on non-Linux platforms, so SDL/desktop builds can also compile page code. +`ext_components/cp0_lvgl/include/compat/input_keys.h` includes `` on Linux and provides common compatible `KEY_*` definitions on non-Linux platforms, so SDL/desktop builds can also compile page code. ## 6. Home Screen Key Mapping -Home screen key handling is in `UILaunchPage::handle_home_key()`; the LVGL C callback entry is `UILaunchPage::on_home_key()` in `projects/APPLaunch/main/ui/UILaunchPage.cpp`. +Home screen key handling is in `UILaunchPage::handle_home_key()`; the LVGL C callback entry is `UILaunchPage::on_home_key()` in `projects/APPLaunch/main/ui/ui_launch_page.cpp`. First, the commonly used `F/X/Z/C` keys on CardputerZero are mapped to arrow keys: @@ -268,8 +268,8 @@ Each page independently binds `LV_EVENT_KEYBOARD` on its `root_screen_`. Common | `UIConsolePage` | `ui_app_console.hpp` | ESC/arrow/Enter/Backspace are converted to PTY control sequences; HOME-related state is used for exit/external locks | | `UIGamePage` | `ui_app_game.hpp` | Arrow keys move, ENTER starts/restarts, ESC returns | | `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN or F/X selects, ENTER/RIGHT or C enters/confirms, ESC/LEFT or Z returns, some pages support R/D | -| `UIMusicPage` | `ui_app_music.hpp` | F/X/Z/C map to LV_KEY_UP/DOWN/LEFT/RIGHT; ENTER plays/loads; ESC returns | -| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | F/X/Z/C map to LV_KEY_*; UP/DOWN selects; ESC returns | +| `UIGamePage` | `ui_app_game.hpp` | uses the common page key handling; ESC returns | +| `UIIpPanelPage` | `ui_app_ip_panel.hpp` | F/X/Z/C map to LV_KEY_*; UP/DOWN selects; ESC returns | | `UIFilePage` | `ui_app_file.hpp` | UP/DOWN selects; RIGHT/ENTER enters; LEFT goes to parent; ESC returns home or to the parent | | `UISSHPage` | `ui_app_ssh.hpp` | UP/DOWN switches Host/Port/User; character input; BACKSPACE deletes; ENTER connects; ESC returns | | `UIMeshPage` | `ui_app_mesh.hpp` | S opens input; R refreshes; UP/DOWN browses; ENTER sends; BACKSPACE deletes; ESC cancels/returns | @@ -283,8 +283,8 @@ Each page independently binds `LV_EVENT_KEYBOARD` on its `root_screen_`. Common On the CardputerZero keyboard, `F/X/Z/C` are commonly used as arrow-key substitutes. The codebase uses three patterns: -1. Home screen `UILaunchPage.cpp`: `fzxc_to_arrow()` converts `F/X/Z/C` to `KEY_UP/DOWN/LEFT/RIGHT`. -2. Page-local conversion to LVGL keys, for example in `UIMusicPage` and `UIIpPanelPage`: +1. Home screen `ui_launch_page.cpp`: `fzxc_to_arrow()` converts `F/X/Z/C` to `KEY_UP/DOWN/LEFT/RIGHT`. +2. Page-local conversion to LVGL keys, for example in `UIGamePage` and `UIIpPanelPage`: ```cpp switch (key) { @@ -336,7 +336,7 @@ The terminal page also handles child-process exit, screen refresh, cursor blinki ## 10. Input Handling While External Apps Are Running -External apps are launched through `LaunchImpl::launch_Exec()`: +External apps are launched through `Launch::launch_Exec()`: ```cpp LVGL_RUN_FLAGE = 0; diff --git a/docs/launcher-project-guide/08-build-and-compilation-guide.md b/docs/launcher-project-guide/08-build-and-compilation-guide.md index cf962699..dfdda5d1 100644 --- a/docs/launcher-project-guide/08-build-and-compilation-guide.md +++ b/docs/launcher-project-guide/08-build-and-compilation-guide.md @@ -1,6 +1,6 @@ # 08 - Build and Compilation Guide -This chapter explains the complete build process for `projects/APPLaunch`, covering Linux SDL2 native simulation, native device builds, Linux x86 cross-compilation, macOS cross-compilation, dependency installation, environment variables, key SCons logic, and common error handling. +This chapter explains the complete build process for `projects/APPLaunch`, covering Linux SDL2 native simulation, native device builds, Linux x86 cross-compilation, macOS cross-compilation, Windows SDL2/cross builds, dependency installation, environment variables, key SCons logic, and common error handling. All commands are assumed to start from the repository root by default: @@ -19,6 +19,8 @@ APPLaunch can be built in several forms. The core difference is determined by th | Linux x86 cross-compilation | Linux x86_64 development machine, output runs on the device | `linux_x86_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | Recommended way to build official device artifacts | | macOS cross-compilation | macOS development machine, output runs on the device | `mac_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | Generate arm64 device artifacts on macOS | | macOS SDL/Darwin configuration | macOS development machine | `darwin_config_defaults.mk` | SDL-related configuration | Base configuration for native SDL work | +| Windows SDL2 native simulation | Windows x86_64 development machine | `win_x86_sdl2_config_defaults.mk` | SDL2 window + SDL input | UI debugging on Windows | +| Windows x86 cross-compilation | Windows x86_64 development machine, output runs on the device | `win_x86_cross_config_defaults.mk` | Linux framebuffer + evdev | Generate arm64 device artifacts on Windows | Build artifacts usually appear in: @@ -578,9 +580,86 @@ file dist/M5CardputerZero-APPLaunch The expected result is an `ARM aarch64` Linux ELF, not Mach-O. -## 10. Key SCons Logic +## 10. Windows Builds -### 10.1 Top-Level `projects/APPLaunch/SConstruct` +Windows builds use the same SCons entry point under `projects/APPLaunch`, but the configuration sets `CONFIG_TOOLCHAIN_SYSTEM_WIN=y` and `CONFIG_TOOLCHAIN_GCCSUFFIX=".exe"` so the SDK build system invokes Windows toolchain executables. + +### 10.1 Windows SDL2 Native Build and Run + +Use an MSYS2 MinGW shell so `gcc`, `g++`, `pkg-config`, SDL2, and FreeType are all available in `PATH`. + +MSYS2 UCRT64 example: + +```bash +pacman -S --needed \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-pkgconf \ + mingw-w64-ucrt-x86_64-SDL2 \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-python-pip + +python -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +Build and run: + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch.exe +``` + +Key entries in `win_x86_sdl2_config_defaults.mk`: + +```make +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_TOOLCHAIN_SYSTEM_WIN=y +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_APPLAUNCH_WIN_X86_SDL2=y +``` + +The SDL2 output is `dist/M5CardputerZero-APPLaunch.exe`. + +### 10.2 Windows Cross-Compilation to the Device + +Install the SysGCC Raspberry64 Windows AArch64 Linux cross toolchain from `https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe`. The default configuration expects: + +```make +CONFIG_TOOLCHAIN_PATH="D:\\app\\SysGCC\\bin" +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_GCC_DUMPMACHINE="aarch64-linux-gnu" +``` + +If the toolchain is installed elsewhere, update `CONFIG_TOOLCHAIN_PATH` in `projects/APPLaunch/win_x86_cross_config_defaults.mk` before building. + +Build: + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_cross_config_defaults.mk +scons -j8 +``` + +Key entries in `win_x86_cross_config_defaults.mk`: + +```make +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +CONFIG_APPLAUNCH_WIN_X86_CROSS_CP0=y +``` + +The cross-build output is `dist/M5CardputerZero-APPLaunch` with no `.exe` suffix because the target is Linux AArch64. The first cross-build may download the SDK sysroot package into `SDK/github_source/static_lib_v0.0.4`. + +## 11. Key SCons Logic + +### 11.1 Top-Level `projects/APPLaunch/SConstruct` This file is responsible for the build entry point and global environment preparation: @@ -614,7 +693,7 @@ SConscript(str(sdk_path / "tools" / "scons" / "project.py"), variant_dir=os.getc 6. Checks and downloads `static_lib_v0.0.4` during cross-compilation. -### 10.2 SDK `project.py` +### 11.2 SDK `project.py` The SDK build system does the following: @@ -627,22 +706,22 @@ The SDK build system does the following: 7. Builds static libraries, shared libraries, and executables. 8. Copies the executable and `STATIC_FILES` to `dist`. -### 10.3 `projects/APPLaunch/main/SConstruct` +### 11.3 `projects/APPLaunch/main/SConstruct` This file registers the APPLaunch main-program component: -- Runs `ui/components/generate_page_app_includes.py` to generate the built-in page include aggregation file. +- Runs `ui/generate_page_app_includes.py` to generate the built-in page include aggregation file. - Reads the current short git hash and injects compile macro `LAUNCHER_GIT_COMMIT_RAW`. - Collects `src/*.c*` and all source files under the `ui` directory. - Adds includes: `main`, `main/include`, `ext_components/cp0_lvgl/include`, and `SDK/components/utilities/include`. - Depends on components: `cp0_lvgl`, `eventpp`, `lvgl_component`, `pthread`, `Miniaudio`, and `RadioLib`. - Optional dependency: `Backward_cpp`. -- Adds SDL2, FreeType, libinput, xkbcommon, udev, libcamera, jpeg, and other dependencies according to different configuration files. +- Adds SDL2, FreeType, libinput, xkbcommon, udev, libcamera, jpeg, and other dependencies according to different configuration files; Windows SDL2 shares the same SDL2/FreeType `pkg-config` flag handling as Linux SDL2. - Uses `ext_components/RadioLib` as a static component; the RadioLib component owns the `wget_github('https://github.com/jgromes/RadioLib.git')` source cache and SX1262-related source list. - Adds the `../APPLaunch` runtime resource tree to `STATIC_FILES`; this tree includes `bin/store_cache_sync.py`. - Registers project target: `M5CardputerZero-APPLaunch`. -## 11. Common SCons Commands +## 12. Common SCons Commands | Command | Purpose | | --- | --- | @@ -663,7 +742,7 @@ scons -j8 Do not simply change `CONFIG_DEFAULT_FILE` and immediately run `scons -j8`, because the old `build/config/global_config.mk` may already exist and the SDK build system will not automatically regenerate the configuration. -## 12. `menuconfig` Recommendations +## 13. `menuconfig` Recommendations Run: @@ -688,9 +767,9 @@ scons save Note: `scons save` writes back to the configuration file. In multi-person collaboration, do not casually save to shared `*_config_defaults.mk` files unless this task explicitly requires that change. -## 13. Common Errors and Fixes +## 14. Common Errors and Fixes -### 13.1 `scons: command not found` +### 14.1 `scons: command not found` Cause: SCons is not installed, or the Python user bin directory is not in `PATH`. @@ -707,7 +786,7 @@ If `python3 -m scons` works, you can also build this way: python3 -m scons -j8 ``` -### 13.2 `ModuleNotFoundError: No module named 'parse'` +### 14.2 `ModuleNotFoundError: No module named 'parse'` Cause: missing Python package. @@ -719,7 +798,7 @@ python3 -m pip install --user parse requests tqdm paramiko scp In a virtual environment, run `source .venv/bin/activate` first. -### 13.3 `Package sdl2 was not found in the pkg-config search path` +### 14.3 `Package sdl2 was not found in the pkg-config search path` Cause: Linux SDL2 simulation dependencies are not installed, or `PKG_CONFIG_PATH` does not include the directory containing SDL2 `.pc` files. @@ -737,7 +816,14 @@ brew install sdl2 pkg-config pkg-config --cflags sdl2 ``` -### 13.4 `Package freetype2 was not found` +Windows/MSYS2: + +```bash +pacman -S --needed mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-SDL2 +pkg-config --cflags sdl2 +``` + +### 14.4 `Package freetype2 was not found` Fix: @@ -753,7 +839,14 @@ brew install freetype pkg-config pkg-config --cflags freetype2 ``` -### 13.5 `aarch64-linux-gnu-gcc: not found` +Windows/MSYS2: + +```bash +pacman -S --needed mingw-w64-ucrt-x86_64-freetype +pkg-config --cflags freetype2 +``` + +### 14.5 `aarch64-linux-gnu-gcc: not found` Cause: Linux cross toolchain is not installed, or `PATH` does not include the toolchain. @@ -766,7 +859,9 @@ aarch64-linux-gnu-gcc --version macOS cross-compilation should use `aarch64-unknown-linux-gnu-gcc`; the corresponding configuration file is `mac_cross_cp0_config_defaults.mk`. -### 13.6 Failed to Download `sdk_bsp.tar.gz` +Windows cross-compilation should use `aarch64-linux-gnu-gcc.exe`; check `CONFIG_TOOLCHAIN_PATH` and `CONFIG_TOOLCHAIN_PREFIX` in `win_x86_cross_config_defaults.mk`. + +### 14.6 Failed to Download `sdk_bsp.tar.gz` Cause: the first cross-compilation needs to download `static_lib_v0.0.4`, but the network is unavailable or GitHub access failed. @@ -783,7 +878,7 @@ SDK/github_source/static_lib_v0.0.4/ If the directory exists but the version does not match, the top-level `SConstruct` still tries to update it. -### 13.7 `libcamera` Headers or Libraries Not Found +### 14.7 `libcamera` Headers or Libraries Not Found In cross-compilation configurations, `main/SConstruct` adds: @@ -801,7 +896,7 @@ ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu | grep camera If they are missing, update the sysroot package or install device-side development libraries and rebuild the sysroot. -### 13.8 Link Errors: `cannot find -linput`, `-lxkbcommon`, or `-ludev` +### 14.8 Link Errors: `cannot find -linput`, `-lxkbcommon`, or `-ludev` Native SDL2 build: install development packages. @@ -817,7 +912,7 @@ ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libxkbcommon.* ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libudev.* ``` -### 13.9 Old Backend Still Used After Switching Configuration +### 14.9 Old Backend Still Used After Switching Configuration Cause: `build/config/global_config.mk` already exists, and the build system will not automatically regenerate the configuration just because the environment variable changed. @@ -835,7 +930,7 @@ Check the final configuration: grep -E 'LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk ``` -### 13.10 SDL2 Runs to a Black Screen or Missing Resources +### 14.10 SDL2 Runs to a Black Screen or Missing Resources Common cause: the program was not run from the `dist` directory, so `CONFIG_V9_5_LV_FS_POSIX_PATH="./"` points to the wrong location. @@ -847,7 +942,7 @@ ls APPLaunch/share/images ./M5CardputerZero-APPLaunch ``` -### 13.11 Device Reports Missing Resource Files +### 14.11 Device Reports Missing Resource Files The device configuration resource path is: @@ -864,7 +959,7 @@ ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch For manual deployment, make sure you copied the contents of `dist/APPLaunch`, not only the executable. -### 13.12 RadioLib Download Failure +### 14.12 RadioLib Download Failure `ext_components/RadioLib/SConstruct` uses `wget_github('https://github.com/jgromes/RadioLib.git')` to fetch RadioLib when `CONFIG_RADIOLIB_COMPONENT_ENABLED=y`. The first build may need network access. @@ -874,9 +969,9 @@ Fix: - Check whether a RadioLib cache already exists under `SDK/github_source`. - Prepare the corresponding source cache in advance for offline environments. -## 14. Recommended Build Flows +## 15. Recommended Build Flows -### 14.1 Daily UI Development +### 15.1 Daily UI Development ```bash cd /home/nihao/w2T/github/launcher/projects/APPLaunch @@ -887,7 +982,7 @@ cd dist ./M5CardputerZero-APPLaunch ``` -### 14.2 Generate Formal Device Artifacts +### 15.2 Generate Formal Device Artifacts ```bash cd /home/nihao/w2T/github/launcher/projects/APPLaunch @@ -899,7 +994,7 @@ file dist/M5CardputerZero-APPLaunch Then follow Chapter 09 for `.deb` packaging, installation, and systemd verification. -### 14.3 Quickly Confirm the Build Target +### 15.3 Quickly Confirm the Build Target ```bash grep CONFIG_DEFAULT_FILE /proc/$$/environ 2>/dev/null || true diff --git a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md index dac9bd4c..ebaa4220 100644 --- a/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md +++ b/docs/launcher-project-guide/09-packaging-deployment-and-systemd.md @@ -35,7 +35,7 @@ After formal installation, the target path is: The systemd service file is installed to: ```text -/lib/systemd/system/APPLaunch.service +/usr/lib/systemd/user/APPLaunch.service ``` The service start command is: @@ -50,6 +50,8 @@ The working directory is: /usr/share/APPLaunch ``` +The package installs APPLaunch as a systemd user service for the UID 1000 user. When checking the service manually, either log in as that user and run `systemctl --user ...`, or set `XDG_RUNTIME_DIR=/run/user/1000` when running through `runuser`/SSH automation. + ## 2. Build the Device Target Before Packaging The `.deb` should use arm64 device artifacts, not Linux SDL2 x86_64 simulation artifacts. @@ -122,11 +124,11 @@ projects/APPLaunch/tools/debian-APPLaunch/ │ ├── control │ ├── postinst │ └── prerm -├── lib/ -│ └── systemd/ -│ └── system/ -│ └── APPLaunch.service └── usr/ + ├── lib/ + │ └── systemd/ + │ └── user/ + │ └── APPLaunch.service └── share/ └── APPLaunch/ ├── applications/ @@ -331,9 +333,17 @@ The post-install script runs: ```sh mkdir -p /var/cache/APPLaunch -ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl enable APPLaunch.service -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl start APPLaunch.service +ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +loginctl enable-linger "$APP_USER" || true +systemctl start "user@$APP_UID.service" || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user enable APPLaunch.service || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user restart APPLaunch.service || true exit 0 ``` @@ -341,7 +351,7 @@ Purpose: - Creates writable cache directory `/var/cache/APPLaunch`. - Creates a `cache` symlink under the read-only/system resource directory. -- Enables and starts the systemd service. +- Enables lingering for the UID 1000 user, then enables and starts the systemd user service. Note: the current shared packager uses `ln -sfn`, so repeated installation can refresh the cache link safely. @@ -350,8 +360,14 @@ Note: the current shared packager uses `ln -sfn`, so repeated installation can r The pre-removal script runs: ```sh -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl stop APPLaunch.service -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl disable APPLaunch.service +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user stop APPLaunch.service || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user disable APPLaunch.service || true rm -rf /var/cache/APPLaunch exit 0 ``` @@ -371,6 +387,8 @@ The script generates: ```ini [Unit] Description=APPLaunch Service +After=pipewire-pulse.service +Wants=pipewire-pulse.service [Service] ExecStart=/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch @@ -380,7 +398,7 @@ RestartSec=1 StartLimitInterval=0 [Install] -WantedBy=multi-user.target +WantedBy=default.target ``` Field descriptions: @@ -392,9 +410,10 @@ Field descriptions: | `Restart=always` | Always restarts after the process exits | | `RestartSec=1` | Restarts 1 second after exit | | `StartLimitInterval=0` | Disables the default start-rate limit so systemd does not stop restarting after frequent crashes | -| `WantedBy=multi-user.target` | Starts with the multi-user target after enable | +| `After` / `Wants` | Starts after PipeWire PulseAudio support when available | +| `WantedBy=default.target` | Enables the service in the user's default systemd target | -The current service file does not explicitly set a user, so it runs as root as a systemd system service by default. This usually helps access framebuffer, evdev, GPIO, audio, and camera devices, but it also means the program has high privileges. +The current package installs a user service under `/usr/lib/systemd/user`, not a root-owned system service. It is enabled for the UID 1000 user by `postinst`; device permissions for framebuffer, evdev, GPIO, audio, and camera must therefore be provided by the image's user/group rules. ## 9. Install on the Device @@ -432,9 +451,9 @@ sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb If the service is running, `postinst` attempts to enable/start it. To reduce framebuffer or input-device contention during installation, you can stop the service manually first: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` ## 10. Quick Deployment with `scons push` @@ -457,8 +476,8 @@ remote_host = 192.168.28.177 remote_port = 22 username = pi password = pi -; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' -; after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +; before_cmd = 'echo pi | sudo -S systemctl --user stop APPLaunch.service' +; after_cmd = 'echo pi | sudo -S systemctl --user stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl --user start APPLaunch.service' ``` Run: @@ -505,17 +524,17 @@ scp -r dist/APPLaunch pi@192.168.28.177:/home/pi/APPLaunch-new Install on the device: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true sudo mkdir -p /usr/share/APPLaunch/bin sudo install -m 0755 /home/pi/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch sudo rsync -a --delete /home/pi/APPLaunch-new/ /usr/share/APPLaunch/ sudo mkdir -p /var/cache/APPLaunch sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache -sudo systemctl daemon-reload -sudo systemctl restart APPLaunch.service +systemctl --user daemon-reload +systemctl --user restart APPLaunch.service ``` -If the service file has not been installed, create `/lib/systemd/system/APPLaunch.service` manually, using the content in Section 8 as reference. +If the service file has not been installed, create `/usr/lib/systemd/user/APPLaunch.service` manually, using the content in Section 8 as reference. ## 12. Deployment Verification Commands @@ -582,46 +601,46 @@ If libraries are missing, install the corresponding system packages, or extend t ### 12.4 systemd Status ```bash -systemctl status APPLaunch.service --no-pager -systemctl is-enabled APPLaunch.service -systemctl is-active APPLaunch.service +systemctl --user status APPLaunch.service --no-pager +systemctl --user is-enabled APPLaunch.service +systemctl --user is-active APPLaunch.service ``` View logs: ```bash -journalctl -u APPLaunch.service -b --no-pager -journalctl -u APPLaunch.service -b -f +journalctl --user -u APPLaunch.service -b --no-pager +journalctl --user -u APPLaunch.service -b -f ``` Restart: ```bash -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` Stop: ```bash -sudo systemctl stop APPLaunch.service +systemctl --user stop APPLaunch.service ``` Enable boot autostart: ```bash -sudo systemctl enable APPLaunch.service +systemctl --user enable APPLaunch.service ``` Disable boot autostart: ```bash -sudo systemctl disable APPLaunch.service +systemctl --user disable APPLaunch.service ``` Reload service files: ```bash -sudo systemctl daemon-reload +systemctl --user daemon-reload ``` ### 12.5 Manual Foreground Run @@ -629,7 +648,7 @@ sudo systemctl daemon-reload Before troubleshooting systemd, run it in the foreground first: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true cd /usr/share/APPLaunch sudo ./bin/M5CardputerZero-APPLaunch ``` @@ -680,28 +699,28 @@ sudo dpkg -P applaunch ### 13.2 Roll Back by Installing an Older Package ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true sudo dpkg -i /home/pi/applaunch_old-version-m5stack1_arm64.deb -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` Verify: ```bash dpkg -s applaunch | grep Version -systemctl status APPLaunch.service --no-pager +systemctl --user status APPLaunch.service --no-pager ``` ### 13.3 Temporarily Disable the Launcher ```bash -sudo systemctl disable --now APPLaunch.service +systemctl --user disable --now APPLaunch.service ``` Restore: ```bash -sudo systemctl enable --now APPLaunch.service +systemctl --user enable --now APPLaunch.service ``` ## 14. Common Deployment Errors @@ -745,8 +764,8 @@ Then repackage and reinstall. Check: ```bash -systemctl status APPLaunch.service --no-pager -journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 ``` Common causes: @@ -775,7 +794,7 @@ Fix: sudo rm -rf /usr/share/APPLaunch/cache sudo mkdir -p /var/cache/APPLaunch sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` The current shared packager already writes `ln -sfn`; rebuild and reinstall the package to make the fix persistent. @@ -820,12 +839,12 @@ Investigation order: Commands: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true cd /usr/share/APPLaunch sudo ./bin/M5CardputerZero-APPLaunch ls -l /dev/fb0 sudo fuser -v /dev/fb0 2>/dev/null || true -journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 ``` ### 14.8 External Apps Cannot Start @@ -870,11 +889,11 @@ After installation: ```bash dpkg -s applaunch | grep -E 'Package|Version|Architecture' -systemctl status APPLaunch.service --no-pager -systemctl is-enabled APPLaunch.service +systemctl --user status APPLaunch.service --no-pager +systemctl --user is-enabled APPLaunch.service ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true ls -l /usr/share/APPLaunch/cache -journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 ``` Functional verification: @@ -900,7 +919,7 @@ file dist/M5CardputerZero-APPLaunch cd /home/nihao/w2T/github/launcher python3 scripts/debian_packager.py scp projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ -ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl status APPLaunch.service --no-pager' +ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl --user status APPLaunch.service --no-pager' ``` For fast replacement during development, use: diff --git a/docs/launcher-project-guide/10-extension-development-guide.md b/docs/launcher-project-guide/10-extension-development-guide.md index aba09ebb..6514f232 100644 --- a/docs/launcher-project-guide/10-extension-development-guide.md +++ b/docs/launcher-project-guide/10-extension-development-guide.md @@ -6,10 +6,10 @@ This chapter explains how to extend APPLaunch, focusing on four common change ty | Entry point | Purpose | | --- | --- | -| `projects/APPLaunch/main/ui/Launch.cpp` | Fixed app list, dynamic `.desktop` scanning, launching built-in pages or external processes | +| `projects/APPLaunch/main/ui/launch.cpp` | Fixed app list, dynamic `.desktop` scanning, launching built-in pages or external processes | | `projects/APPLaunch/main/ui/page_app/` | Built-in page implementation directory; pages are usually header-only `.hpp` files | | `projects/APPLaunch/main/ui/ui_app_page.hpp` | Shared page capabilities such as `AppPage`, top bar, `img_path()`, and `audio_path()` | -| `projects/APPLaunch/main/ui/components/generate_page_app_includes.py` | Automatically generates `page_app.h` before build and includes every `page_app/*.hpp` file | +| `projects/APPLaunch/main/ui/generate_page_app_includes.py` | Automatically generates `generated/page_app.h` before build and includes every `page_app/*.hpp` file | | `projects/APPLaunch/APPLaunch/` | Runtime asset tree; after packaging it maps to `/usr/share/APPLaunch/` on the device | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | Device-side `cp0_file_path()` path rules | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 development-host `cp0_file_path()` path rules | @@ -22,7 +22,7 @@ APPLaunch has two kinds of app sources: ## 2. Adding a Built-in Page -Built-in pages are suitable for features that run in the same process as Launcher, use LVGL directly, and need to share the input group, top bar, or status bar. Examples include Settings, Music, Files, Camera, and LoRa pages. +Built-in pages are suitable for features that run in the same process as Launcher, use LVGL directly, and need to share the input group, top bar, or status bar. Examples include Settings, Game, File, Camera, and LoRa pages. ### 2.1 Create the Page File @@ -80,7 +80,7 @@ private: Notes: - The page must inherit from `AppPage` so it can reuse mechanisms such as `screen()`, `input_group()`, and `navigate_home`. -- Prefer calling `navigate_home()` to return to the home page. Do not load the home screen directly, or `LaunchImpl` will not be able to release the current page object correctly. +- Prefer calling `navigate_home()` to return to the home page. Do not load the home screen directly, or `Launch` will not be able to release the current page object correctly. - If the page creates LVGL timers, file descriptors, threads, or peripheral handles, release them in the destructor. - Use 320x170 as the baseline page size. A common layout is a 20 px top bar and a 320x150 body. - Do not hard-code absolute asset paths. Use `img_path("xxx.png")` for images and `audio_path("xxx.wav")` for audio. @@ -90,12 +90,12 @@ Notes: `projects/APPLaunch/main/SConstruct` runs this script before building: ```python -ui/components/generate_page_app_includes.py +ui/generate_page_app_includes.py ``` -The script scans `projects/APPLaunch/main/ui/page_app/*.hpp` and generates `projects/APPLaunch/main/ui/page_app.h`. In most cases, as long as the file suffix is `.hpp`, it will be included automatically during the build. +The script scans `projects/APPLaunch/main/ui/page_app/*.hpp` and generates `projects/APPLaunch/build/generated/include/generated/page_app.h`. In most cases, as long as the file suffix is `.hpp`, it will be included automatically during the build. -If you check manually, `page_app.h` should contain: +If you check manually, `generated/page_app.h` should contain: ```cpp #include "page_app/ui_app_my_tool.hpp" @@ -103,7 +103,7 @@ If you check manually, `page_app.h` should contain: ### 2.3 Register the Page in the Home App List -Open `projects/APPLaunch/main/ui/Launch.cpp` and find `LaunchImpl::LaunchImpl()`. Register a built-in page like this: +Open `projects/APPLaunch/main/ui/launch.cpp` and find `Launch::Launch()`. Register a built-in page like this: ```cpp app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); @@ -136,16 +136,16 @@ Example: ```cpp static const char *app_keys[] = { "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", "MyTool" + "Game", "Math", "MyTool" }; static const char *app_labels[] = { "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", "My Tool" + "Game", "Math", "My Tool" }; ``` -`save_app_toggle()` stores the switch as `app_`, for example `app_MyTool=0`. Read the same key in `Launch.cpp`: +`save_app_toggle()` stores the switch as `app_`, for example `app_MyTool=0`. Read the same key in `launch.cpp`: ```cpp cp0_config_get_int("app_MyTool", 1) @@ -263,7 +263,7 @@ Type=Application ### 3.3 External App Launch Behavior -`Launch.cpp` supports two external-app launch modes: +`launch.cpp` supports two external-app launch modes: - `Terminal=true`: creates `UIConsolePage`, displays a PTY terminal inside the APPLaunch process, and executes `Exec`. - `Terminal=false`: calls `cp0_process_exec_blocking()` to start an external process. APPLaunch pauses the LVGL timer and input group, waits for the child process to exit, and then restores the home page. @@ -283,7 +283,7 @@ If it does not refresh: ```bash # Device side ls -l /usr/share/APPLaunch/applications -journalctl -u APPLaunch.service -f +journalctl --user -u APPLaunch.service -f # SDL2 development host: confirm APPLaunch/applications exists near the run directory find projects/APPLaunch -path '*APPLaunch/applications*' -maxdepth 5 -type f @@ -364,9 +364,9 @@ Steps: 1. Add an internal key such as `MyTool` to `app_keys` in `UISetupPage::menu_init()`. 2. Add a display label such as `My Tool` to `app_labels` in the same location. -3. Use the same key when registering the app in `Launch.cpp`: `APP_ENABLED("MyTool")`. +3. Use the same key when registering the app in `launch.cpp`: `APP_ENABLED("MyTool")`. 4. Open the Settings page, enter the `Launcher` menu, and toggle O/X. -5. If the list does not refresh after returning to the home page, restart APPLaunch. The current fixed/built-in list reads configuration when `LaunchImpl` is constructed. +5. If the list does not refresh after returning to the home page, restart APPLaunch. The current fixed/built-in list reads configuration when `Launch` is constructed. ### 5.2 Add a Regular Setting @@ -401,8 +401,8 @@ Common commands: ```bash sudo cat /var/lib/applaunch/settings -sudo sed -i 's/^app_Music=.*/app_Music=1/' /var/lib/applaunch/settings -sudo systemctl restart APPLaunch.service +sudo sed -i 's/^app_Game=.*/app_Game=1/' /var/lib/applaunch/settings +systemctl --user restart APPLaunch.service ``` If you add many configuration items, remember that the current maximum is 32 entries. After that limit, `cp0_config_set_*` returns directly and the setting will not be saved. diff --git a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md index 4de058b5..b5cb01be 100644 --- a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md +++ b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md @@ -47,15 +47,15 @@ CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed If started by systemd: ```bash -sudo systemctl status APPLaunch.service --no-pager -sudo journalctl -u APPLaunch.service -b --no-pager -sudo journalctl -u APPLaunch.service -f +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager +journalctl --user -u APPLaunch.service -f ``` If running the device binary manually: ```bash -sudo systemctl stop APPLaunch.service +systemctl --user stop APPLaunch.service cd /usr/share/APPLaunch sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch 2>&1 | tee /tmp/applaunch.log ``` @@ -101,7 +101,7 @@ sudo cat /var/lib/applaunch/settings Common configuration keys: -- `app_Music`, `app_Math`, `app_File`, `app_Camera`, etc.: Launcher page visibility toggles. +- `app_Game`, `app_Math`, `app_File`, `app_Camera`, etc.: Launcher page visibility toggles. - `brightness`: brightness. - `volume`: volume. - `dark_time`: screen-off timeout. @@ -118,16 +118,16 @@ Common configuration keys: | `[BOOT] cp0_lvgl_init() starting...` | `main.cpp` | Starting platform adaptation layer, display, input, audio, and other initialization | | `[BOOT] First frame flushed to fb0.` | `main.cpp` | First frame was forcibly flushed to the display device | | `Entering main loop` | `main.cpp` | Main loop has started | -| `[LAUNCHER] set panel icon` | `Launch.cpp` | Home icon was set successfully | -| `set panel icon missing/unreadable` | `Launch.cpp` | Icon path does not exist or is unreadable | -| `applications_load: opendir failed` | `Launch.cpp` | applications directory does not exist or is unreadable | -| `missing Name or Exec` | `Launch.cpp` | `.desktop` is missing required fields | -| `duplicate Exec` | `Launch.cpp` | `.desktop` has the same Exec as an existing app | -| `Launching terminal app` | `Launch.cpp` | Entering the built-in terminal page to run a command | -| `Launching external app` | `Launch.cpp` | Starting a non-terminal external program | +| `[LAUNCHER] set panel icon` | `launch.cpp` | Home icon was set successfully | +| `set panel icon missing/unreadable` | `launch.cpp` | Icon path does not exist or is unreadable | +| `applications_load: opendir failed` | `launch.cpp` | applications directory does not exist or is unreadable | +| `missing Name or Exec` | `launch.cpp` | `.desktop` is missing required fields | +| `duplicate Exec` | `launch.cpp` | `.desktop` has the same Exec as an existing app | +| `Launching terminal app` | `launch.cpp` | Entering the built-in terminal page to run a command | +| `Launching external app` | `launch.cpp` | Starting a non-terminal external program | | `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | Parent process read ESC while an external app was running | | `[cp0] Returned to launcher` | `cp0_app_process.cpp` | External app exited; preparing to return home | -| `[HOME_STATUS] connected=` | `Launch.cpp` | Home status bar refreshed WiFi/battery state | +| `[HOME_STATUS] connected=` | `launch.cpp` | Home status bar refreshed WiFi/battery state | ## 3. Black Screen Troubleshooting @@ -137,8 +137,8 @@ For black screens, first determine whether the process did not start, LVGL did n ```bash pgrep -a M5CardputerZero-APPLaunch -sudo systemctl status APPLaunch.service --no-pager -sudo journalctl -u APPLaunch.service -b --no-pager | tail -120 +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager | tail -120 ``` If there is no process: @@ -150,7 +150,7 @@ If there is no process: If the process restarts repeatedly: ```bash -sudo journalctl -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' +journalctl --user -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' ``` ### 3.2 Check the Startup Log Stage @@ -265,8 +265,8 @@ Input failures include the home page not responding, a built-in page not respond Check whether the correct input group is bound: - Home page: `UILaunchPage::bind_home_input_group()`. -- Built-in page: after creating the page, `Launch.cpp` calls `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())`. -- Return home: `LaunchImpl::lv_go_back_home()` rebinds the home input group. +- Built-in page: after creating the page, `launch.cpp` calls `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())`. +- Return home: `Launch::lv_go_back_home()` rebinds the home input group. When adding events to a built-in page, make sure the event is attached to the correct object and that the object belongs to the page input group. Refer to existing pages' `event_handler_init()` implementations. @@ -290,7 +290,7 @@ Related files: | File | Purpose | | --- | --- | | `ext_components/cp0_lvgl/include/compat/input_keys.h` | Compatible input key definitions | -| `projects/APPLaunch/main/include/keyboard_input.h` | APPLaunch private input header | +| `ext_components/cp0_lvgl/include/keyboard_input.h` | APPLaunch private input header | | `ext_components/cp0_lvgl/include/keyboard_input.h` | cp0_lvgl input interface | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | Device-side keyboard input implementation | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | SDL2 keyboard input implementation | @@ -317,7 +317,7 @@ External apps usually fail to return because the child process does not exit, th ### 6.1 Normal Return Path -`launch_Exec()` in `Launch.cpp`: +`launch_Exec()` in `launch.cpp`: 1. Shows Loading. 2. Sets `LVGL_RUN_FLAGE = 0`. @@ -343,7 +343,7 @@ If the child process is still running: Check: ```bash -sudo journalctl -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' +journalctl --user -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' ``` If there are no `[CP0-APP] evdev` logs: @@ -436,14 +436,14 @@ Common causes: | Symptom | Cause | Fix | | --- | --- | --- | -| `PageT not declared` | Page class name and registration name do not match, or `.hpp` was not included by `page_app.h` | Check `page_app.h` and rerun scons | +| `PageT not declared` | Page class name and registration name do not match, or `.hpp` was not included by `generated/page_app.h` | Check `generated/page_app.h` and rerun scons | | SDL2 build cannot find Linux headers | Page directly includes device-only headers | Wrap device-only code with `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` | | Linker cannot find symbols | Functions called by the new page were not added to component dependencies | Check `REQUIREMENTS`/`LDFLAGS` in `main/SConstruct` | | Duplicate definition | A header-only page defines non-inline global variables/functions | Convert them to class members, `static`, `inline`, or move them into a `.cpp` | -### 7.5 `page_app.h` Auto-generation Changes the Working Tree +### 7.5 `generated/page_app.h` Auto-generation Changes the Working Tree -`generate_page_app_includes.py` generates `page_app.h` sorted by filename. After adding or deleting `page_app/*.hpp`, a build may modify this file. This is expected, but before committing, confirm that the diff only contains the intended include-list change. +`generate_page_app_includes.py` generates `generated/page_app.h` sorted by filename. After adding or deleting `page_app/*.hpp`, a build may modify this file. This is expected, but before committing, confirm that the diff only contains the intended include-list change. ## 8. `.desktop` Load Failure Troubleshooting @@ -506,7 +506,7 @@ If APPLaunch starts as root, external apps normally attempt to lower privileges 1. Run `git status --short` to confirm the current change scope. 2. Build and run SDL2 to eliminate basic UI/syntax issues. 3. Check whether assets exist in both `projects/APPLaunch/APPLaunch` and device `/usr/share/APPLaunch`. -4. Watch `journalctl -u APPLaunch.service -f` to identify the startup stage. +4. Watch `journalctl --user -u APPLaunch.service -f` to identify the startup stage. 5. Use `evtest` to verify the input device and key codes. 6. Use `ps` to inspect external apps and process groups. 7. Check `/var/lib/applaunch/settings` to rule out settings toggles, brightness, or runtime user issues. diff --git a/docs/launcher-project-guide/12-common-modification-entry-points.md b/docs/launcher-project-guide/12-common-modification-entry-points.md index d256327a..2d447472 100644 --- a/docs/launcher-project-guide/12-common-modification-entry-points.md +++ b/docs/launcher-project-guide/12-common-modification-entry-points.md @@ -12,19 +12,19 @@ git status --short | Task | Main files/directories | Key points | Verification | | --- | --- | --- | --- | | Add a built-in page | `projects/APPLaunch/main/ui/page_app/` | Create `ui_app_xxx.hpp` and inherit from `AppPage` | Build with SDL2 and open the page | -| Register a built-in page on home | `projects/APPLaunch/main/ui/Launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | Icon appears in the home carousel | -| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/Launch.cpp` | Settings page writes `app_Key`, Launcher reads `APP_ENABLED("Key")` | Toggle in Settings, then restart or refresh home | +| Register a built-in page on home | `projects/APPLaunch/main/ui/launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | Icon appears in the home carousel | +| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/launch.cpp` | Settings page writes `app_Key`, Launcher reads `APP_ENABLED("Key")` | Toggle in Settings, then restart or refresh home | | Add external `.desktop` app | `projects/APPLaunch/APPLaunch/applications/` | Filename must end in `.desktop` and include `Name` and `Exec` | No skip logs; app appears on home | | Add icon | `projects/APPLaunch/APPLaunch/share/images/` | Built-in pages use `img_path()`, `.desktop` uses `Icon=share/images/xxx.png` | No `missing/unreadable` logs | | Add sound effect | `projects/APPLaunch/APPLaunch/share/audio/` | Pages use `audio_path()` and `cp0_signal_audio_api()` | Sound plays on device | | Add font | `projects/APPLaunch/APPLaunch/share/font/` | Use `launcher_fonts().get()` and confirm FreeType dependency | Page text uses the new font | -| Change home carousel layout | `projects/APPLaunch/main/ui/UILaunchPage.cpp`, `projects/APPLaunch/main/ui/UILaunchPage.h` | 5 slots, left/right switching, center card | Check animation and input in SDL2 | -| Change carousel animation | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | Card movement, scale, opacity, and other animations | Switch left/right repeatedly in SDL2 | -| Change home status bar | `projects/APPLaunch/main/ui/Launch.cpp`, `projects/APPLaunch/main/ui/ui.c` | `update_home_status_bar()` refreshes WiFi/time/battery | Check `[HOME_STATUS]` logs | +| Change home carousel layout | `projects/APPLaunch/main/ui/ui_launch_page.cpp`, `projects/APPLaunch/main/ui/ui_launch_page.h` | 5 slots, left/right switching, center card | Check animation and input in SDL2 | +| Change carousel animation | `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | Card movement, scale, opacity, and other animations | Switch left/right repeatedly in SDL2 | +| Change home status bar | `projects/APPLaunch/main/ui/launch.cpp`, `projects/APPLaunch/main/ui/ui.cpp` | `update_home_status_bar()` refreshes WiFi/time/battery | Check `[HOME_STATUS]` logs | | Change Settings menu | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | Add `MenuItem`/`SubItem` in `menu_init()` | Enter the SETTING page and test | | Change configuration saving logic | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Currently saves to `/var/lib/applaunch/settings`, max 32 entries | Inspect the settings file | | Change asset path rules | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | Consider device and SDL2 consistently | Check assets on both SDL2 and device | -| Change external app launch/return | `projects/APPLaunch/main/ui/Launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | External app starts, ESC returns | +| Change external app launch/return | `projects/APPLaunch/main/ui/launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | External app starts, ESC returns | | Change terminal apps | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY, command execution, input/output | Verify with a `Terminal=true` app | | Change input mapping | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | Device and SDL2 input differences | `evtest` + SDL2 keyboard | | Change startup flow | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`, `cp0_lvgl_init()`, `ui_init()`, main loop | Check `[BOOT]` logs | @@ -38,21 +38,21 @@ git status --short | Path | Purpose | | --- | --- | | `projects/APPLaunch/main/src/main.cpp` | APPLaunch process entry, initialization order, main loop, external app lock detection | -| `projects/APPLaunch/main/ui/ui.c` | Creates global LVGL UI objects; most `ui_*` globals originate here | +| `projects/APPLaunch/main/ui/ui.cpp` | Creates global LVGL UI objects; most `ui_*` globals originate here | | `projects/APPLaunch/main/ui/ui.cpp` | C++ UI initialization bridge | | `projects/APPLaunch/main/ui/ui.h` | UI global declarations and C/C++ shared interface | -| `projects/APPLaunch/main/ui/Launch.cpp` | App model, app list, launch logic, dynamic `.desktop` loading, status bar refresh | -| `projects/APPLaunch/main/ui/Launch.h` | Public wrapper class for `Launch` | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Home screen, carousel slots, input events, home-page behavior | -| `projects/APPLaunch/main/ui/UILaunchPage.h` | Home class interface, including panel/label/input group accessors | +| `projects/APPLaunch/main/ui/launch.cpp` | App model, app list, launch logic, dynamic `.desktop` loading, status bar refresh | +| `projects/APPLaunch/main/ui/launch.h` | Public wrapper class for `Launch` | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Home screen, carousel slots, input events, home-page behavior | +| `projects/APPLaunch/main/ui/ui_launch_page.h` | Home class interface, including panel/label/input group accessors | | `projects/APPLaunch/main/ui/ui_loading.cpp` | Loading overlay show/hide | | `projects/APPLaunch/main/ui/ui_global_hint.cpp` | Global hint overlay | -| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | LVGL OS/thread helpers | -| `projects/APPLaunch/main/ui/Animation/` | Home carousel animation implementation | +| `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp` | LVGL OS/thread helpers | +| `projects/APPLaunch/main/ui/animation/` | Home carousel animation implementation | | `projects/APPLaunch/main/ui/ui_app_page.hpp` | Built-in page base class, top bar, shared asset path helpers | -| `projects/APPLaunch/main/ui/page_app.h` | Auto-generated built-in page include aggregate | +| `projects/APPLaunch/build/generated/include/generated/page_app.h` | Auto-generated built-in page include aggregate | | `projects/APPLaunch/main/ui/page_app/` | Built-in page implementation directory | -| `projects/APPLaunch/main/include/` | APPLaunch private headers and compatible input headers | +| `ext_components/cp0_lvgl/include/` | Shared CP0/LVGL headers, including keyboard and compatibility input headers | ## 3. Built-in Page Entry Table @@ -60,9 +60,9 @@ git status --short | --- | --- | --- | --- | | GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | | SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings page, including app toggles, brightness, volume, WiFi, camera, etc. | -| MUSIC | `projects/APPLaunch/main/ui/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | Music page | +| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | | Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass page | -| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP information panel, enabled on device | +| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_ip_panel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP information panel, enabled on device | | FILE | `projects/APPLaunch/main/ui/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | File page, enabled on device | | SSH | `projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH page, enabled on device | | MESH | `projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh page, enabled on device | @@ -72,7 +72,7 @@ git status --short | TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game, enabled on device | | CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`, used by bash, python, and `Terminal=true` apps | -Fixed registration entry in `LaunchImpl::LaunchImpl()`: +Fixed registration entry in `Launch::Launch()`: ```cpp app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); @@ -88,11 +88,11 @@ app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v Launcher | `app_` | `save_app_toggle()` in `ui_app_setup.hpp`, `APP_ENABLED()` in `Launch.cpp` | +| App visibility toggle | SETTING -> Launcher | `app_` | `save_app_toggle()` in `ui_app_setup.hpp`, `APP_ENABLED()` in `launch.cpp` | | Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | | Screen-off timeout | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | | Volume | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`, `cp0_volume_read/write()` | @@ -190,23 +190,23 @@ Configuration implementation: | SDL2 build | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | | SDL2 run | `cd projects/APPLaunch && ./dist/M5CardputerZero-APPLaunch` | | Cross build | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | -| View systemd status | `sudo systemctl status APPLaunch.service --no-pager` | -| Follow logs | `sudo journalctl -u APPLaunch.service -f` | -| View boot logs | `sudo journalctl -u APPLaunch.service -b --no-pager` | +| View systemd status | `systemctl --user status APPLaunch.service --no-pager` | +| Follow logs | `journalctl --user -u APPLaunch.service -f` | +| View boot logs | `journalctl --user -u APPLaunch.service -b --no-pager` | | Check assets | `find /usr/share/APPLaunch -maxdepth 3 -type f | sort` | | Check `.desktop` files | `find /usr/share/APPLaunch/applications -maxdepth 1 -type f -name '*.desktop' -print -exec sed -n '1,80p' {} \;` | | Check settings | `sudo cat /var/lib/applaunch/settings` | | Check input devices | `ls -l /dev/input/by-path/ && sudo evtest` | | Check external app processes | `ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|sh -c|M5CardputerZero'` | | Check dynamic libraries | `ldd /usr/share/APPLaunch/bin/my_app` | -| Check icon logs | `sudo journalctl -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | +| Check icon logs | `journalctl --user -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | ## 10. Pre-/Post-change Checklist | Stage | Check item | | --- | --- | | Before change | Run `git status --short` and confirm which files already have changes from others | -| After adding a page | Confirm the `.hpp` file is in `page_app/`, and the class name matches the registration in `Launch.cpp` | +| After adding a page | Confirm the `.hpp` file is in `page_app/`, and the class name matches the registration in `launch.cpp` | | After adding assets | Confirm files can be found in both the source tree and device `/usr/share/APPLaunch` | | After adding `.desktop` | File suffix is `.desktop`, with `[Desktop Entry]`, `Name`, and `Exec` | | After changing settings | `/var/lib/applaunch/settings` contains the correct key and has not exceeded the configuration entry limit | diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" index 8589a3a7..2f44a93a 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" @@ -68,8 +68,8 @@ APPLaunch 首页、状态栏、轮播、应用管理器 | --- | --- | | `projects/APPLaunch` | 启动器主工程 | | `projects/APPLaunch/main/src/main.cpp` | APPLaunch 入口和 LVGL 主循环 | -| `projects/APPLaunch/main/ui/Launch.cpp` | 应用列表、启动逻辑、状态栏刷新 | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 UI、轮播、首页按键 | +| `projects/APPLaunch/main/ui/launch.cpp` | 应用列表、启动逻辑、状态栏刷新 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | 首页 UI、轮播、首页按键 | | `projects/APPLaunch/main/ui/page_app` | 内置页面实现 | | `projects/APPLaunch/APPLaunch` | 打包进运行环境的资源树 | | `ext_components/cp0_lvgl` | 平台适配层,封装文件、进程、输入、系统接口 | diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" index 3e7e48cd..11c83b6c 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" @@ -167,27 +167,27 @@ projects/APPLaunch/main/ ```text main/ui/ ├── ui.cpp / ui.h -├── Launch.cpp / Launch.h -├── UILaunchPage.cpp / UILaunchPage.h +├── launch.cpp / launch.h +├── ui_launch_page.cpp / ui_launch_page.h ├── ui_app_page.hpp -├── page_app.h +├── generated/page_app.h ├── generate_page_app_includes.py ├── ui_loading.* ├── ui_global_hint.* -├── zero_lvgl_os.* -├── Animation/ +├── LauncherUiRuntime.* +├── animation/ └── page_app/ ``` | 文件/目录 | 作用 | | --- | --- | | `ui.c` / `ui.cpp` / `ui.h` | UI 初始化、全局对象、C/C++ 桥接 | -| `Launch.cpp` | 应用管理器,实现应用列表、启动、状态栏刷新、目录监听 | -| `UILaunchPage.cpp` | 首页 UI 创建、轮播槽位、按键处理、启动动画 | +| `launch.cpp` | 应用管理器,实现应用列表、启动、状态栏刷新、目录监听 | +| `ui_launch_page.cpp` | 首页 UI 创建、轮播槽位、按键处理、启动动画 | | `ui_loading.cpp` | Loading 遮罩 | | `ui_global_hint.cpp` | 全局提示 | -| `zero_lvgl_os.cpp` | LVGL OS/线程相关辅助 | -| `Animation/` | 首页轮播动画实现 | +| `LauncherUiRuntime.cpp` | LVGL OS/线程相关辅助 | +| `animation/` | 首页轮播动画实现 | | `components/` | 页面基类、组件、自定义页面 | ### 2.5 `components/page_app/` 内置页面目录 @@ -201,12 +201,12 @@ main/ui/page_app/ ├── ui_app_game.hpp ├── ui_app_lora.hpp ├── ui_app_mesh.hpp -├── ui_app_music.hpp +├── ui_app_game.hpp ├── ui_app_rec.hpp ├── ui_app_setup.hpp ├── ui_app_ssh.hpp ├── ui_app_tank_battle.hpp -└── ui_app_IpPanel.hpp +└── ui_app_ip_panel.hpp ``` 这些页面通常以 header-only 方式实现,便于被 `generate_page_app_includes.py` 自动包含。 @@ -228,7 +228,7 @@ ui_init() ├── ui_loading └── page_app/* -LaunchImpl +Launch ├── UILaunchPage::panel()/label() ├── page_v ├── cp0_file_path() diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" index f773c372..1237a086 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/02-\350\277\220\350\241\214\346\241\206\346\236\266\344\270\216\345\220\257\345\212\250\346\265\201\347\250\213.md" @@ -1,6 +1,6 @@ # 02 - 运行框架与启动流程 -本章说明 APPLaunch 从进程入口到首页首帧显示的完整路径,重点参考 `projects/APPLaunch/main/src/main.cpp`、`projects/APPLaunch/main/ui/ui.cpp`、`projects/APPLaunch/main/ui/zero_lvgl_os.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`。 +本章说明 APPLaunch 从进程入口到首页首帧显示的完整路径,重点参考 `projects/APPLaunch/main/src/main.cpp`、`projects/APPLaunch/main/ui/ui.cpp`、`projects/APPLaunch/main/ui/launcher_ui_runtime.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.cpp`。 ## 1. 运行框架概览 @@ -18,17 +18,17 @@ APPLaunch 进程 │ ├── lv_timer_handler() │ └── usleep(5000) └── ui_init() - └── zero_lvgl_os() - ├── creat_display() + └── LauncherUiRuntime() + ├── create_display() ├── 创建 Launch / UILaunchPage 绑定对象 - └── create_launcher_home() + └── build_launcher_home() ``` 核心特点: - LVGL 初始化和平台适配初始化只在 `main()` 中执行一次。 -- 首页 UI 的创建由 `zero_lvgl_os` 驱动,实际对象在 `UILaunchPage::create_screen()` 中创建。 -- `Launch` / `LaunchImpl` 负责应用列表、启动方式、状态栏刷新、动态应用目录监听。 +- 首页 UI 的创建由 `LauncherUiRuntime` 驱动,实际对象在 `UILaunchPage::create_screen()` 中创建。 +- `Launch` / `Launch` 负责应用列表、启动方式、状态栏刷新、动态应用目录监听。 - 首页首帧在 `ui_init()` 后立即通过 `lv_obj_invalidate()` + `lv_refr_now(NULL)` 强制刷新,避免启动后黑屏等待下一次自然刷新。 ## 2. 入口文件与关键源码路径 @@ -36,10 +36,10 @@ APPLaunch 进程 | 路径 | 作用 | | --- | --- | | `projects/APPLaunch/main/src/main.cpp` | 进程入口、LVGL 主循环、外部应用运行锁检测 | -| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`,创建全局 `zero_lvgl_os home` | -| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | 设置 LVGL theme、创建首页、创建 Launch 绑定对象 | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 screen、启动 GIF、首页加载、输入 group | -| `projects/APPLaunch/main/ui/Launch.cpp` | 应用管理器、外部/终端/内置页面启动、状态栏 timer | +| `projects/APPLaunch/main/ui/ui.cpp` | `ui_init()`,创建全局 `LauncherUiRuntime home` | +| `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp` | 设置 LVGL theme、创建首页、创建 Launch 绑定对象 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | 首页 screen、启动 GIF、首页加载、输入 group | +| `projects/APPLaunch/main/ui/launch.cpp` | 应用管理器、外部/终端/内置页面启动、状态栏 timer | | `ext_components/cp0_lvgl` | `cp0_lvgl_init()`、文件路径、输入、进程、系统能力封装 | ## 3. `main()` 启动流程 @@ -110,41 +110,41 @@ lv_refr_now(NULL); `ui_init()` 位于 `projects/APPLaunch/main/ui/ui.cpp`: ```cpp -std::unique_ptr home; +std::unique_ptr home; void ui_init(void) { - home = std::make_unique(); + home = std::make_unique(); } ``` -`zero_lvgl_os` 构造函数继续执行: +`LauncherUiRuntime` 构造函数继续执行: ```cpp -zero_lvgl_os::zero_lvgl_os() +LauncherUiRuntime::LauncherUiRuntime() { - creat_display(); + create_display(); launch_ = std::make_shared(); launch_page_ = std::make_shared(launch_); launch_->set_launch_page(launch_page_); - create_launcher_home(); + build_launcher_home(); } ``` 需要注意这里的顺序: -1. `creat_display()` 先创建字体管理器并设置 LVGL theme。 +1. `create_display()` 先创建字体管理器并设置 LVGL theme。 2. 构造 `Launch` 与 `UILaunchPage`,并通过 `Launch::set_launch_page()` 建立双向协作关系。 -3. `create_launcher_home()` 创建首页 screen、调用 `Launch::bind_ui()` 建立应用列表、初始化 input group,并显示首页或启动 GIF。 +3. `build_launcher_home()` 创建首页 screen、调用 `Launch::bind_ui()` 建立应用列表、初始化 input group,并显示首页或启动 GIF。 ## 5. Display / Theme 初始化 -`zero_lvgl_os::creat_display()` 的核心代码: +`LauncherUiRuntime::create_display()` 的核心代码: ```cpp -void zero_lvgl_os::creat_display() +void LauncherUiRuntime::create_display() { fonts_ = std::make_shared(); @@ -163,14 +163,14 @@ void zero_lvgl_os::creat_display() - `LauncherFonts` 是首页和页面共享的 FreeType 字体缓存,入口函数为 `launcher_fonts()`。 - `lv_disp_get_default()` 依赖 `cp0_lvgl_init()` 已经注册显示设备。 -- theme 只是基础主题,首页多数控件仍在 `UILaunchPage.cpp` 中手工设置尺寸、颜色、背景图和字体。 +- theme 只是基础主题,首页多数控件仍在 `ui_launch_page.cpp` 中手工设置尺寸、颜色、背景图和字体。 ## 6. 首页创建与显示流程 -`zero_lvgl_os::create_launcher_home()` 是首页显示的主要入口: +`LauncherUiRuntime::build_launcher_home()` 是首页显示的主要入口: ```cpp -void zero_lvgl_os::create_launcher_home() +void LauncherUiRuntime::build_launcher_home() { LV_EVENT_GET_COMP_CHILD = lv_event_register_id(); @@ -273,21 +273,21 @@ main() -> cp0_lvgl_init() -> register LV_EVENT_KEYBOARD -> ui_init() - -> new zero_lvgl_os - -> creat_display() + -> new LauncherUiRuntime + -> create_display() -> new LauncherFonts -> lv_disp_get_default() -> lv_theme_default_init() -> new Launch -> new UILaunchPage(Launch) -> Launch::set_launch_page() - -> create_launcher_home() + -> build_launcher_home() -> register LV_EVENT_GET_COMP_CHILD -> launch_page_->create_screen() -> home_base::creat_Top_UI() -> create_app_container(content_container()) -> launch_->bind_ui() - -> new LaunchImpl + -> new Launch -> 注册固定/动态应用并写入首页槽位 -> 创建状态栏和应用目录监听 timer -> launch_page_->init_input_group() diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" index d6e81ad5..9f70d6de 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/03-UI\346\241\206\346\236\266\344\270\216\351\246\226\351\241\265\350\275\256\346\222\255.md" @@ -1,6 +1,6 @@ # 03 - UI框架与首页轮播 -本章说明 APPLaunch 首页 UI 的组织方式、轮播卡片的数据流和按键事件。重点参考 `projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.h`、`projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp`、`projects/APPLaunch/main/ui/Launch.cpp`。 +本章说明 APPLaunch 首页 UI 的组织方式、轮播卡片的数据流和按键事件。重点参考 `projects/APPLaunch/main/ui/ui_launch_page.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.h`、`projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp`、`projects/APPLaunch/main/ui/launch.cpp`。 ## 1. UI 框架概览 @@ -22,16 +22,16 @@ UILaunchPage : home_base └── 5 个页点 dot ``` -首页复用 `home_base` / `AppPageRoot` 页面框架创建根 screen、状态栏和输入 group。`UILaunchPage.cpp` 负责在继承来的内容容器中填充轮播,并绑定 LVGL 回调。 +首页复用 `home_base` / `AppPageRoot` 页面框架创建根 screen、状态栏和输入 group。`ui_launch_page.cpp` 负责在继承来的内容容器中填充轮播,并绑定 LVGL 回调。 ## 2. 关键源码路径 | 路径 | 说明 | | --- | --- | -| `projects/APPLaunch/main/ui/UILaunchPage.h` | 首页类定义、轮播元素枚举、`carousel_elements` 数组 | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 screen 创建、轮播切换、键盘事件、启动 GIF、字体缓存 | -| `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | 轮播左右切换动画 | -| `projects/APPLaunch/main/ui/Launch.cpp` | 切换后填充新卡片内容、启动当前应用、刷新状态栏 | +| `projects/APPLaunch/main/ui/ui_launch_page.h` | 首页类定义、轮播元素枚举、`carousel_elements` 数组 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | 首页 screen 创建、轮播切换、键盘事件、启动 GIF、字体缓存 | +| `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | 轮播左右切换动画 | +| `projects/APPLaunch/main/ui/launch.cpp` | 切换后填充新卡片内容、启动当前应用、刷新状态栏 | | `projects/APPLaunch/main/ui/ui.h` | 首页布局常量,例如 `LABEL_Y_CENTER`、`BORDER_COLOR_CENTER` | ## 3. `UILaunchPage` 的职责 @@ -84,8 +84,8 @@ private: 它有两类职责: -- 静态兼容职责:保留共享的 `carousel_elements` 数组、维护首页输入 group 桥接、提供 `Launch.cpp` 使用的 `panel()` / `label()` 访问器。 -- 实例职责:持有 `Launch` 指针,拥有页面级 UI 状态,处理 LVGL 事件,并把轮播更新和应用启动转发给 `LaunchImpl`。 +- 静态兼容职责:保留共享的 `carousel_elements` 数组、维护首页输入 group 桥接、提供 `launch.cpp` 使用的 `panel()` / `label()` 访问器。 +- 实例职责:持有 `Launch` 指针,拥有页面级 UI 状态,处理 LVGL 事件,并把轮播更新和应用启动转发给 `Launch`。 LVGL 仍然要求 C 风格静态回调,但当前代码不再依赖全局状态做常规事件分发。每个回调都通过 LVGL user data 取回所属页面实例: @@ -122,7 +122,7 @@ std::array UILaunchPage::carousel_elements = {}; ``` -枚举定义在 `UILaunchPage.h`: +枚举定义在 `ui_launch_page.h`: ```cpp enum LauncherCarouselElement : size_t { @@ -171,7 +171,7 @@ lv_obj_t *UILaunchPage::label(size_t slot) ## 5. 标准槽位布局 -`UILaunchPage.cpp` 中用 `CarouselSlot` 描述轮播静态布局: +`ui_launch_page.cpp` 中用 `CarouselSlot` 描述轮播静态布局: ```cpp struct CarouselSlot { @@ -228,7 +228,7 @@ void UILaunchPage::create_screen() - `ui_Panel1` 时间背景图 `status_time_background.png` 与 `ui_timeLabel`。 - `ui_batteryPanel` 电池背景图 `status_battery_background.png`、`ui_Bar1`、`ui_powerLabel`。 -状态栏数据刷新不在 `UILaunchPage`,而在 `LaunchImpl::update_home_status_bar()`: +状态栏数据刷新不在 `UILaunchPage`,而在 `Launch::update_home_status_bar()`: ```cpp cp0_wifi_status_t wifi = cp0_wifi_get_status(); @@ -236,7 +236,7 @@ cp0_time_str(time_buf, sizeof(time_buf)); cp0_battery_info_t bat = cp0_battery_read(); ``` -`LaunchImpl` 构造时创建 5 秒 timer: +`Launch` 构造时创建 5 秒 timer: ```cpp status_timer = lv_timer_create(home_status_timer_cb, 5000, this); @@ -263,7 +263,7 @@ lv_obj_clear_flag(app_container, - 5 个卡片:中心 100x100,左右 80x80,远端 61x61 且隐藏。 - 左右按钮:背景图 `carousel_left_arrow.png` / `carousel_right_arrow.png`。 -默认标题只是 UI 初始占位,真正内容会由 `LaunchImpl` 初始化应用列表后写入。 +默认标题只是 UI 初始占位,真正内容会由 `Launch` 初始化应用列表后写入。 ## 7. 轮播切换流程 @@ -374,7 +374,7 @@ void UILaunchPage::run_pending_switch() ## 9. 应用数据如何写入轮播 -`LaunchImpl` 维护 `current_app` 和 `app_list`。切换时,`UILaunchPage` 只传入要被复用的 panel/label;具体要显示哪个应用由 `LaunchImpl` 计算。 +`Launch` 维护 `current_app` 和 `app_list`。切换时,`UILaunchPage` 只传入要被复用的 panel/label;具体要显示哪个应用由 `Launch` 计算。 向左切换后填充新右端: @@ -512,5 +512,5 @@ UILaunchPage::create_screen() - `switch_left()` / `switch_right()` 的命名描述动画方向,不一定等同于用户按键方向;当前 `KEY_LEFT` 调用 `switch_right()`,`KEY_RIGHT` 调用 `switch_left()`。 - 动画期间只记录一个 `pending_switch_` 枚举值,连续快速按键不会无限排队。 - 首页卡片点击事件都绑定到 `on_app_clicked()`,再桥接到 `launch_selected_app()`,但正常交互主要通过中心选择 + Enter 启动;如果开放鼠标/触摸,需要确认点击非中心卡片时是否符合预期。 -- 状态栏对象由 `UILaunchPage` 创建,但刷新 timer 在 `LaunchImpl` 构造时创建;如果只创建首页但没有执行 `Launch::bind_ui()`,应用列表和状态栏刷新不会启动。 +- 状态栏对象由 `UILaunchPage` 创建,但刷新 timer 在 `Launch` 构造时创建;如果只创建首页但没有执行 `Launch::bind_ui()`,应用列表和状态栏刷新不会启动。 - 新增或调整轮播槽位时,要同步修改 `CAROUSEL_SLOTS`、`create_app_container()` 初始位置、动画文件中的槽位定义,避免动画结束跳变。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" index cfe6f8a2..68967386 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" @@ -1,6 +1,6 @@ # 04 - 应用模型与启动机制 -本章说明 APPLaunch 如何把内置页面、终端命令、外部独立程序统一成一个应用列表,以及用户按 Enter 后如何启动应用。重点参考 `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/Launch.h`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/page_app/*`。 +本章说明 APPLaunch 如何把内置页面、终端命令、外部独立程序统一成一个应用列表,以及用户按 Enter 后如何启动应用。重点参考 `projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/launch.h`、`projects/APPLaunch/main/ui/ui_launch_page.cpp`、`projects/APPLaunch/main/ui/page_app/*`。 ## 1. 应用模型概览 @@ -11,7 +11,7 @@ app ├── Name 显示标题 ├── Icon 图标路径 ├── Exec 外部命令,内置页面可为空 -└── launch(LaunchImpl*) 启动动作 +└── launch(Launch*) 启动动作 ``` 统一后,首页轮播不需要关心应用类型,只显示 `Name` 和 `Icon`;按 Enter 时只调用当前 `app.launch()`。 @@ -19,7 +19,7 @@ app ```text 首页中心卡片 -> Launch::launch_app() - -> LaunchImpl::launch_app() + -> Launch::launch_app() -> app.launch(this) ├── 内置页面:new PageT + lv_disp_load_scr() ├── 终端应用:UIConsolePage + PTY exec() @@ -30,67 +30,34 @@ app | 路径 | 说明 | | --- | --- | -| `projects/APPLaunch/main/ui/Launch.h` | `Launch` 对外门面,隐藏 `LaunchImpl` | -| `projects/APPLaunch/main/ui/Launch.cpp` | `app`、`LaunchImpl`、应用列表、启动逻辑、`.desktop` 扫描 | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | Enter / click 事件转发到 `Launch::launch_app()` | +| `projects/APPLaunch/main/ui/launch.h` | `Launch` 公共接口和应用模型声明 | +| `projects/APPLaunch/main/ui/launch.cpp` | `app`、`Launch`、应用列表、启动逻辑、`.desktop` 扫描 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Enter / click 事件转发到 `Launch::launch_app()` | | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | 终端页面 `UIConsolePage` | | `projects/APPLaunch/main/ui/page_app/*.hpp` | 各内置页面,例如设置、音乐、文件、相机、LoRa | | `projects/APPLaunch/APPLaunch/applications/` | 运行时 `.desktop` 应用描述目录 | | `ext_components/cp0_lvgl` | 进程启动、PTY、目录监听、路径解析等底层能力 | -## 3. `Launch` 与 `LaunchImpl` 分层 - -`Launch.h` 对外暴露很少: - -```cpp -class Launch -{ -public: - void bind_ui(); - void set_launch_page(std::shared_ptr launch_page); - void update_left_slot(lv_obj_t *panel, lv_obj_t *label); - void update_right_slot(lv_obj_t *panel, lv_obj_t *label); - void launch_app(); - -private: - std::unique_ptr impl_; - std::shared_ptr launch_page_; -}; -``` - -真正逻辑在 `LaunchImpl`: +## 3. `Launch` 运行时状态 -```cpp -class LaunchImpl -{ -private: - int current_app = 2; - cp0_watcher_t dir_watcher = NULL; - lv_timer_t *watch_timer = nullptr; - lv_timer_t *status_timer = nullptr; - int fixed_count; - -public: - std::list app_list; - std::shared_ptr app_Page; - std::shared_ptr home_Page; -}; -``` +`launch.h` 直接暴露 `Launch` 类。当前代码已经没有独立的 `LaunchImpl` 分层;应用列表、目录 watcher、当前页面持有对象和轮播辅助逻辑都在 `Launch` 中。 -字段含义: +重要私有状态包括: | 字段 | 说明 | | --- | --- | -| `current_app` | 当前中心卡片对应的应用索引,默认 `2`,即初始中心为 CLI | -| `app_list` | 所有固定应用和动态 `.desktop` 应用 | -| `fixed_count` | 固定应用数量,动态重载时保留此前元素 | +| `launch_page_` | 指向首页 `UILaunchPage` 的 weak reference | +| `current_app` | 当前中心卡片对应的应用索引。默认是 `2`,因此初始中心卡片是 CLI | +| `dir_watcher_` / `watch_timer_` | 监听 `applications/` 目录并重载动态应用 | +| `fixed_count` | 内置/固定应用数量。动态重载会保留这一段之前的元素 | +| `app_list` | 内置入口加动态 `.desktop` 入口 | | `app_Page` | 当前内置页面或终端页面的生命周期持有者 | -| `dir_watcher` / `watch_timer` | 监听 `applications/` 目录变化并重载动态应用 | -| `status_timer` | 首页状态栏刷新 timer | + +`Launch::bind_ui()` 会构建初始列表、加载动态 `.desktop` 文件、启动目录 watcher timer,并注册应用注册表变更回调。 ## 4. `app` 结构与三类启动方式 -`app` 定义在 `Launch.cpp`: +`app` 定义在 `launch.cpp`: ```cpp struct app @@ -99,7 +66,7 @@ struct app std::string Icon; std::string Exec; - std::function launch; + std::function launch; app(std::string name, std::string icon, std::string exec, bool terminal); app(std::string name, std::string icon, std::string exec, bool terminal, bool sysplause); @@ -114,32 +81,32 @@ struct app | 类型 | 构造方式 | 启动函数 | 示例 | | --- | --- | --- | --- | -| 内置页面 | `page_v` | 构造页面并 `lv_disp_load_scr()` | `GAME`、`SETTING`、`MUSIC` | +| 内置页面 | `page_v` | 构造页面并 `lv_disp_load_scr()` | `GAME`、`SETTING`、`Compass` | | 终端命令 | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`、`CLI` | | 外部进程 | `exec, terminal=false` | `launch_Exec()` | AppStore、Calculator | ## 5. 固定应用注册 -`LaunchImpl` 构造函数先注册固定入口: +内置入口在 `launch.cpp` 的 `kBuiltinApps[]` 中声明。每个入口都带有一个 `AppDescriptor`,包含显示名、图标、配置 key、是否可在设置页配置,以及是否始终启用。 -```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", - false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); -``` - -随后把前 5 个应用写入首页 5 个槽位: +代表性入口: ```cpp -lv_label_set_text(UILaunchPage::label(0), it->Name.c_str()); -panel_set_icon(UILaunchPage::panel(0), it->Icon.c_str()); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, + {{"MATH", "math_100.png", "app_Math", true, false}, + "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false, true, false, nullptr}, +}; ``` -初始状态: +`Launch::rebuild_builtin_apps()` 会清空列表,通过 `launcher_app_registry_is_enabled()` 追加已启用的内置入口,并更新 `fixed_count`。设置页通过 `launcher_app_registry_set_enabled()` 保存开关,然后触发 `Launch::applications_reload()`。 + +前 5 个入口初始化首页 5 个轮播槽: ```text slot 0 far-left : Python @@ -150,23 +117,6 @@ slot 4 far-right: SETTING current_app : 2 ``` -之后根据设置项和平台条件继续追加可选应用: - -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("Music")) - app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); - -if (APP_ENABLED("Math")) - app_list.emplace_back("MATH", img_path("math_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-Calculator", false); - -app_list.emplace_back("Compass", img_path("compass_needle_80.png"), page_v); -``` - -Linux 设备端还会按配置追加 `IP_PANEL`、`FILE`、`SSH`、`MESH`、`REC`、`CAMERA`、`LORA`、`TANK` 等页面。 - ## 6. 内置页面启动机制 内置页面通过模板构造: @@ -176,7 +126,7 @@ template app::app(std::string name, std::string icon, page_t) : Name(std::move(name)), Icon(std::move(icon)) { - launch = [](LaunchImpl *self) + launch = [](Launch *self) { ui_loading_show("Loading..."); lv_refr_now(NULL); @@ -185,7 +135,7 @@ app::app(std::string name, std::string icon, page_t) self->app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); - p->navigate_home = std::bind(&LaunchImpl::go_back_home, self); + p->navigate_home = std::bind(&Launch::go_back_home, self); ui_loading_hide(); }; @@ -203,14 +153,14 @@ app::app(std::string name, std::string icon, page_t) ```text Enter - -> app.launch(LaunchImpl*) + -> app.launch(Launch*) -> ui_loading_show("Loading...") -> lv_refr_now(NULL) -> make_shared() -> app_Page = p 保持生命周期 -> lv_disp_load_scr(p->screen()) -> 输入设备切换到 p->input_group() - -> p->navigate_home = LaunchImpl::go_back_home + -> p->navigate_home = Launch::go_back_home -> ui_loading_hide() ``` @@ -228,7 +178,7 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); - p->navigate_home = std::bind(&LaunchImpl::go_back_home, this); + p->navigate_home = std::bind(&Launch::go_back_home, this); p->terminal_sysplause = sysplause; ui_loading_hide(); @@ -331,7 +281,7 @@ void go_back_home() static void lv_go_back_home(void *arg) { - auto self = (LaunchImpl *)arg; + auto self = (Launch *)arg; lv_timer_enable(true); if (self->launch_page_) self->launch_page_->show_home_screen(); @@ -406,7 +356,7 @@ if (!in_list) ## 11. 动态应用目录监听与重载 -`LaunchImpl` 构造末尾: +`Launch` 构造末尾: ```cpp fixed_count = app_list.size(); @@ -478,7 +428,7 @@ static void panel_set_icon(lv_obj_t *panel, const char *src) -> launch_->launch_app() -> Launch::launch_app() -> impl_->launch_app() - -> LaunchImpl::launch_app() + -> Launch::launch_app() -> auto it = std::next(app_list.begin(), current_app) -> it->launch(this) -> 根据 app 类型进入内置页面 / 终端页面 / 外部进程 @@ -486,7 +436,7 @@ static void panel_set_icon(lv_obj_t *panel, const char *src) ## 14. 注意事项 -- `Launch::bind_ui()` 必须被调用后才会创建 `LaunchImpl`,否则首页可以显示,但应用列表更新、状态栏 timer、目录监听和启动逻辑都不会工作。 +- `Launch::bind_ui()` 必须被调用后才会创建 `Launch`,否则首页可以显示,但应用列表更新、状态栏 timer、目录监听和启动逻辑都不会工作。 - `current_app` 默认是 `2`,固定应用前 5 个入口的顺序会影响初始中心卡片;调整固定应用顺序时要同步考虑首页初始体验。 - 内置页面构造期间如果耗时较长,应保留 `ui_loading_show()` + `lv_refr_now()`,否则用户看不到即时反馈。 - 外部应用启动时会暂停 APPLaunch 的 LVGL timer 和输入 group;外部程序必须正常退出或响应 HOME 逻辑,否则用户会感觉卡在外部界面。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" index b3086f5b..1c1e952d 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" @@ -1,15 +1,15 @@ # 05 - 内置页面框架 -本章说明 APPLaunch 内置页面的类层次、生命周期、页面列表、页面注册方式以及新增页面时应遵守的约定。重点源码在 `projects/APPLaunch/main/ui/ui_app_page.hpp`、`projects/APPLaunch/main/ui/page_app/*.hpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.cpp`。 +本章说明 APPLaunch 内置页面的类层次、生命周期、页面列表、页面注册方式以及新增页面时应遵守的约定。重点源码在 `projects/APPLaunch/main/ui/ui_app_page.hpp`、`projects/APPLaunch/main/ui/page_app/*.hpp`、`projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.cpp`。 ## 1. 内置页面是什么 内置页面是编译进 APPLaunch 进程内的 LVGL 页面类。它和 `.desktop` 外部应用不同: - 内置页面直接创建 `lv_obj_t *root_screen_`,通过 `lv_disp_load_scr(page->screen())` 切换到自己的 screen。 -- 页面对象保存在 `LaunchImpl::app_Page`,退出时由 `navigate_home` 回调异步释放。 +- 页面对象保存在 `Launch::app_Page`,退出时由 `navigate_home` 回调异步释放。 - 页面与首页共享 APPLaunch 进程、LVGL 主循环、输入线程、资源解析和 `cp0_lvgl_app.h` 系统接口。 -- 页面通常是 header-only,放在 `projects/APPLaunch/main/ui/page_app/`,由 `components/page_app.h` 汇总包含。 +- 页面通常是 header-only,放在 `projects/APPLaunch/main/ui/page_app/`,由 `build/generated/include/generated/page_app.h` 汇总包含。 简化关系: @@ -17,7 +17,7 @@ UILaunchPage 首页轮播 | v -LaunchImpl::launch_app() +Launch::launch_app() | +-- 外部命令: cp0_process_exec_blocking() +-- 终端命令: UIConsolePage + PTY @@ -62,7 +62,7 @@ public: - `root_screen_` 是页面自己的顶层 screen,不挂在首页 `UILaunchPage::screen()` 下。 - `input_group_` 默认只加入 `root_screen_`,启动页面时会被绑定到当前 `lv_indev_t`。 -- `navigate_home` 由 `LaunchImpl` 注入,页面按 ESC 或完成任务后调用它返回首页。 +- `navigate_home` 由 `Launch` 注入,页面按 ESC 或完成任务后调用它返回首页。 - 析构函数删除 `root_screen_` 和 `input_group_`,因此页面内创建的 LVGL 子对象会随 screen 一起释放。 ### 2.2 顶栏、内容区、底栏区域 @@ -123,31 +123,31 @@ lv_obj_set_style_bg_img_src(time_panel_, ### 4.1 从首页启动内置页面 -`Launch.cpp` 通过模板构造内置页面 app 描述: +`launch.cpp` 通过模板构造内置页面 app 描述: ```cpp template app::app(std::string name, std::string icon, page_t) : Name(std::move(name)), Icon(std::move(icon)) { - launch = [](LaunchImpl *ctx) { + launch = [](Launch *ctx) { auto p = std::make_shared(); ctx->app_Page = p; - p->navigate_home = std::bind(&LaunchImpl::go_back_home, ctx); + p->navigate_home = std::bind(&Launch::go_back_home, ctx); lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); }; } ``` -实际代码在 `projects/APPLaunch/main/ui/Launch.cpp` 中,核心逻辑是: +实际代码在 `projects/APPLaunch/main/ui/launch.cpp` 中,核心逻辑是: 1. 首页释放 ENTER 后由 `UILaunchPage::handle_home_key()` 调用 `launch_selected_app()`。 2. `UILaunchPage::launch_selected_app()` 转到 `Launch::launch_app()`。 -3. `LaunchImpl::launch_app()` 找到当前 app,并执行该 app 的 `launch` 函数。 +3. `Launch::launch_app()` 找到当前 app,并执行该 app 的 `launch` 函数。 4. 内置页面创建对象、加载 screen、切换输入组。 5. 页面内部按 ESC 或业务完成后调用 `navigate_home()`。 -6. `LaunchImpl::go_back_home()` 使用 `lv_async_call()` 回到首页,重新绑定首页输入组并 reset `app_Page`。 +6. `Launch::go_back_home()` 使用 `lv_async_call()` 回到首页,重新绑定首页输入组并 reset `app_Page`。 ### 4.2 返回首页 @@ -158,7 +158,7 @@ if (navigate_home) navigate_home(); ``` -`LaunchImpl::lv_go_back_home()` 会: +`Launch::lv_go_back_home()` 会: - `lv_timer_enable(true)` 恢复 LVGL timer。 - `UILaunchPage::bind_home_input_group()` 绑定首页输入组。 @@ -168,7 +168,7 @@ if (navigate_home) 注意事项: - 页面析构时必须停止自建 `lv_timer_t`、后台线程、文件 watcher、PTY 或音频资源。 -- 不要在键盘事件回调栈中直接 `delete this`,用 `navigate_home` 交给 `LaunchImpl` 异步处理。 +- 不要在键盘事件回调栈中直接 `delete this`,用 `navigate_home` 交给 `Launch` 异步处理。 - 如果页面临时切换到子页面或嵌套页面,要恢复正确的输入组。 ## 5. 当前内置页面列表 @@ -180,9 +180,9 @@ if (navigate_home) | `UIConsolePage` | `ui_app_console.hpp` | `CLI` 或终端外部命令 | `AppPage` | 终端模拟器,PTY 读写,支持 ANSI/VT 序列和键盘转义序列 | | `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | 贪吃蛇游戏,全屏自绘,使用 LVGL timer 驱动 | | `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | 系统设置、应用开关、亮度、音量、WiFi、相机分辨率等 | -| `UIMusicPage` | `ui_app_music.hpp` | `MUSIC` | `AppPage` | 音乐播放器,目录浏览、播放列表、音频回调 | +| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPage` | 内置游戏入口 | | `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | 指南针页面,传感器线程 + UI timer | -| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | `IP_PANEL` | `AppPage` | 网络接口/IP 信息列表,每秒刷新 | +| `UIIpPanelPage` | `ui_app_ip_panel.hpp` | `IP_PANEL` | `AppPage` | 网络接口/IP 信息列表,每秒刷新 | | `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | 文件浏览器,目录列表和进入/返回 | | `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH 参数输入,连接后嵌入 `UIConsolePage` | | `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh 消息列表、输入弹层、发送/刷新 | @@ -195,7 +195,7 @@ if (navigate_home) ## 6. 页面注册和显示顺序 -内置页面在 `LaunchImpl::LaunchImpl()` 中插入 `app_list`。固定前 5 个应用先初始化首页 5 个轮播槽: +内置页面在 `Launch::Launch()` 中插入 `app_list`。固定前 5 个应用先初始化首页 5 个轮播槽: ```cpp app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); @@ -205,22 +205,12 @@ app_list.emplace_back("GAME", img_path("game_100.png"), page_v); app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); ``` -随后根据设置开关添加可选内置页: - -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("Music")) - app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); - -if (APP_ENABLED("IP_Panel")) - app_list.emplace_back("IP_PANEL", img_path("ip_panel_100.png"), page_v); -``` +内置页面显示现在由 `kBuiltinApps[]` 和 `AppDescriptor.config_key` 驱动。`Launch::rebuild_builtin_apps()` 追加每个描述符前会调用 `launcher_app_registry_is_enabled()`,设置页修改则调用 `launcher_app_registry_set_enabled()` 并触发 `Launch::applications_reload()`。 约定: - `Store`、`CLI`、`Game`、`Setting` 在设置页中是 always-on,不能禁用。 -- `Compass` 当前在 `Launch.cpp` 中无条件加入,不受 `UISetupPage` 的 Launcher 开关列表控制。 +- `Compass` 当前在 `launch.cpp` 中无条件加入,不受 `UISetupPage` 的 Launcher 开关列表控制。 - Linux 设备构建下才加入 `IP_PANEL`、`FILE`、`SSH`、`MESH`、`REC`、`CAMERA`、`LORA`、`TANK` 等页面;SDL 构建会受 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 限制。 - `.desktop` 动态应用在内置页之后扫描加入,目录变化由 watcher 每 3 秒检查。 @@ -315,13 +305,13 @@ lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); ## 10. 与首页轮播的关系 -首页轮播本身由 `UILaunchPage.cpp` 管理: +首页轮播本身由 `ui_launch_page.cpp` 管理: - `carousel_elements` 保存 5 张卡片、5 个标题和 5 个页点。 -- 左右切换时调用 `switch_left()` / `switch_right()`,动画完成后旋转数组并让 `LaunchImpl` 更新远端槽位内容。 +- 左右切换时调用 `switch_left()` / `switch_right()`,动画完成后旋转数组并让 `Launch` 更新远端槽位内容。 - ENTER 触发 `UILaunchPage::launch_selected_app()`,最终调用当前 app 的 `launch()`。 -内置页面不直接操作首页轮播;返回首页后轮播状态由 `LaunchImpl` 保持。 +内置页面不直接操作首页轮播;返回首页后轮播状态由 `Launch` 保持。 ## 11. 常见注意事项 @@ -330,4 +320,4 @@ lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); - 不要在页面中直接访问首页全局对象,除非是明确的首页功能。 - 页面标题请调用 `set_page_title()`,不要直接改顶栏内部 label。 - 所有可退出页面都应支持 `KEY_ESC`,并调用 `navigate_home` 或返回上一级视图。 -- 页面开关 key 必须和 `UISetupPage::save_app_toggle()`、`Launch.cpp` 的 `APP_ENABLED()` 保持一致。 +- 页面开关 key 必须和 `UISetupPage::save_app_toggle()`、`launch.cpp` 的 `APP_ENABLED()` 保持一致。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" index 45b2f1e5..bcd0cbf5 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" @@ -1,13 +1,13 @@ # 06 - 资源与配置系统 -本章说明 APPLaunch 的运行时资源目录、路径解析规则、`.desktop` 动态应用文件、配置 API、设置页配置 key 以及资源使用注意事项。重点源码在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`、`projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`。 +本章说明 APPLaunch 的运行时资源目录、路径解析规则、`.desktop` 动态应用文件、配置 API、设置页配置 key 以及资源使用注意事项。重点源码在 `ext_components/cp0_lvgl/include/cp0_lvgl_app.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp`、`projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`。 ## 1. 资源系统概览 APPLaunch 不建议页面直接拼接运行时路径,而是通过 `cp0_file_path()` / `cp0_file_path_c()` 做统一解析。 ```text -页面代码 / Launch.cpp +页面代码 / launch.cpp | v img_path(), audio_path(), cp0_file_path_c() @@ -66,7 +66,7 @@ projects/APPLaunch/APPLaunch/ | 目录 | 内容 | 使用者 | | --- | --- | --- | -| `applications/` | `.desktop` 应用描述 | `LaunchImpl::applications_load()` | +| `applications/` | `.desktop` 应用描述 | `Launch::applications_load()` | | `share/images/` | 图标、状态栏背景、页面图片、GIF | 首页、顶栏、各内置页面 | | `share/audio/` | `startup.mp3`、`switch.wav`、`enter.wav`、页面按键音 | 首页音效、设置页、页面音效 | | `share/font/` | TTF/OTF 字体 | `LauncherFonts`、页面自定义字体 | @@ -145,14 +145,14 @@ extern "C" const char *cp0_file_path_c(const char *file) 首页和内置页常用: ```cpp -app_list.emplace_back("MUSIC", img_path("music_100.png"), page_v); +app_list.emplace_back("GAME", img_path("game_100.png"), page_v); lv_obj_set_style_bg_img_src(time_panel_, cp0_file_path_c("status_time_background.png"), LV_PART_MAIN | LV_STATE_DEFAULT); ``` -首页卡片图标由 `Launch.cpp::panel_set_icon()` 设置: +首页卡片图标由 `launch.cpp::panel_set_icon()` 设置: ```cpp static void panel_set_icon(lv_obj_t *panel, const char *src) @@ -203,7 +203,7 @@ launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD) ## 5. `.desktop` 动态应用 -动态应用文件放在 `cp0_file_path("applications")` 指向的目录。`LaunchImpl::applications_load()` 只处理 `*.desktop` 文件,并解析 `[Desktop Entry]` 段。 +动态应用文件放在 `cp0_file_path("applications")` 指向的目录。`Launch::applications_load()` 只处理 `*.desktop` 文件,并解析 `[Desktop Entry]` 段。 支持 key: @@ -273,13 +273,13 @@ cp0_config_save(); | 配置 key | 默认值 | 含义 | 备注 | | --- | --- | --- | --- | -| `app_Python` | `1` | Python 入口显示开关 | 设置页可见,但 Python 在 `Launch.cpp` 固定加入,当前开关不影响固定项 | +| `app_Python` | `1` | Python 入口显示开关 | 设置页可见,但 Python 在 `launch.cpp` 固定加入,当前开关不影响固定项 | | `app_Store` | `1` | Store 入口 | always-on,不能关闭 | | `app_CLI` | `1` | CLI 入口 | always-on,不能关闭 | | `app_Game` | `1` | GAME 入口 | always-on,不能关闭 | | `app_Setting` | `1` | SETTING 入口 | always-on,不能关闭 | -| `app_Music` | `1` | MUSIC 内置页 | `Launch.cpp` 读取 | -| `app_Math` | `1` | Calculator 外部应用 | `Launch.cpp` 读取 | +| `app_Game` | `1` | GAME 内置页 | `launch.cpp` 读取 | +| `app_Math` | `1` | Calculator 外部应用 | `launch.cpp` 读取 | | `app_IP_Panel` | `1` | IP_PANEL 内置页 | Linux 非 SDL 下读取 | | `app_File` | `1` | FILE 内置页 | Linux 非 SDL 下读取 | | `app_SSH` | `1` | SSH 内置页 | Linux 非 SDL 下读取 | @@ -289,7 +289,7 @@ cp0_config_save(); | `app_LoRa` | `1` | LORA 内置页 | Linux 非 SDL 下读取 | | `app_Tank` | `1` | TANK 内置页 | Linux 非 SDL 下读取 | -注意:`Compass` 当前无对应 `app_Compass` 设置项,`Launch.cpp` 无条件加入。 +注意:`Compass` 当前无对应 `app_Compass` 设置项,`launch.cpp` 无条件加入。 ### 7.2 系统与页面配置 @@ -339,12 +339,12 @@ void save_app_toggle(int idx) - `UISetupPage::menu_init()` 的 `app_keys` / `app_labels`。 - `UISetupPage::save_app_toggle()` 的 `app_keys` 和 always-on 列表。 -- `Launch.cpp` 的 `APP_ENABLED("...")`。 +- `launch.cpp` 的 `APP_ENABLED("...")`。 - 文档和默认配置。 ## 9. 资源命名建议 -- 首页图标按 `_100.png` 命名,如 `music_100.png`、`setting_100.png`。 +- 首页图标按 `_100.png` 命名,如 `game_100.png`、`setting_100.png`。 - 小图标或状态背景按功能命名,如 `status_time_background.png`、`status_battery_background.png`。 - 页面专用资源使用页面前缀,如 `setting_ok.png`、`setting_cross.png`。 - 音效用短名,如 `switch.wav`、`enter.wav`、`key_back.wav`。 @@ -357,5 +357,5 @@ void save_app_toggle(int idx) - `.desktop` 的 `Icon` 不会自动调用 `cp0_file_path()`;建议写 LVGL 能直接读取的路径,或与现有模板保持一致。 - 如果新增资源用于设备端,确认打包脚本会把 `projects/APPLaunch/APPLaunch/share/...` 带入安装包。 - 配置写入后如果忘记 `cp0_config_save()`,重启后会丢失。 -- `app_*` 开关影响的是下次构造 `LaunchImpl` 时的列表;运行中修改后不一定立即改变首页固定列表,视是否触发重建/重启而定。 +- `app_*` 开关影响的是下次构造 `Launch` 时的列表;运行中修改后不一定立即改变首页固定列表,视是否触发重建/重启而定。 - `run_as_user` 会影响外部进程和 PTY 命令执行身份,调试权限问题时要检查该配置。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" index 5699d5a8..54ed56d7 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" @@ -1,6 +1,6 @@ # 07 - 输入系统与按键映射 -本章说明 APPLaunch 的键盘输入线程、`key_item` 事件结构、LVGL 事件投递、首页和内置页面按键映射、终端输入转义以及调试注意事项。重点源码在 `projects/APPLaunch/main/include/keyboard_input.h`、`projects/APPLaunch/main/ui/ui.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`、`projects/APPLaunch/main/ui/UILaunchPage.cpp` 和 `projects/APPLaunch/main/ui/page_app/*.hpp`。 +本章说明 APPLaunch 的键盘输入线程、`key_item` 事件结构、LVGL 事件投递、首页和内置页面按键映射、终端输入转义以及调试注意事项。重点源码在 `ext_components/cp0_lvgl/include/keyboard_input.h`、`projects/APPLaunch/main/ui/ui.h`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c`、`projects/APPLaunch/main/ui/ui_launch_page.cpp` 和 `projects/APPLaunch/main/ui/page_app/*.hpp`。 ## 1. 输入系统概览 @@ -43,7 +43,7 @@ if (LV_EVENT_KEYBOARD == 0) ## 2. `key_item` 数据结构 -`projects/APPLaunch/main/include/keyboard_input.h` 定义输入事件: +`ext_components/cp0_lvgl/include/keyboard_input.h` 定义输入事件: ```c struct key_item { @@ -226,11 +226,11 @@ free(elm); 如果页面直接处理 `LV_EVENT_KEYBOARD`,通常使用原始 `KEY_*`;如果页面交给 LVGL 控件焦点机制,则依赖 `data->key`。 -`projects/APPLaunch/main/include/compat/input_keys.h` 在 Linux 下包含 ``,在非 Linux 平台提供常见 `KEY_*` 兼容定义,保证 SDL/桌面构建也能编译页面代码。 +`ext_components/cp0_lvgl/include/compat/input_keys.h` 在 Linux 下包含 ``,在非 Linux 平台提供常见 `KEY_*` 兼容定义,保证 SDL/桌面构建也能编译页面代码。 ## 6. 首页按键映射 -首页按键处理在 `UILaunchPage::handle_home_key()`;LVGL C 回调入口是 `projects/APPLaunch/main/ui/UILaunchPage.cpp` 中的 `UILaunchPage::on_home_key()`。 +首页按键处理在 `UILaunchPage::handle_home_key()`;LVGL C 回调入口是 `projects/APPLaunch/main/ui/ui_launch_page.cpp` 中的 `UILaunchPage::on_home_key()`。 先将 CardputerZero 上常用的 `F/X/Z/C` 映射为方向键: @@ -268,8 +268,8 @@ static uint32_t fzxc_to_arrow(uint32_t key) | `UIConsolePage` | `ui_app_console.hpp` | ESC/方向/回车/退格转 PTY 控制序列;HOME 相关状态用于退出/外部锁 | | `UIGamePage` | `ui_app_game.hpp` | 方向键移动,ENTER 开始/重开,ESC 返回 | | `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN 或 F/X 选择,ENTER/RIGHT 或 C 进入/确认,ESC/LEFT 或 Z 返回,部分页面支持 R/D | -| `UIMusicPage` | `ui_app_music.hpp` | F/X/Z/C 映射到 LV_KEY_UP/DOWN/LEFT/RIGHT;ENTER 播放/载入;ESC 返回 | -| `UIIpPanelPage` | `ui_app_IpPanel.hpp` | F/X/Z/C 映射到 LV_KEY_*;UP/DOWN 选择;ESC 返回 | +| `UIGamePage` | `ui_app_game.hpp` | 使用通用页面按键处理;ESC 返回 | +| `UIIpPanelPage` | `ui_app_ip_panel.hpp` | F/X/Z/C 映射到 LV_KEY_*;UP/DOWN 选择;ESC 返回 | | `UIFilePage` | `ui_app_file.hpp` | UP/DOWN 选择;RIGHT/ENTER 进入;LEFT 返回上级;ESC 返回首页或上级 | | `UISSHPage` | `ui_app_ssh.hpp` | UP/DOWN 切换 Host/Port/User;字符输入;BACKSPACE 删除;ENTER 连接;ESC 返回 | | `UIMeshPage` | `ui_app_mesh.hpp` | S 打开输入;R 刷新;UP/DOWN 浏览;ENTER 发送;BACKSPACE 删除;ESC 取消/返回 | @@ -283,8 +283,8 @@ static uint32_t fzxc_to_arrow(uint32_t key) CardputerZero 键盘上常用 `F/X/Z/C` 作为方向替代。代码中有三种用法: -1. 首页 `UILaunchPage.cpp`:`fzxc_to_arrow()` 把 `F/X/Z/C` 转为 `KEY_UP/DOWN/LEFT/RIGHT`。 -2. 页面内部转换为 LVGL key,如 `UIMusicPage`、`UIIpPanelPage`: +1. 首页 `ui_launch_page.cpp`:`fzxc_to_arrow()` 把 `F/X/Z/C` 转为 `KEY_UP/DOWN/LEFT/RIGHT`。 +2. 页面内部转换为 LVGL key,如 `UIGamePage`、`UIIpPanelPage`: ```cpp switch (key) { @@ -336,7 +336,7 @@ static char keycode_to_char(uint32_t key) ## 10. 外部应用运行时的输入处理 -外部应用通过 `LaunchImpl::launch_Exec()` 启动: +外部应用通过 `Launch::launch_Exec()` 启动: ```cpp LVGL_RUN_FLAGE = 0; diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" index 1b5c7378..5da68f48 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/08-\346\236\204\345\273\272\344\270\216\347\274\226\350\257\221\346\214\207\345\215\227.md" @@ -1,6 +1,6 @@ # 08 - 构建与编译指南 -本章说明 `projects/APPLaunch` 的完整构建方法,覆盖 Linux SDL2 本机仿真、设备端原生编译、Linux x86 交叉编译、macOS 交叉编译、依赖安装、环境变量、SCons 关键逻辑和常见错误处理。 +本章说明 `projects/APPLaunch` 的完整构建方法,覆盖 Linux SDL2 本机仿真、设备端原生编译、Linux x86 交叉编译、macOS 交叉编译、Windows SDL2/交叉编译、依赖安装、环境变量、SCons 关键逻辑和常见错误处理。 所有命令默认从仓库根目录开始: @@ -19,6 +19,8 @@ APPLaunch 可以编译成多种形态。核心差异由 `CONFIG_DEFAULT_FILE` | Linux x86 交叉编译 | Linux x86_64 开发机,产物运行在设备 | `linux_x86_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | 推荐的正式设备产物构建方式 | | macOS 交叉编译 | macOS 开发机,产物运行在设备 | `mac_cross_cp0_config_defaults.mk` | Linux framebuffer + evdev | 在 macOS 上生成 arm64 设备产物 | | macOS SDL/Darwin 配置 | macOS 开发机 | `darwin_config_defaults.mk` | SDL 相关配置 | 本机 SDL 方向的配置基础 | +| Windows SDL2 本机仿真 | Windows x86_64 开发机 | `win_x86_sdl2_config_defaults.mk` | SDL2 窗口 + SDL 输入 | Windows 上调试 UI | +| Windows x86 交叉编译 | Windows x86_64 开发机,产物运行在设备 | `win_x86_cross_config_defaults.mk` | Linux framebuffer + evdev | 在 Windows 上生成 arm64 设备产物 | 构建产物通常出现在: @@ -578,9 +580,87 @@ file dist/M5CardputerZero-APPLaunch 期望是 `ARM aarch64` Linux ELF,而不是 Mach-O。 -## 10. SCons 关键逻辑 -### 10.1 顶层 `projects/APPLaunch/SConstruct` +## 10. Windows 构建 + +Windows 构建使用 `projects/APPLaunch` 下同一套 SCons 入口。配置文件会设置 `CONFIG_TOOLCHAIN_SYSTEM_WIN=y` 和 `CONFIG_TOOLCHAIN_GCCSUFFIX=".exe"`,让 SDK 构建系统调用 Windows 工具链可执行文件。 + +### 10.1 Windows SDL2 本机编译和运行 + +建议使用 MSYS2 MinGW Shell,使 `gcc`、`g++`、`pkg-config`、SDL2 和 FreeType 都在 `PATH` 中。 + +MSYS2 UCRT64 示例: + +```bash +pacman -S --needed \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-pkgconf \ + mingw-w64-ucrt-x86_64-SDL2 \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-python-pip + +python -m pip install parse scons requests tqdm setuptools-rust paramiko scp +``` + +编译和运行: + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_sdl2_config_defaults.mk +scons -j8 +cd dist +./M5CardputerZero-APPLaunch.exe +``` + +`win_x86_sdl2_config_defaults.mk` 的关键项: + +```make +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_TOOLCHAIN_SYSTEM_WIN=y +CONFIG_V9_5_LV_USE_SDL=y +CONFIG_V9_5_LV_FS_POSIX_PATH="./" +CONFIG_APPLAUNCH_WIN_X86_SDL2=y +``` + +SDL2 产物是 `dist/M5CardputerZero-APPLaunch.exe`。 + +### 10.2 Windows 交叉编译到设备 + +安装 SysGCC Raspberry64 Windows AArch64 Linux 交叉工具链:`https://sysprogs.com/getfile/2542/raspberry64-gcc14.2.0.exe`。默认配置期望: + +```make +CONFIG_TOOLCHAIN_PATH="D:\\app\\SysGCC\\bin" +CONFIG_TOOLCHAIN_PREFIX="aarch64-linux-gnu-" +CONFIG_TOOLCHAIN_GCCSUFFIX=".exe" +CONFIG_GCC_DUMPMACHINE="aarch64-linux-gnu" +``` + +如果工具链安装在其他位置,请先修改 `projects/APPLaunch/win_x86_cross_config_defaults.mk` 中的 `CONFIG_TOOLCHAIN_PATH`。 + +编译: + +```bash +cd /path/to/launcher/projects/APPLaunch +scons distclean +export CONFIG_DEFAULT_FILE=win_x86_cross_config_defaults.mk +scons -j8 +``` + +`win_x86_cross_config_defaults.mk` 的关键项: + +```make +CONFIG_V9_5_LV_USE_LINUX_FBDEV=y +CONFIG_V9_5_LV_USE_EVDEV=y +CONFIG_V9_5_LV_FS_POSIX_PATH="/usr/share/APPLaunch/" +CONFIG_APPLAUNCH_WIN_X86_CROSS_CP0=y +``` + +交叉编译产物是 `dist/M5CardputerZero-APPLaunch`,没有 `.exe` 后缀,因为目标平台是 Linux AArch64。首次交叉编译可能会下载 SDK sysroot 到 `SDK/github_source/static_lib_v0.0.4`。 + +## 11. SCons 关键逻辑 + +### 11.1 顶层 `projects/APPLaunch/SConstruct` 该文件负责构建入口和全局环境准备: @@ -614,7 +694,7 @@ SConscript(str(sdk_path / "tools" / "scons" / "project.py"), variant_dir=os.getc 6. 交叉编译时检查并下载 `static_lib_v0.0.4`。 -### 10.2 SDK `project.py` +### 11.2 SDK `project.py` SDK 构建系统做以下事情: @@ -627,11 +707,11 @@ SDK 构建系统做以下事情: 7. 编译静态库、共享库和可执行文件。 8. 把可执行文件和 `STATIC_FILES` 复制到 `dist`。 -### 10.3 `projects/APPLaunch/main/SConstruct` +### 11.3 `projects/APPLaunch/main/SConstruct` 该文件注册 APPLaunch 主程序组件: -- 执行 `ui/components/generate_page_app_includes.py`,生成内置页面 include 聚合文件。 +- 执行 `ui/generate_page_app_includes.py`,生成内置页面 include 聚合文件。 - 读取当前 git 短 hash,注入编译宏 `LAUNCHER_GIT_COMMIT_RAW`。 - 收集 `src/*.c*` 和整个 `ui` 目录源码。 - 添加 include:`main`、`main/include`、`ext_components/cp0_lvgl/include`、`SDK/components/utilities/include`。 @@ -642,7 +722,7 @@ SDK 构建系统做以下事情: - 把 `../APPLaunch` 运行时资源树加入 `STATIC_FILES`;该资源树包含 `bin/store_cache_sync.py`。 - 注册项目 target:`M5CardputerZero-APPLaunch`。 -## 11. 常用 SCons 命令 +## 12. 常用 SCons 命令 | 命令 | 作用 | | --- | --- | @@ -663,7 +743,7 @@ scons -j8 不要只改 `CONFIG_DEFAULT_FILE` 后直接 `scons -j8`,因为旧的 `build/config/global_config.mk` 可能已经存在,SDK 构建系统不会自动重新生成配置。 -## 12. `menuconfig` 使用建议 +## 13. `menuconfig` 使用建议 运行: @@ -688,9 +768,9 @@ scons save 注意:`scons save` 会写回配置文件。多人协作时不要随意保存到共享的 `*_config_defaults.mk`,除非这是本次任务明确要求的变更。 -## 13. 常见错误和处理 +## 14. 常见错误和处理 -### 13.1 `scons: command not found` +### 14.1 `scons: command not found` 原因:未安装 SCons 或 Python 用户 bin 目录不在 PATH。 @@ -707,7 +787,7 @@ python3 -m scons --version python3 -m scons -j8 ``` -### 13.2 `ModuleNotFoundError: No module named 'parse'` +### 14.2 `ModuleNotFoundError: No module named 'parse'` 原因:缺少 Python 包。 @@ -719,7 +799,7 @@ python3 -m pip install --user parse requests tqdm paramiko scp 虚拟环境中请先 `source .venv/bin/activate`。 -### 13.3 `Package sdl2 was not found in the pkg-config search path` +### 14.3 `Package sdl2 was not found in the pkg-config search path` 原因:Linux SDL2 仿真依赖未安装,或 `PKG_CONFIG_PATH` 未包含 SDL2 `.pc` 文件目录。 @@ -737,7 +817,7 @@ brew install sdl2 pkg-config pkg-config --cflags sdl2 ``` -### 13.4 `Package freetype2 was not found` +### 14.4 `Package freetype2 was not found` 处理: @@ -753,7 +833,7 @@ brew install freetype pkg-config pkg-config --cflags freetype2 ``` -### 13.5 `aarch64-linux-gnu-gcc: not found` +### 14.5 `aarch64-linux-gnu-gcc: not found` 原因:Linux 交叉工具链未安装,或 PATH 不包含工具链。 @@ -766,7 +846,9 @@ aarch64-linux-gnu-gcc --version macOS 交叉编译应使用 `aarch64-unknown-linux-gnu-gcc`,对应配置文件是 `mac_cross_cp0_config_defaults.mk`。 -### 13.6 下载 `sdk_bsp.tar.gz` 失败 +Windows 交叉编译应使用 `aarch64-linux-gnu-gcc.exe`;请检查 `win_x86_cross_config_defaults.mk` 中的 `CONFIG_TOOLCHAIN_PATH` 和 `CONFIG_TOOLCHAIN_PREFIX`。 + +### 14.6 下载 `sdk_bsp.tar.gz` 失败 原因:首次交叉编译需要下载 `static_lib_v0.0.4`,网络不可用或 GitHub 访问失败。 @@ -783,7 +865,7 @@ SDK/github_source/static_lib_v0.0.4/ 如果目录存在但版本不匹配,顶层 `SConstruct` 仍会尝试更新。 -### 13.7 `libcamera` 头文件或库找不到 +### 14.7 `libcamera` 头文件或库找不到 交叉编译配置中,`main/SConstruct` 会添加: @@ -801,7 +883,7 @@ ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu | grep camera 如果缺失,需要更新 sysroot 包或安装设备侧开发库后重新制作 sysroot。 -### 13.8 链接时报 `cannot find -linput`、`-lxkbcommon`、`-ludev` +### 14.8 链接时报 `cannot find -linput`、`-lxkbcommon`、`-ludev` 本机 SDL2 构建:安装开发包。 @@ -817,7 +899,7 @@ ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libxkbcommon.* ls SDK/github_source/static_lib_v0.0.4/usr/lib/aarch64-linux-gnu/libudev.* ``` -### 13.9 切换配置后仍然使用旧后端 +### 14.9 切换配置后仍然使用旧后端 原因:`build/config/global_config.mk` 已经存在,构建系统不会因为你改了环境变量就自动重建配置。 @@ -835,7 +917,7 @@ scons -j8 grep -E 'LV_USE_SDL|LV_USE_LINUX_FBDEV|LV_USE_EVDEV|FS_POSIX_PATH' build/config/global_config.mk ``` -### 13.10 SDL2 运行黑屏或资源缺失 +### 14.10 SDL2 运行黑屏或资源缺失 常见原因:没有从 `dist` 目录运行,导致 `CONFIG_V9_5_LV_FS_POSIX_PATH="./"` 指向错误位置。 @@ -847,7 +929,7 @@ ls APPLaunch/share/images ./M5CardputerZero-APPLaunch ``` -### 13.11 设备运行提示资源文件不存在 +### 14.11 设备运行提示资源文件不存在 设备配置资源路径是: @@ -864,7 +946,7 @@ ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch 如果是手动部署,请确保复制的是 `dist/APPLaunch` 的内容,而不是只复制可执行文件。 -### 13.12 RadioLib 下载失败 +### 14.12 RadioLib 下载失败 `ext_components/RadioLib/SConstruct` 在 `CONFIG_RADIOLIB_COMPONENT_ENABLED=y` 时使用 `wget_github('https://github.com/jgromes/RadioLib.git')` 获取 RadioLib。首次构建可能需要网络。 @@ -874,9 +956,9 @@ ls /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch - 检查 `SDK/github_source` 下是否已有 RadioLib 缓存。 - 在离线环境提前准备对应源码缓存。 -## 14. 推荐构建流程 +## 15. 推荐构建流程 -### 14.1 日常 UI 开发 +### 15.1 日常 UI 开发 ```bash cd /home/nihao/w2T/github/launcher/projects/APPLaunch @@ -887,7 +969,7 @@ cd dist ./M5CardputerZero-APPLaunch ``` -### 14.2 生成设备正式产物 +### 15.2 生成设备正式产物 ```bash cd /home/nihao/w2T/github/launcher/projects/APPLaunch @@ -899,7 +981,7 @@ file dist/M5CardputerZero-APPLaunch 然后进入第 09 章执行 `.deb` 打包、安装和 systemd 验证。 -### 14.3 快速确认构建目标 +### 15.3 快速确认构建目标 ```bash grep CONFIG_DEFAULT_FILE /proc/$$/environ 2>/dev/null || true diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" index c1484f27..c8850b6b 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/09-\346\211\223\345\214\205\351\203\250\347\275\262\344\270\216systemd.md" @@ -35,7 +35,7 @@ APPLaunch 的设备端运行依赖两类文件: systemd 服务文件安装到: ```text -/lib/systemd/system/APPLaunch.service +/usr/lib/systemd/user/APPLaunch.service ``` 服务启动命令是: @@ -50,6 +50,8 @@ systemd 服务文件安装到: /usr/share/APPLaunch ``` +当前包把 APPLaunch 安装为 UID 1000 用户的 systemd user service。手动检查服务时,可以以该用户登录后执行 `systemctl --user ...`,或在 `runuser`/SSH 自动化中设置 `XDG_RUNTIME_DIR=/run/user/1000`。 + ## 2. 打包前必须先完成设备目标构建 `.deb` 应该使用 arm64 设备产物,而不是 Linux SDL2 x86_64 仿真产物。 @@ -122,11 +124,11 @@ projects/APPLaunch/tools/debian-APPLaunch/ │ ├── control │ ├── postinst │ └── prerm -├── lib/ -│ └── systemd/ -│ └── system/ -│ └── APPLaunch.service └── usr/ + ├── lib/ + │ └── systemd/ + │ └── user/ + │ └── APPLaunch.service └── share/ └── APPLaunch/ ├── applications/ @@ -331,9 +333,17 @@ Description: M5CardputerZero APPLaunch ```sh mkdir -p /var/cache/APPLaunch -ln -s /var/cache/APPLaunch /usr/share/APPLaunch/cache -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl enable APPLaunch.service -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl start APPLaunch.service +ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +loginctl enable-linger "$APP_USER" || true +systemctl start "user@$APP_UID.service" || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user enable APPLaunch.service || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user restart APPLaunch.service || true exit 0 ``` @@ -341,7 +351,7 @@ exit 0 - 创建可写缓存目录 `/var/cache/APPLaunch`。 - 在只读/系统资源目录下建立 `cache` 软链接。 -- 启用并启动 systemd 服务。 +- 为 UID 1000 用户启用 linger,并启用/启动 systemd user service。 注意:当前通用打包脚本使用 `ln -sfn`,重复安装时可以安全刷新缓存链接。 @@ -350,8 +360,14 @@ exit 0 卸载前脚本执行: ```sh -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl stop APPLaunch.service -[ -f "/lib/systemd/system/APPLaunch.service" ] && systemctl disable APPLaunch.service +APP_UID=1000 +APP_USER="$(getent passwd "$APP_UID" | cut -d: -f1)" +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user stop APPLaunch.service || true +runuser -u "$APP_USER" -- env XDG_RUNTIME_DIR="/run/user/$APP_UID" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$APP_UID/bus" \ + systemctl --user disable APPLaunch.service || true rm -rf /var/cache/APPLaunch exit 0 ``` @@ -371,6 +387,8 @@ exit 0 ```ini [Unit] Description=APPLaunch Service +After=pipewire-pulse.service +Wants=pipewire-pulse.service [Service] ExecStart=/usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch @@ -380,7 +398,7 @@ RestartSec=1 StartLimitInterval=0 [Install] -WantedBy=multi-user.target +WantedBy=default.target ``` 字段说明: @@ -392,9 +410,10 @@ WantedBy=multi-user.target | `Restart=always` | 进程退出后总是重启 | | `RestartSec=1` | 退出 1 秒后重启 | | `StartLimitInterval=0` | 关闭默认启动频率限制,避免频繁崩溃后 systemd 停止重启 | -| `WantedBy=multi-user.target` | enable 后随多用户目标启动 | +| `After` / `Wants` | 在可用时等待 PipeWire PulseAudio 支持 | +| `WantedBy=default.target` | enable 后随用户默认 target 启动 | -当前服务文件没有显式设置用户,默认以 systemd system service 的 root 身份运行。这通常有利于访问 framebuffer、evdev、GPIO、音频和相机设备,但也意味着程序权限较高。 +当前包安装的是 `/usr/lib/systemd/user` 下的用户服务,不是 root 身份的 system service。`postinst` 会为 UID 1000 用户启用该服务;framebuffer、evdev、GPIO、音频和相机访问权限需由系统镜像的用户/用户组规则提供。 ## 9. 安装到设备 @@ -432,9 +451,9 @@ sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb 如果服务正在运行,`postinst` 会尝试 enable/start。为了减少安装期间 framebuffer 或输入设备占用问题,可以手动先停服务: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` ## 10. 使用 `scons push` 快速部署 @@ -457,8 +476,8 @@ remote_host = 192.168.28.177 remote_port = 22 username = pi password = pi -; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' -; after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +; before_cmd = 'echo pi | sudo -S systemctl --user stop APPLaunch.service' +; after_cmd = 'echo pi | sudo -S systemctl --user stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl --user start APPLaunch.service' ``` 执行: @@ -505,17 +524,17 @@ scp -r dist/APPLaunch pi@192.168.28.177:/home/pi/APPLaunch-new 在设备上安装: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true sudo mkdir -p /usr/share/APPLaunch/bin sudo install -m 0755 /home/pi/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch sudo rsync -a --delete /home/pi/APPLaunch-new/ /usr/share/APPLaunch/ sudo mkdir -p /var/cache/APPLaunch sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache -sudo systemctl daemon-reload -sudo systemctl restart APPLaunch.service +systemctl --user daemon-reload +systemctl --user restart APPLaunch.service ``` -如果服务文件尚未安装,可以手动创建 `/lib/systemd/system/APPLaunch.service`,内容参考第 8 节。 +如果服务文件尚未安装,可以手动创建 `/usr/lib/systemd/user/APPLaunch.service`,内容参考第 8 节。 ## 12. 部署验证命令 @@ -582,46 +601,46 @@ ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || tru ### 12.4 systemd 状态 ```bash -systemctl status APPLaunch.service --no-pager -systemctl is-enabled APPLaunch.service -systemctl is-active APPLaunch.service +systemctl --user status APPLaunch.service --no-pager +systemctl --user is-enabled APPLaunch.service +systemctl --user is-active APPLaunch.service ``` 查看日志: ```bash -journalctl -u APPLaunch.service -b --no-pager -journalctl -u APPLaunch.service -b -f +journalctl --user -u APPLaunch.service -b --no-pager +journalctl --user -u APPLaunch.service -b -f ``` 重启: ```bash -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` 停止: ```bash -sudo systemctl stop APPLaunch.service +systemctl --user stop APPLaunch.service ``` 开机自启动: ```bash -sudo systemctl enable APPLaunch.service +systemctl --user enable APPLaunch.service ``` 取消开机自启动: ```bash -sudo systemctl disable APPLaunch.service +systemctl --user disable APPLaunch.service ``` 重新读取服务文件: ```bash -sudo systemctl daemon-reload +systemctl --user daemon-reload ``` ### 12.5 手动前台运行 @@ -629,7 +648,7 @@ sudo systemctl daemon-reload 排查 systemd 前,建议先前台运行: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true cd /usr/share/APPLaunch sudo ./bin/M5CardputerZero-APPLaunch ``` @@ -680,28 +699,28 @@ sudo dpkg -P applaunch ### 13.2 安装旧包回滚 ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true sudo dpkg -i /home/pi/applaunch_旧版本-m5stack1_arm64.deb -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` 验证: ```bash dpkg -s applaunch | grep Version -systemctl status APPLaunch.service --no-pager +systemctl --user status APPLaunch.service --no-pager ``` ### 13.3 临时禁用 launcher ```bash -sudo systemctl disable --now APPLaunch.service +systemctl --user disable --now APPLaunch.service ``` 恢复: ```bash -sudo systemctl enable --now APPLaunch.service +systemctl --user enable --now APPLaunch.service ``` ## 14. 常见部署错误 @@ -745,8 +764,8 @@ scons -j8 检查: ```bash -systemctl status APPLaunch.service --no-pager -journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 ``` 常见原因: @@ -775,7 +794,7 @@ ls /dev/fb0 sudo rm -rf /usr/share/APPLaunch/cache sudo mkdir -p /var/cache/APPLaunch sudo ln -sfn /var/cache/APPLaunch /usr/share/APPLaunch/cache -sudo systemctl restart APPLaunch.service +systemctl --user restart APPLaunch.service ``` 当前通用打包脚本已经写入 `ln -sfn`;重新打包并安装即可持久修复。 @@ -820,12 +839,12 @@ python3 scripts/debian_packager.py 命令: ```bash -sudo systemctl stop APPLaunch.service || true +systemctl --user stop APPLaunch.service || true cd /usr/share/APPLaunch sudo ./bin/M5CardputerZero-APPLaunch ls -l /dev/fb0 sudo fuser -v /dev/fb0 2>/dev/null || true -journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 ``` ### 14.8 外部应用无法启动 @@ -870,11 +889,11 @@ dpkg-deb -c projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb | head - ```bash dpkg -s applaunch | grep -E 'Package|Version|Architecture' -systemctl status APPLaunch.service --no-pager -systemctl is-enabled APPLaunch.service +systemctl --user status APPLaunch.service --no-pager +systemctl --user is-enabled APPLaunch.service ldd /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch | grep 'not found' || true ls -l /usr/share/APPLaunch/cache -journalctl -u APPLaunch.service -b --no-pager | tail -n 100 +journalctl --user -u APPLaunch.service -b --no-pager | tail -n 100 ``` 功能验证: @@ -900,7 +919,7 @@ file dist/M5CardputerZero-APPLaunch cd /home/nihao/w2T/github/launcher python3 scripts/debian_packager.py scp projects/APPLaunch/tools/applaunch_0.2.1-m5stack1_arm64.deb pi@192.168.28.177:/home/pi/ -ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl status APPLaunch.service --no-pager' +ssh pi@192.168.28.177 'sudo dpkg -i /home/pi/applaunch_0.2.1-m5stack1_arm64.deb && systemctl --user status APPLaunch.service --no-pager' ``` 开发阶段快速替换建议使用: diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" index 596bddec..b66eeb78 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -6,10 +6,10 @@ | 入口 | 作用 | | --- | --- | -| `projects/APPLaunch/main/ui/Launch.cpp` | 固定应用列表、动态 `.desktop` 扫描、启动内置页面或外部进程 | +| `projects/APPLaunch/main/ui/launch.cpp` | 固定应用列表、动态 `.desktop` 扫描、启动内置页面或外部进程 | | `projects/APPLaunch/main/ui/page_app/` | 内置页面实现目录,页面通常为 header-only `.hpp` | | `projects/APPLaunch/main/ui/ui_app_page.hpp` | `AppPage`、顶部栏、`img_path()`、`audio_path()` 等页面公共能力 | -| `projects/APPLaunch/main/ui/components/generate_page_app_includes.py` | 构建前自动生成 `page_app.h`,把 `page_app/*.hpp` 全部 include 进来 | +| `projects/APPLaunch/main/ui/generate_page_app_includes.py` | 构建前自动生成 `generated/page_app.h`,把 `page_app/*.hpp` 全部 include 进来 | | `projects/APPLaunch/APPLaunch/` | 运行时资源树,打包后对应设备端 `/usr/share/APPLaunch/` | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | 设备端 `cp0_file_path()` 路径规则 | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 开发机 `cp0_file_path()` 路径规则 | @@ -80,7 +80,7 @@ private: 注意事项: - 页面必须继承 `AppPage`,这样才能复用 `screen()`、`input_group()`、`navigate_home` 等机制。 -- 返回首页优先调用 `navigate_home()`,不要直接加载首页 screen,否则 `LaunchImpl` 无法正确释放当前页面对象。 +- 返回首页优先调用 `navigate_home()`,不要直接加载首页 screen,否则 `Launch` 无法正确释放当前页面对象。 - 页面内如果创建 LVGL timer、文件描述符、线程或外设句柄,要在析构函数里释放。 - 页面尺寸以 320x170 为基准;常见布局是顶部栏 20px,正文 320x150。 - 资源路径不要硬编码绝对路径;图片用 `img_path("xxx.png")`,音频用 `audio_path("xxx.wav")`。 @@ -90,12 +90,12 @@ private: `projects/APPLaunch/main/SConstruct` 在构建前运行: ```python -ui/components/generate_page_app_includes.py +ui/generate_page_app_includes.py ``` -该脚本会扫描 `projects/APPLaunch/main/ui/page_app/*.hpp`,生成 `projects/APPLaunch/main/ui/page_app.h`。通常只要文件后缀是 `.hpp`,构建时就会自动被 include。 +该脚本会扫描 `projects/APPLaunch/main/ui/page_app/*.hpp`,生成 `projects/APPLaunch/build/generated/include/generated/page_app.h`。通常只要文件后缀是 `.hpp`,构建时就会自动被 include。 -如果你手动检查,`page_app.h` 中应出现: +如果你手动检查,`generated/page_app.h` 中应出现: ```cpp #include "page_app/ui_app_my_tool.hpp" @@ -103,7 +103,7 @@ ui/components/generate_page_app_includes.py ### 2.3 在首页应用列表注册 -打开 `projects/APPLaunch/main/ui/Launch.cpp`,找到 `LaunchImpl::LaunchImpl()`。内置页面注册方式如下: +打开 `projects/APPLaunch/main/ui/launch.cpp`,找到 `Launch::Launch()`。内置页面注册方式如下: ```cpp app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); @@ -136,16 +136,16 @@ if (APP_ENABLED("MyTool")) ```cpp static const char *app_keys[] = { "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", "MyTool" + "Game", "Math", "MyTool" }; static const char *app_labels[] = { "Python", "Store", "CLI", "Game", "Setting", - "Music", "Math", "My Tool" + "Game", "Math", "My Tool" }; ``` -`save_app_toggle()` 会把开关保存为 `app_`,例如 `app_MyTool=0`。`Launch.cpp` 里用相同 key 读取: +`save_app_toggle()` 会把开关保存为 `app_`,例如 `app_MyTool=0`。`launch.cpp` 里用相同 key 读取: ```cpp cp0_config_get_int("app_MyTool", 1) @@ -263,7 +263,7 @@ Type=Application ### 3.3 外部应用启动行为 -`Launch.cpp` 对外部应用有两种启动方式: +`launch.cpp` 对外部应用有两种启动方式: - `Terminal=true`:创建 `UIConsolePage`,在 APPLaunch 进程内显示 PTY 终端,执行 `Exec`。 - `Terminal=false`:调用 `cp0_process_exec_blocking()` 启动外部进程,APPLaunch 暂停 LVGL timer 和输入组,等子进程退出后恢复首页。 @@ -283,7 +283,7 @@ APPLaunch 启动时会调用 `applications_load()` 扫描 `.desktop` 文件; ```bash # 设备端 ls -l /usr/share/APPLaunch/applications -journalctl -u APPLaunch.service -f +journalctl --user -u APPLaunch.service -f # SDL2 开发机,在运行目录附近确认 APPLaunch/applications 是否存在 find projects/APPLaunch -path '*APPLaunch/applications*' -maxdepth 5 -type f @@ -364,9 +364,9 @@ lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); 1. 在 `UISetupPage::menu_init()` 的 `app_keys` 增加内部 key,例如 `MyTool`。 2. 在相同位置的 `app_labels` 增加显示名,例如 `My Tool`。 -3. 在 `Launch.cpp` 注册应用时使用相同 key:`APP_ENABLED("MyTool")`。 +3. 在 `launch.cpp` 注册应用时使用相同 key:`APP_ENABLED("MyTool")`。 4. 运行设置页,进入 `Launcher` 菜单,切换 O/X。 -5. 返回首页后如列表未刷新,可重启 APPLaunch;当前固定/内置列表是在 `LaunchImpl` 构造时读取配置。 +5. 返回首页后如列表未刷新,可重启 APPLaunch;当前固定/内置列表是在 `Launch` 构造时读取配置。 ### 5.2 新增普通设置项 @@ -401,8 +401,8 @@ lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); ```bash sudo cat /var/lib/applaunch/settings -sudo sed -i 's/^app_Music=.*/app_Music=1/' /var/lib/applaunch/settings -sudo systemctl restart APPLaunch.service +sudo sed -i 's/^app_Game=.*/app_Game=1/' /var/lib/applaunch/settings +systemctl --user restart APPLaunch.service ``` 如果新增大量配置,注意当前最大条目数是 32;超过后 `cp0_config_set_*` 会直接返回,设置不会保存。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" index 5c7a967e..e7ba9e4a 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" @@ -47,15 +47,15 @@ CONFIG_DEFAULT_FILE=config_defaults.mk scons -j4 --implicit-deps-changed 如果通过 systemd 启动: ```bash -sudo systemctl status APPLaunch.service --no-pager -sudo journalctl -u APPLaunch.service -b --no-pager -sudo journalctl -u APPLaunch.service -f +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager +journalctl --user -u APPLaunch.service -f ``` 如果手动运行设备端二进制: ```bash -sudo systemctl stop APPLaunch.service +systemctl --user stop APPLaunch.service cd /usr/share/APPLaunch sudo /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch 2>&1 | tee /tmp/applaunch.log ``` @@ -101,7 +101,7 @@ sudo cat /var/lib/applaunch/settings 常见配置 key: -- `app_Music`、`app_Math`、`app_File`、`app_Camera` 等:Launcher 页面显示开关。 +- `app_Game`、`app_Math`、`app_File`、`app_Camera` 等:Launcher 页面显示开关。 - `brightness`:亮度。 - `volume`:音量。 - `dark_time`:息屏时间。 @@ -118,16 +118,16 @@ sudo cat /var/lib/applaunch/settings | `[BOOT] cp0_lvgl_init() starting...` | `main.cpp` | 开始初始化平台适配层、显示、输入、音频等 | | `[BOOT] First frame flushed to fb0.` | `main.cpp` | 首帧已强制刷新到显示设备 | | `Entering main loop` | `main.cpp` | 主循环已进入 | -| `[LAUNCHER] set panel icon` | `Launch.cpp` | 首页图标设置成功 | -| `set panel icon missing/unreadable` | `Launch.cpp` | 图标路径不存在或不可读 | -| `applications_load: opendir failed` | `Launch.cpp` | applications 目录不存在或不可读 | -| `missing Name or Exec` | `Launch.cpp` | `.desktop` 缺少必需字段 | -| `duplicate Exec` | `Launch.cpp` | `.desktop` 与已有应用 Exec 重复 | -| `Launching terminal app` | `Launch.cpp` | 进入内置终端页面运行命令 | -| `Launching external app` | `Launch.cpp` | 启动非终端外部程序 | +| `[LAUNCHER] set panel icon` | `launch.cpp` | 首页图标设置成功 | +| `set panel icon missing/unreadable` | `launch.cpp` | 图标路径不存在或不可读 | +| `applications_load: opendir failed` | `launch.cpp` | applications 目录不存在或不可读 | +| `missing Name or Exec` | `launch.cpp` | `.desktop` 缺少必需字段 | +| `duplicate Exec` | `launch.cpp` | `.desktop` 与已有应用 Exec 重复 | +| `Launching terminal app` | `launch.cpp` | 进入内置终端页面运行命令 | +| `Launching external app` | `launch.cpp` | 启动非终端外部程序 | | `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | 外部应用运行期间父进程读到 ESC | | `[cp0] Returned to launcher` | `cp0_app_process.cpp` | 外部应用退出,准备回到首页 | -| `[HOME_STATUS] connected=` | `Launch.cpp` | 首页状态栏刷新 WiFi/电量 | +| `[HOME_STATUS] connected=` | `launch.cpp` | 首页状态栏刷新 WiFi/电量 | ## 3. 黑屏排查 @@ -137,8 +137,8 @@ sudo cat /var/lib/applaunch/settings ```bash pgrep -a M5CardputerZero-APPLaunch -sudo systemctl status APPLaunch.service --no-pager -sudo journalctl -u APPLaunch.service -b --no-pager | tail -120 +systemctl --user status APPLaunch.service --no-pager +journalctl --user -u APPLaunch.service -b --no-pager | tail -120 ``` 如果没有进程: @@ -150,7 +150,7 @@ sudo journalctl -u APPLaunch.service -b --no-pager | tail -120 如果进程反复重启: ```bash -sudo journalctl -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' +journalctl --user -u APPLaunch.service -b --no-pager | grep -Ei 'segfault|assert|error|failed|No such|permission' ``` ### 3.2 检查启动日志阶段 @@ -265,8 +265,8 @@ find /usr/share/APPLaunch/share/font -maxdepth 1 -type f | sort 检查是否绑定了正确输入组: - 首页:`UILaunchPage::bind_home_input_group()`。 -- 内置页面:`Launch.cpp` 创建页面后调用 `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())`。 -- 返回首页:`LaunchImpl::lv_go_back_home()` 会重新绑定首页 input group。 +- 内置页面:`launch.cpp` 创建页面后调用 `lv_indev_set_group(lv_indev_get_next(NULL), p->input_group())`。 +- 返回首页:`Launch::lv_go_back_home()` 会重新绑定首页 input group。 内置页面新增事件时,要确保事件挂在正确对象上,并且对象属于页面 input group。可参考已有页面的 `event_handler_init()`。 @@ -290,7 +290,7 @@ APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/ | 文件 | 作用 | | --- | --- | | `ext_components/cp0_lvgl/include/compat/input_keys.h` | 兼容输入 key 定义 | -| `projects/APPLaunch/main/include/keyboard_input.h` | APPLaunch 私有输入头 | +| `ext_components/cp0_lvgl/include/keyboard_input.h` | APPLaunch 私有输入头 | | `ext_components/cp0_lvgl/include/keyboard_input.h` | cp0_lvgl 输入接口 | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | 设备端键盘输入实现 | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | SDL2 键盘输入实现 | @@ -317,7 +317,7 @@ APPLAUNCH_LINUX_KEYBOARD_DEVICE=/dev/input/eventX sudo /usr/share/APPLaunch/bin/ ### 6.1 正常返回路径 -`Launch.cpp` 的 `launch_Exec()`: +`launch.cpp` 的 `launch_Exec()`: 1. 显示 Loading。 2. `LVGL_RUN_FLAGE = 0`。 @@ -343,7 +343,7 @@ ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|my_app|sh -c' | grep -v grep 检查: ```bash -sudo journalctl -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' +journalctl --user -u APPLaunch.service -f | grep -E 'CP0-APP|ESC|Returned' ``` 如果没有 `[CP0-APP] evdev` 日志: @@ -436,14 +436,14 @@ cat ../../SDK/github_source/static_lib_v0.0.4/version 2>/dev/null || true | 现象 | 原因 | 处理 | | --- | --- | --- | -| `PageT not declared` | 页面类名和注册名不一致,或 `.hpp` 未被 `page_app.h` include | 检查 `page_app.h`,重新运行 scons | +| `PageT not declared` | 页面类名和注册名不一致,或 `.hpp` 未被 `generated/page_app.h` include | 检查 `generated/page_app.h`,重新运行 scons | | SDL2 构建找不到 Linux 头 | 页面直接 include 设备端专用头 | 用 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 包住设备端代码 | | 链接找不到符号 | 新页面调用的函数未加入组件依赖 | 检查 `main/SConstruct` 的 `REQUIREMENTS`/`LDFLAGS` | | 重复定义 | header-only 页面里定义了非 inline 全局变量/函数 | 改成类成员、`static`、`inline`,或移入 `.cpp` | -### 7.5 `page_app.h` 自动生成导致工作区变化 +### 7.5 `generated/page_app.h` 自动生成导致工作区变化 -`generate_page_app_includes.py` 会按文件名排序生成 `page_app.h`。新增/删除 `page_app/*.hpp` 后,构建可能修改该文件。这是预期行为,但提交前要确认 diff 是否只包含预期 include 列表变化。 +`generate_page_app_includes.py` 会按文件名排序生成 `generated/page_app.h`。新增/删除 `page_app/*.hpp` 后,构建可能修改该文件。这是预期行为,但提交前要确认 diff 是否只包含预期 include 列表变化。 ## 8. `.desktop` 加载失败排查 @@ -506,7 +506,7 @@ sudo -u pi /usr/share/APPLaunch/bin/my_app 1. `git status --short` 确认当前改动范围。 2. SDL2 构建运行,排除基础 UI/语法问题。 3. 检查资源是否存在于 `projects/APPLaunch/APPLaunch` 和设备 `/usr/share/APPLaunch`。 -4. 查看 `journalctl -u APPLaunch.service -f`,定位启动阶段。 +4. 查看 `journalctl --user -u APPLaunch.service -f`,定位启动阶段。 5. 用 `evtest` 验证输入设备和按键码。 6. 用 `ps` 检查外部应用和进程组。 7. 检查 `/var/lib/applaunch/settings`,排除设置开关、亮度、运行用户导致的问题。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" index fee2680d..8c95f42a 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" @@ -12,19 +12,19 @@ git status --short | 任务 | 主要文件/目录 | 关键点 | 验证方式 | | --- | --- | --- | --- | | 新增内置页面 | `projects/APPLaunch/main/ui/page_app/` | 新建 `ui_app_xxx.hpp`,继承 `AppPage` | SDL2 编译并打开页面 | -| 注册内置页面到首页 | `projects/APPLaunch/main/ui/Launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | 首页轮播出现图标 | -| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/Launch.cpp` | 设置页写 `app_Key`,Launcher 读 `APP_ENABLED("Key")` | 切换设置后重启或刷新首页 | +| 注册内置页面到首页 | `projects/APPLaunch/main/ui/launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | 首页轮播出现图标 | +| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/launch.cpp` | 设置页写 `app_Key`,Launcher 读 `APP_ENABLED("Key")` | 切换设置后重启或刷新首页 | | 新增外部 `.desktop` 应用 | `projects/APPLaunch/APPLaunch/applications/` | 文件名必须以 `.desktop` 结尾,包含 `Name` 和 `Exec` | 日志无 skip,首页出现应用 | | 新增图标 | `projects/APPLaunch/APPLaunch/share/images/` | 内置页用 `img_path()`,`.desktop` 用 `Icon=share/images/xxx.png` | 日志无 `missing/unreadable` | | 新增音效 | `projects/APPLaunch/APPLaunch/share/audio/` | 页面用 `audio_path()` 和 `cp0_signal_audio_api()` | 设备端播放声音 | | 新增字体 | `projects/APPLaunch/APPLaunch/share/font/` | 用 `launcher_fonts().get()`,确认 FreeType 依赖 | 页面文字使用新字体 | -| 修改首页轮播布局 | `projects/APPLaunch/main/ui/UILaunchPage.cpp`、`projects/APPLaunch/main/ui/UILaunchPage.h` | 5 个 slot、左右切换、中心卡片 | SDL2 查看动画和输入 | -| 修改轮播动画 | `projects/APPLaunch/main/ui/Animation/ui_launcher_animation.cpp` | 卡片移动、缩放、透明度等动画 | SDL2 连续左右切换 | -| 修改首页状态栏 | `projects/APPLaunch/main/ui/Launch.cpp`、`projects/APPLaunch/main/ui/ui.c` | `update_home_status_bar()` 刷新 WiFi/时间/电量 | 看 `[HOME_STATUS]` 日志 | +| 修改首页轮播布局 | `projects/APPLaunch/main/ui/ui_launch_page.cpp`、`projects/APPLaunch/main/ui/ui_launch_page.h` | 5 个 slot、左右切换、中心卡片 | SDL2 查看动画和输入 | +| 修改轮播动画 | `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | 卡片移动、缩放、透明度等动画 | SDL2 连续左右切换 | +| 修改首页状态栏 | `projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/ui.cpp` | `update_home_status_bar()` 刷新 WiFi/时间/电量 | 看 `[HOME_STATUS]` 日志 | | 修改设置页菜单 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `menu_init()` 增加 `MenuItem`/`SubItem` | 进入 SETTING 页面测试 | | 修改配置保存逻辑 | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 当前保存到 `/var/lib/applaunch/settings`,最多 32 项 | 查看 settings 文件 | | 修改资源路径规则 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | 设备端和 SDL2 要同步考虑 | SDL2 + 设备端都检查资源 | -| 修改外部应用启动/返回 | `projects/APPLaunch/main/ui/Launch.cpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`、`cp0_process_exec_blocking()` | 外部应用启动、ESC 返回 | +| 修改外部应用启动/返回 | `projects/APPLaunch/main/ui/launch.cpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`、`cp0_process_exec_blocking()` | 外部应用启动、ESC 返回 | | 修改终端应用 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、命令执行、输入输出 | `Terminal=true` 应用验证 | | 修改输入映射 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | 设备端和 SDL2 输入差异 | `evtest` + SDL2 键盘 | | 修改启动流程 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、主循环 | 看 `[BOOT]` 日志 | @@ -38,21 +38,21 @@ git status --short | 路径 | 用途 | | --- | --- | | `projects/APPLaunch/main/src/main.cpp` | APPLaunch 进程入口、初始化顺序、主循环、外部应用锁检测 | -| `projects/APPLaunch/main/ui/ui.c` | LVGL 全局 UI 对象创建,多数 `ui_*` 全局对象来源于这里 | +| `projects/APPLaunch/main/ui/ui.cpp` | LVGL 全局 UI 对象创建,多数 `ui_*` 全局对象来源于这里 | | `projects/APPLaunch/main/ui/ui.cpp` | C++ UI 初始化桥接 | | `projects/APPLaunch/main/ui/ui.h` | UI 全局声明、C/C++ 共享接口 | -| `projects/APPLaunch/main/ui/Launch.cpp` | 应用模型、应用列表、启动逻辑、动态 `.desktop` 加载、状态栏刷新 | -| `projects/APPLaunch/main/ui/Launch.h` | `Launch` 对外包装类 | -| `projects/APPLaunch/main/ui/UILaunchPage.cpp` | 首页 screen、轮播 slot、输入事件、首页页面行为 | -| `projects/APPLaunch/main/ui/UILaunchPage.h` | 首页类接口,包含 panel/label/input group 访问函数 | +| `projects/APPLaunch/main/ui/launch.cpp` | 应用模型、应用列表、启动逻辑、动态 `.desktop` 加载、状态栏刷新 | +| `projects/APPLaunch/main/ui/launch.h` | `Launch` 对外包装类 | +| `projects/APPLaunch/main/ui/ui_launch_page.cpp` | 首页 screen、轮播 slot、输入事件、首页页面行为 | +| `projects/APPLaunch/main/ui/ui_launch_page.h` | 首页类接口,包含 panel/label/input group 访问函数 | | `projects/APPLaunch/main/ui/ui_loading.cpp` | Loading 遮罩显示和隐藏 | | `projects/APPLaunch/main/ui/ui_global_hint.cpp` | 全局提示浮层 | -| `projects/APPLaunch/main/ui/zero_lvgl_os.cpp` | LVGL OS/线程辅助 | -| `projects/APPLaunch/main/ui/Animation/` | 首页轮播动画实现 | +| `projects/APPLaunch/main/ui/launcher_ui_runtime.cpp` | LVGL OS/线程辅助 | +| `projects/APPLaunch/main/ui/animation/` | 首页轮播动画实现 | | `projects/APPLaunch/main/ui/ui_app_page.hpp` | 内置页面基类、顶部栏、公共资源路径辅助 | -| `projects/APPLaunch/main/ui/page_app.h` | 自动生成的内置页面 include 汇总 | +| `projects/APPLaunch/build/generated/include/generated/page_app.h` | 自动生成的内置页面 include 汇总 | | `projects/APPLaunch/main/ui/page_app/` | 内置页面实现目录 | -| `projects/APPLaunch/main/include/` | APPLaunch 私有头和兼容输入头 | +| `ext_components/cp0_lvgl/include/` | CP0/LVGL 共享头,包括键盘和兼容输入头 | ## 3. 内置页面入口表 @@ -60,9 +60,9 @@ git status --short | --- | --- | --- | --- | | GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | | SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | 设置页,包含应用开关、亮度、音量、WiFi、相机等 | -| MUSIC | `projects/APPLaunch/main/ui/page_app/ui_app_music.hpp` | `MUSIC` / `music_100.png` | 音乐页 | +| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | | Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | 指南针页 | -| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_IpPanel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP 信息面板,设备端启用 | +| IP_PANEL | `projects/APPLaunch/main/ui/page_app/ui_app_ip_panel.hpp` | `IP_PANEL` / `ip_panel_100.png` | IP 信息面板,设备端启用 | | FILE | `projects/APPLaunch/main/ui/page_app/ui_app_file.hpp` | `FILE` / `file_100.png` | 文件页,设备端启用 | | SSH | `projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp` | `SSH` / `ssh_100.png` | SSH 页面,设备端启用 | | MESH | `projects/APPLaunch/main/ui/page_app/ui_app_mesh.hpp` | `MESH` / `mesh_100.png` | Mesh 页面,设备端启用 | @@ -72,7 +72,7 @@ git status --short | TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | 坦克游戏,设备端启用 | | CLI/终端 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`,用于 bash、python、Terminal=true 应用 | -固定注册入口在 `LaunchImpl::LaunchImpl()`: +固定注册入口在 `Launch::Launch()`: ```cpp app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); @@ -88,11 +88,11 @@ app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v Launcher | `app_` | `ui_app_setup.hpp` 的 `save_app_toggle()`,`Launch.cpp` 的 `APP_ENABLED()` | +| 应用显示开关 | SETTING -> Launcher | `app_` | `ui_app_setup.hpp` 的 `save_app_toggle()`,`launch.cpp` 的 `APP_ENABLED()` | | 亮度 | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | | 息屏时间 | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | | 音量 | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`、`cp0_volume_read/write()` | @@ -190,23 +190,23 @@ Type=Application | SDL2 构建 | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_sdl2_config_defaults.mk scons -j8 --implicit-deps-changed` | | SDL2 运行 | `cd projects/APPLaunch && ./dist/M5CardputerZero-APPLaunch` | | 交叉编译 | `cd projects/APPLaunch && CONFIG_DEFAULT_FILE=linux_x86_cross_cp0_config_defaults.mk scons -j8 --implicit-deps-changed` | -| 查看 systemd 状态 | `sudo systemctl status APPLaunch.service --no-pager` | -| 跟踪日志 | `sudo journalctl -u APPLaunch.service -f` | -| 查看启动日志 | `sudo journalctl -u APPLaunch.service -b --no-pager` | +| 查看 systemd 状态 | `systemctl --user status APPLaunch.service --no-pager` | +| 跟踪日志 | `journalctl --user -u APPLaunch.service -f` | +| 查看启动日志 | `journalctl --user -u APPLaunch.service -b --no-pager` | | 查资源 | `find /usr/share/APPLaunch -maxdepth 3 -type f | sort` | | 查 `.desktop` | `find /usr/share/APPLaunch/applications -maxdepth 1 -type f -name '*.desktop' -print -exec sed -n '1,80p' {} \;` | | 查设置 | `sudo cat /var/lib/applaunch/settings` | | 查输入设备 | `ls -l /dev/input/by-path/ && sudo evtest` | | 查外部应用进程 | `ps -eo pid,ppid,pgid,stat,cmd | grep -E 'APPLaunch|sh -c|M5CardputerZero'` | | 查动态库 | `ldd /usr/share/APPLaunch/bin/my_app` | -| 查图标日志 | `sudo journalctl -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | +| 查图标日志 | `journalctl --user -u APPLaunch.service -b --no-pager | grep 'set panel icon'` | ## 10. 改动前后检查清单 | 阶段 | 检查项 | | --- | --- | | 改动前 | `git status --short`,确认哪些文件已有他人改动 | -| 新增页面后 | 确认 `.hpp` 文件在 `page_app/`,类名与 `Launch.cpp` 注册一致 | +| 新增页面后 | 确认 `.hpp` 文件在 `page_app/`,类名与 `launch.cpp` 注册一致 | | 新增资源后 | 源码树和设备端 `/usr/share/APPLaunch` 都能找到文件 | | 新增 `.desktop` 后 | 文件后缀是 `.desktop`,有 `[Desktop Entry]`、`Name`、`Exec` | | 修改设置后 | `/var/lib/applaunch/settings` 写入正确 key,未超过配置条目上限 | diff --git a/docs/macos-docker-build-ja.md b/docs/macos-docker-build-ja.md new file mode 100644 index 00000000..bcb869fe --- /dev/null +++ b/docs/macos-docker-build-ja.md @@ -0,0 +1,108 @@ +# macOS Docker ビルド & デプロイガイド + +macOS (Apple Silicon) 上で Docker のネイティブ arm64 環境を使って APPLaunch をビルドします。 + +## 前提条件 + +1. Lima (Docker 実行環境) をインストールします: +```bash +brew install lima +limactl start default +docker context use lima-default +``` + +2. Docker が arm64 ネイティブであることを確認します: +```bash +docker info | grep Architecture +# 应输出: aarch64 +``` + +3. 事前キャッシュ済みイメージをビルドします(一度だけ実行。以後のビルドではパッケージの再インストールは不要): +```bash +docker build --platform linux/arm64 -t cardputer-build -f - . <<'EOF' +FROM --platform=linux/arm64 ubuntu:24.04 +RUN apt-get update && apt-get install -y \ + gcc g++ python3 python3-pip scons \ + libfreetype-dev libpng-dev libjpeg-dev \ + libinput-dev libxkbcommon-dev libudev-dev libcamera-dev pip && \ + pip install parse requests tqdm --break-system-packages && \ + rm -rf /var/lib/apt/lists/* +EOF +``` + +## 実行ファイルをビルド + +事前ビルド済みイメージを使用します(数秒程度): +```bash +docker run --rm -v $(git rev-parse --show-toplevel):/src -w /src/projects/APPLaunch \ + cardputer-build bash -c "CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4" +``` + +事前ビルド済みイメージを使用しない場合(初回は apt install が必要。約 2〜3 分): +```bash +docker run --rm --platform linux/arm64 \ + -v $(git rev-parse --show-toplevel):/src \ + -w /src/projects/APPLaunch \ + ubuntu:24.04 bash -c " + apt-get update -qq && + apt-get install -y -qq gcc g++ python3 python3-pip scons \ + libfreetype-dev libpng-dev libjpeg-dev \ + libinput-dev libxkbcommon-dev libudev-dev libcamera-dev pip >/dev/null 2>&1 && + pip install parse requests tqdm --break-system-packages -q && + rm -rf build dist && + CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4 + " +``` + +成果物のパス:`projects/APPLaunch/dist/M5CardputerZero-APPLaunch` + +## deb パッケージをビルド + +ビルド + deb パッケージング(1 コマンド): +```bash +docker run --rm -v $(git rev-parse --show-toplevel):/src -w /src/projects/APPLaunch \ + cardputer-build bash -c "CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4 && cd ../.. && python3 scripts/debian_packager.py" +``` + +成果物のパス:`projects/APPLaunch/tools/applaunch_*.deb`(約 15MB) + +## 実行ファイルをデプロイ + +バイナリを直接 scp でデバイスへ転送し、サービスを再起動します(高速な反復開発向け): +```bash +scp projects/APPLaunch/dist/M5CardputerZero-APPLaunch pi@192.168.50.150:/tmp/ +ssh pi@192.168.50.150 "echo pi | sudo -S install -m 0755 /tmp/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch && systemctl --user restart APPLaunch.service" +``` + +または scons push を使用します(paramiko/scp が必要): +```bash +pip3 install paramiko scp --break-system-packages +python3 -m SCons push +``` + +scons push は `setup.ini` 内のデバイス IP と認証情報を読み取り、MD5 を自動比較して差分だけをプッシュします。 + +## deb パッケージをデプロイ + +```bash +scp projects/APPLaunch/tools/applaunch_*.deb pi@192.168.50.150:/tmp/ +ssh pi@192.168.50.150 "echo pi | sudo -S dpkg -i /tmp/applaunch_*.deb && systemctl --user status APPLaunch.service --no-pager" +``` + +deb パッケージはリソースファイル一式(フォント、画像、systemd user service など)をインストールするため、正式リリースに適しています。 + +## ワンコマンドビルド + デプロイ + +```bash +docker run --rm -v $(git rev-parse --show-toplevel):/src -w /src/projects/APPLaunch \ + cardputer-build bash -c "CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4" && \ +scp projects/APPLaunch/dist/M5CardputerZero-APPLaunch pi@192.168.50.150:/tmp/ && \ +ssh pi@192.168.50.150 "echo pi | sudo -S install -m 0755 /tmp/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch && systemctl --user restart APPLaunch.service" +``` + +## 注意事項 + +- Lima VM は aarch64 ネイティブのため、QEMU エミュレーションは不要で、ビルド速度は実機に近くなります +- 初回ビルドでは GitHub から lvgl ソースコード zip(約 100MB)をダウンロードし、その後は `SDK/github_source/` にキャッシュされます +- Docker volume mount はローカルの `build/` と `dist/` に直接書き込むため、追加のコピーは不要です +- `setup.ini` でデバイス IP を設定します(デフォルトは 192.168.50.150) diff --git a/docs/macos-docker-build.md b/docs/macos-docker-build.md index a4bfadac..9752698f 100644 --- a/docs/macos-docker-build.md +++ b/docs/macos-docker-build.md @@ -1,6 +1,6 @@ # macOS Docker 编译 & 部署指南 -在 macOS (Apple Silicon) 上使用 Docker 原生 arm64 编译 APPLauncher。 +在 macOS (Apple Silicon) 上使用 Docker 原生 arm64 编译 APPLaunch。 ## 前置条件 @@ -71,7 +71,7 @@ docker run --rm -v $(git rev-parse --show-toplevel):/src -w /src/projects/APPLau 直接 scp 二进制到设备并重启服务(快速迭代): ```bash scp projects/APPLaunch/dist/M5CardputerZero-APPLaunch pi@192.168.50.150:/tmp/ -ssh pi@192.168.50.150 "echo pi | sudo -S cp /tmp/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/ && echo pi | sudo -S systemctl restart APPLaunch" +ssh pi@192.168.50.150 "echo pi | sudo -S install -m 0755 /tmp/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch && systemctl --user restart APPLaunch.service" ``` 或使用 scons push(需要 paramiko/scp): @@ -86,10 +86,10 @@ scons push 会读取 `setup.ini` 中的设备 IP 和凭据,自动比较 MD5 ```bash scp projects/APPLaunch/tools/applaunch_*.deb pi@192.168.50.150:/tmp/ -ssh pi@192.168.50.150 "echo pi | sudo -S dpkg -i /tmp/applaunch_*.deb && echo pi | sudo -S systemctl restart APPLaunch" +ssh pi@192.168.50.150 "echo pi | sudo -S dpkg -i /tmp/applaunch_*.deb && systemctl --user status APPLaunch.service --no-pager" ``` -deb 包会安装完整的资源文件(字体、图片、systemd service 等),适合正式发布。 +deb 包会安装完整的资源文件(字体、图片、systemd user service 等),适合正式发布。 ## 一键编译 + 部署 @@ -97,7 +97,7 @@ deb 包会安装完整的资源文件(字体、图片、systemd service 等) docker run --rm -v $(git rev-parse --show-toplevel):/src -w /src/projects/APPLaunch \ cardputer-build bash -c "CardputerZero=y CONFIG_REPO_AUTOMATION=y scons -j4" && \ scp projects/APPLaunch/dist/M5CardputerZero-APPLaunch pi@192.168.50.150:/tmp/ && \ -ssh pi@192.168.50.150 "echo pi | sudo -S cp /tmp/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/ && echo pi | sudo -S systemctl restart APPLaunch" +ssh pi@192.168.50.150 "echo pi | sudo -S install -m 0755 /tmp/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin/M5CardputerZero-APPLaunch && systemctl --user restart APPLaunch.service" ``` ## 注意事项 diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index eebfb3a9..da31544d 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -106,8 +106,10 @@ if 'CONFIG_APPLAUNCH_CROSS' in os.environ: LDFLAGS += [rootfs_path/"usr"/"lib"/"aarch64-linux-gnu"/"libstdc++.so.6"] -# x86 -if 'CONFIG_APPLAUNCH_LINUX_X86_SDL2' in os.environ: +# Host SDL2 builds (Linux x86_64 and Windows x86) need pkg-config flags +# for the APPLaunch sources and for LVGL's SDL/FreeType features. +if ('CONFIG_APPLAUNCH_LINUX_X86_SDL2' in os.environ or + 'CONFIG_APPLAUNCH_WIN_X86_SDL2' in os.environ): lvgl_component['DEFINITIONS'] += pkg_config_cflags("freetype2") lvgl_component['REQUIREMENTS'] += pkg_config_ldflags("freetype2") DEFINITIONS += pkg_config_cflags("sdl2") From f214001cc17510a109627272b7a480e13dc42b7a Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 16 Jun 2026 11:54:22 +0800 Subject: [PATCH 66/70] Refactor LoRa app page lifecycle --- .../main/ui/page_app/ui_app_lora.hpp | 989 +++++++++--------- 1 file changed, 511 insertions(+), 478 deletions(-) diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp index 644f2705..fdb4b292 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp @@ -20,64 +20,29 @@ #include #include -namespace Lora_APP -{ - -void ui_app_lora_create(lv_obj_t* parent, lv_obj_t* root); -void ui_app_lora_set_go_back(std::function go_back); -void ui_app_lora_destroy(void); -void lora_app_task(); +namespace lora_app_detail { -static std::function g_go_back_home_fn; +constexpr uint32_t kPollIntervalMs = 300; +constexpr uint32_t kRxPopupDurationMs = 2000; +constexpr lv_coord_t kScreenWidth = 320; +constexpr lv_coord_t kContentWidth = 304; -void ui_app_lora_set_go_back(std::function go_back) +inline const char *safe_text(const char *text, const char *fallback = "") { - g_go_back_home_fn = go_back; + return text && text[0] ? text : fallback; } -enum LoraView { - LORA_VIEW_MESSAGES = 0, - LORA_VIEW_INFO, - LORA_VIEW_SEND, -}; +inline bool is_printable_ascii(uint32_t key) +{ + return key >= 0x20 && key <= 0x7e; +} -static LoraView g_lora_view = LORA_VIEW_MESSAGES; -static bool g_app_active = false; -static uint32_t g_lora_sent_popup_until_ms = 0; -static char g_lora_tx_input[128] = ""; -static cp0_lora_info_t g_lora_info{}; - -static lv_obj_t *g_ui_parent = nullptr; -static lv_obj_t *g_ui_root = nullptr; -static lv_obj_t *g_title_label = nullptr; -static lv_obj_t *g_content_label = nullptr; -static lv_obj_t *g_info_pins = nullptr; -static lv_obj_t *g_info_device = nullptr; -static lv_obj_t *g_info_mode = nullptr; -static lv_obj_t *g_info_status = nullptr; -static lv_obj_t *g_info_hint = nullptr; -static lv_timer_t *g_lora_timer = nullptr; -static lv_obj_t *g_rx_bubble_bg = nullptr; -static lv_obj_t *g_rx_bubble_lbl = nullptr; -static lv_obj_t *g_tx_bubble_bg = nullptr; -static lv_obj_t *g_tx_bubble_lbl = nullptr; - -static void lora_render_current_view(void); -static void lora_render_messages_view(void); -static void lora_render_info_view(void); -static void lora_render_send_view(void); -static void lora_render_sent_popup(void); -static void lora_open_send_view(uint32_t first_key); -static bool is_lora_text_key(uint32_t key); -static char lora_key_to_char(uint32_t key); -static bool handle_app_key(uint32_t key); - -static const char *safe_text(const char *text, const char *fallback = "") +inline char key_to_ascii(uint32_t key) { - return text && text[0] ? text : fallback; + return is_printable_ascii(key) ? static_cast(key) : '\0'; } -static int lora_api_call(const std::list& args, cp0_lora_info_t *info = nullptr) +inline int call_lora_api(const std::list& args, cp0_lora_info_t *info = nullptr) { int result = -1; cp0_signal_lora_api(args, [&](int code, std::string data) { @@ -89,509 +54,577 @@ static int lora_api_call(const std::list& args, cp0_lora_info_t *in return result; } -static void refresh_lora_info(bool poll) -{ - (void)lora_api_call({poll ? "Poll" : "Info"}, &g_lora_info); -} - -static void lora_ui_clear(void) +inline bool is_menu_prev_key(uint32_t key) { - if (g_title_label) lv_obj_add_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); - if (g_content_label) lv_obj_add_flag(g_content_label, LV_OBJ_FLAG_HIDDEN); - if (g_info_pins) lv_obj_add_flag(g_info_pins, LV_OBJ_FLAG_HIDDEN); - if (g_info_device) lv_obj_add_flag(g_info_device, LV_OBJ_FLAG_HIDDEN); - if (g_info_mode) lv_obj_add_flag(g_info_mode, LV_OBJ_FLAG_HIDDEN); - if (g_info_status) lv_obj_add_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - if (g_info_hint) lv_obj_add_flag(g_info_hint, LV_OBJ_FLAG_HIDDEN); - if (g_rx_bubble_bg) lv_obj_add_flag(g_rx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - if (g_tx_bubble_bg) lv_obj_add_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); -} - -static lv_obj_t* lora_make_label(lv_obj_t *parent, const char *text, lv_coord_t x, lv_coord_t y, lv_coord_t w, lv_coord_t h, - const lv_font_t *font, lv_color_t color, lv_text_align_t align) -{ - lv_obj_t *lbl = lv_label_create(parent); - lv_label_set_text(lbl, text ? text : ""); - lv_obj_set_pos(lbl, x, y); - lv_obj_set_size(lbl, w, h); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); - lv_obj_set_style_text_font(lbl, font ? font : &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(lbl, color, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_opa(lbl, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(lbl, LV_OPA_TRANSP, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(lbl, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(lbl, align, LV_PART_MAIN | LV_STATE_DEFAULT); - return lbl; + return key == LV_KEY_LEFT || key == LV_KEY_PREV || key == 'z' || key == 'Z'; } -static lv_obj_t* lora_make_bubble(lv_obj_t *parent, lv_coord_t x, lv_coord_t y, lv_coord_t w, lv_coord_t h, lv_color_t bg_color) +inline bool is_menu_next_key(uint32_t key) { - lv_obj_t *bg = lv_obj_create(parent); - lv_obj_set_pos(bg, x, y); - lv_obj_set_size(bg, w, h); - lv_obj_set_style_bg_color(bg, bg_color, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(bg, 10, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(bg, 8, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(bg, 8, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(bg, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(bg, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_width(bg, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_color(bg, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_opa(bg, LV_OPA_20, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_spread(bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_x(bg, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(bg, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_clear_flag(bg, LV_OBJ_FLAG_SCROLLABLE); - return bg; + return key == LV_KEY_RIGHT || key == LV_KEY_NEXT || key == 'c' || key == 'C'; } -static lv_obj_t* lora_make_bubble_label(lv_obj_t *parent, lv_coord_t max_w) -{ - lv_obj_t *lbl = lv_label_create(parent); - lv_obj_set_width(lbl, max_w); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(lbl, LV_OPA_TRANSP, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_all(lbl, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - return lbl; -} +} // namespace lora_app_detail -static void lora_render_boot_diag(void) +class UILoraPage : public AppPage { - lora_ui_clear(); - if (g_title_label) { - lv_obj_clear_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_title_label, "LoRa-1262"); +public: + UILoraPage() : AppPage() + { + create_ui(); + bind_events(); + init_lora(); } - if (g_content_label) { - lv_obj_clear_flag(g_content_label, LV_OBJ_FLAG_HIDDEN); - char text[256]; - snprintf(text, sizeof(text), "SPI:%s\n%s\n%s", - safe_text(g_lora_info.spi_device, "n/a"), - safe_text(g_lora_info.pi4io_status, "I2C status unavailable"), - safe_text(g_lora_info.probe_summary, "probe not started")); - lv_label_set_text(g_content_label, text); - lv_obj_set_style_text_color(g_content_label, lv_color_hex(0xFF4D4F), LV_PART_MAIN | LV_STATE_DEFAULT); + + ~UILoraPage() override + { + app_active_ = false; + if (poll_timer_) { + lv_timer_delete(poll_timer_); + poll_timer_ = nullptr; + } } - if (g_info_status) { - lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_status, safe_text(g_lora_info.diag, "LoRa not ready")); - lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFF4D4F), LV_PART_MAIN | LV_STATE_DEFAULT); + +private: + enum class View { + Messages, + Info, + Send, + }; + + View current_view_ = View::Messages; + bool app_active_ = false; + bool pending_rx_popup_ = false; + uint32_t rx_popup_started_ms_ = 0; + char tx_input_[128] = ""; + char send_status_[64] = ""; + cp0_lora_info_t lora_info_{}; + + lv_timer_t *poll_timer_ = nullptr; + lv_obj_t *title_label_ = nullptr; + lv_obj_t *content_label_ = nullptr; + lv_obj_t *info_pins_label_ = nullptr; + lv_obj_t *info_device_label_ = nullptr; + lv_obj_t *info_mode_label_ = nullptr; + lv_obj_t *info_status_label_ = nullptr; + lv_obj_t *info_hint_label_ = nullptr; + lv_obj_t *rx_bubble_ = nullptr; + lv_obj_t *rx_bubble_label_ = nullptr; + lv_obj_t *tx_bubble_ = nullptr; + lv_obj_t *tx_bubble_label_ = nullptr; + + static lv_obj_t *make_label(lv_obj_t *parent, + const char *text, + lv_coord_t x, + lv_coord_t y, + lv_coord_t w, + lv_coord_t h, + const lv_font_t *font, + lv_color_t color, + lv_text_align_t align) + { + lv_obj_t *label = lv_label_create(parent); + lv_label_set_text(label, text ? text : ""); + lv_obj_set_pos(label, x, y); + lv_obj_set_size(label, w, h); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(label, font ? font : &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(label, color, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_opa(label, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(label, LV_OPA_TRANSP, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(label, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(label, align, LV_PART_MAIN | LV_STATE_DEFAULT); + return label; + } + + static lv_obj_t *make_bubble(lv_obj_t *parent, lv_coord_t x, lv_coord_t y, lv_coord_t w, lv_coord_t h, lv_color_t bg_color) + { + lv_obj_t *bubble = lv_obj_create(parent); + lv_obj_set_pos(bubble, x, y); + lv_obj_set_size(bubble, w, h); + lv_obj_set_style_bg_color(bubble, bg_color, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(bubble, 10, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(bubble, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(bubble, 8, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(bubble, 8, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(bubble, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(bubble, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_width(bubble, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_color(bubble, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_opa(bubble, LV_OPA_20, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_spread(bubble, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_x(bubble, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(bubble, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_clear_flag(bubble, LV_OBJ_FLAG_SCROLLABLE); + return bubble; + } + + static lv_obj_t *make_bubble_label(lv_obj_t *parent, lv_coord_t max_width) + { + lv_obj_t *label = lv_label_create(parent); + lv_obj_set_width(label, max_width); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_font(label, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(label, LV_OPA_TRANSP, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(label, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + return label; } - if (g_info_hint) { - lv_obj_clear_flag(g_info_hint, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_hint, safe_text(g_lora_info.probe_display, "Boot diag for SPI check")); - lv_obj_set_style_text_color(g_info_hint, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); + + void create_ui() + { + title_label_ = make_label(ui_APP_Container, "LoRa-1262", 0, 0, lora_app_detail::kScreenWidth, 18, + &lv_font_montserrat_14, lv_color_hex(0x8D44FF), LV_TEXT_ALIGN_CENTER); + content_label_ = make_label(ui_APP_Container, "", 8, 22, lora_app_detail::kContentWidth, 90, + &lv_font_montserrat_12, lv_color_hex(0xFFFFFF), LV_TEXT_ALIGN_LEFT); + info_pins_label_ = make_label(ui_APP_Container, "", 8, 22, lora_app_detail::kContentWidth, 18, + &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); + info_device_label_ = make_label(ui_APP_Container, "", 8, 42, lora_app_detail::kContentWidth, 18, + &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); + info_mode_label_ = make_label(ui_APP_Container, "", 8, 62, lora_app_detail::kContentWidth, 18, + &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); + info_status_label_ = make_label(ui_APP_Container, "", 8, 114, lora_app_detail::kContentWidth, 16, + &lv_font_montserrat_12, lv_color_hex(0xFFD24A), LV_TEXT_ALIGN_LEFT); + info_hint_label_ = make_label(ui_APP_Container, "", 8, 132, lora_app_detail::kContentWidth, 14, + &lv_font_montserrat_10, lv_color_hex(0x8AA8FF), LV_TEXT_ALIGN_LEFT); + + rx_bubble_ = make_bubble(ui_APP_Container, 4, 20, 250, 44, lv_color_hex(0x3A7DFF)); + rx_bubble_label_ = make_bubble_label(rx_bubble_, 234); + lv_obj_add_flag(rx_bubble_, LV_OBJ_FLAG_HIDDEN); + + tx_bubble_ = make_bubble(ui_APP_Container, 66, 68, 250, 44, lv_color_hex(0x00A854)); + tx_bubble_label_ = make_bubble_label(tx_bubble_, 234); + lv_obj_add_flag(tx_bubble_, LV_OBJ_FLAG_HIDDEN); + } + + void bind_events() + { + lv_obj_add_event_cb(root_screen_, &UILoraPage::static_key_event_cb, + static_cast(LV_EVENT_KEYBOARD), this); } -} -static void lora_render_page(void) -{ - if (!g_ui_parent) return; - if (!g_lora_info.hw_ready) { - lora_render_boot_diag(); - return; + void init_lora() + { + app_active_ = true; + current_view_ = View::Messages; + pending_rx_popup_ = false; + rx_popup_started_ms_ = 0; + tx_input_[0] = '\0'; + send_status_[0] = '\0'; + + (void)lora_app_detail::call_lora_api({"Init"}); + refresh_lora_info(false); + render_lora_page(); + poll_timer_ = lv_timer_create(&UILoraPage::static_poll_timer_cb, lora_app_detail::kPollIntervalMs, this); } - g_lora_view = LORA_VIEW_MESSAGES; - lora_render_current_view(); - if (!g_lora_info.tx_mode && !g_lora_info.tx_in_progress) { - (void)lora_api_call({"StartReceive"}); + + void refresh_lora_info(bool poll) + { + (void)lora_app_detail::call_lora_api({poll ? "Poll" : "Info"}, &lora_info_); } -} -static void lora_render_messages_view(void) -{ - lora_ui_clear(); - if (g_title_label) { - lv_obj_clear_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_title_label, "Messages"); + void clear_view() + { + if (title_label_) lv_obj_add_flag(title_label_, LV_OBJ_FLAG_HIDDEN); + if (content_label_) lv_obj_add_flag(content_label_, LV_OBJ_FLAG_HIDDEN); + if (info_pins_label_) lv_obj_add_flag(info_pins_label_, LV_OBJ_FLAG_HIDDEN); + if (info_device_label_) lv_obj_add_flag(info_device_label_, LV_OBJ_FLAG_HIDDEN); + if (info_mode_label_) lv_obj_add_flag(info_mode_label_, LV_OBJ_FLAG_HIDDEN); + if (info_status_label_) lv_obj_add_flag(info_status_label_, LV_OBJ_FLAG_HIDDEN); + if (info_hint_label_) lv_obj_add_flag(info_hint_label_, LV_OBJ_FLAG_HIDDEN); + if (rx_bubble_) lv_obj_add_flag(rx_bubble_, LV_OBJ_FLAG_HIDDEN); + if (tx_bubble_) lv_obj_add_flag(tx_bubble_, LV_OBJ_FLAG_HIDDEN); + } + + void reset_content_label_style() + { + if (!content_label_) return; + lv_obj_set_style_text_font(content_label_, &lv_font_montserrat_12, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(content_label_, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } - if (g_rx_bubble_bg && g_rx_bubble_lbl) { - lv_obj_clear_flag(g_rx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(g_rx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_pos(g_rx_bubble_bg, 4, 20); - lv_obj_set_size(g_rx_bubble_bg, 250, 44); - lv_obj_set_width(g_rx_bubble_lbl, 234); - if (g_lora_info.last_rx[0]) { - lv_label_set_text(g_rx_bubble_lbl, g_lora_info.last_rx); - lv_obj_set_style_text_color(g_rx_bubble_lbl, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_label_set_text(g_rx_bubble_lbl, "Waiting for message..."); - lv_obj_set_style_text_color(g_rx_bubble_lbl, lv_color_hex(0xAACCFF), LV_PART_MAIN | LV_STATE_DEFAULT); + void render_lora_page() + { + if (!lora_info_.hw_ready) { + render_boot_diag_view(); + return; } - } - if (g_tx_bubble_bg && g_tx_bubble_lbl) { - if (g_lora_info.has_sent_message) { - lv_obj_clear_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(g_tx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_pos(g_tx_bubble_bg, 66, 68); - lv_obj_set_size(g_tx_bubble_bg, 250, 44); - lv_obj_set_width(g_tx_bubble_lbl, 234); - lv_label_set_text(g_tx_bubble_lbl, safe_text(g_lora_info.last_tx)); - } else { - lv_obj_add_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(g_tx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); + current_view_ = View::Messages; + render_current_view(); + if (!lora_info_.tx_mode && !lora_info_.tx_in_progress) { + (void)lora_app_detail::call_lora_api({"StartReceive"}); } } - if (g_info_status) { - lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - char text[128]; - snprintf(text, sizeof(text), "RSSI: %.1fdBm | SNR: %.1fdB", g_lora_info.rssi, g_lora_info.snr); - lv_label_set_text(g_info_status, text); - lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_hint) { - lv_obj_clear_flag(g_info_hint, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_hint, "Type to send | C/Right: Info | ESC: Back"); - lv_obj_set_style_text_color(g_info_hint, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); + void render_boot_diag_view() + { + clear_view(); + if (title_label_) { + lv_obj_clear_flag(title_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(title_label_, "LoRa-1262"); + } + if (content_label_) { + reset_content_label_style(); + lv_obj_clear_flag(content_label_, LV_OBJ_FLAG_HIDDEN); + char text[256]; + std::snprintf(text, sizeof(text), "SPI:%s\n%s\n%s", + lora_app_detail::safe_text(lora_info_.spi_device, "n/a"), + lora_app_detail::safe_text(lora_info_.pi4io_status, "I2C status unavailable"), + lora_app_detail::safe_text(lora_info_.probe_summary, "probe not started")); + lv_label_set_text(content_label_, text); + lv_obj_set_style_text_color(content_label_, lv_color_hex(0xFF4D4F), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_status_label_) { + lv_obj_clear_flag(info_status_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_status_label_, lora_app_detail::safe_text(lora_info_.diag, "LoRa not ready")); + lv_obj_set_style_text_color(info_status_label_, lv_color_hex(0xFF4D4F), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_hint_label_) { + lv_obj_clear_flag(info_hint_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_hint_label_, lora_app_detail::safe_text(lora_info_.probe_display, "Boot diag for SPI check")); + lv_obj_set_style_text_color(info_hint_label_, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); + } } -} -static void lora_render_info_view(void) -{ - lora_ui_clear(); - if (g_title_label) { - lv_obj_clear_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_title_label, "LoRa Info"); - } - if (g_info_pins) { - lv_obj_clear_flag(g_info_pins, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_pins, "Role: Client"); - lv_obj_set_style_text_color(g_info_pins, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_device) { - lv_obj_clear_flag(g_info_device, LV_OBJ_FLAG_HIDDEN); - char text[192]; - snprintf(text, sizeof(text), "Device: %s | RX:%s TX:%s", - safe_text(g_lora_info.spi_device, "n/a"), - g_lora_info.hw_ready ? "ready" : "off", - g_lora_info.tx_in_progress ? "busy" : "idle"); - lv_label_set_text(g_info_device, text); - lv_obj_set_style_text_color(g_info_device, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_mode) { - lv_obj_clear_flag(g_info_mode, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_mode, safe_text(g_lora_info.probe_display, "Channel: 868MHz | BW:125kHz SF12")); - lv_obj_set_style_text_color(g_info_mode, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_status) { - lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_status, safe_text(g_lora_info.diag, "LoRa ready")); - lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_hint) { - lv_obj_clear_flag(g_info_hint, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_hint, "Z/Left: Messages | Type: Send | ESC: Back"); - lv_obj_set_style_text_color(g_info_hint, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); - } -} + void render_messages_view() + { + clear_view(); + if (title_label_) { + lv_obj_clear_flag(title_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(title_label_, "Messages"); + } -static void lora_render_send_view(void) -{ - lora_ui_clear(); - if (g_title_label) { - lv_obj_clear_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_title_label, "Send"); - } - if (g_content_label) { - lv_obj_clear_flag(g_content_label, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_content_label, g_lora_tx_input[0] ? g_lora_tx_input : "_"); - lv_obj_set_style_text_font(g_content_label, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(g_content_label, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(g_content_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_status) { - lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_info_status, "OK Send | DEL Delete | ESC Cancel"); - lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); - } -} + if (rx_bubble_ && rx_bubble_label_) { + lv_obj_clear_flag(rx_bubble_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(rx_bubble_label_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(rx_bubble_, 4, 20); + lv_obj_set_size(rx_bubble_, 250, 44); + lv_obj_set_width(rx_bubble_label_, 234); + if (lora_info_.last_rx[0]) { + lv_label_set_text(rx_bubble_label_, lora_info_.last_rx); + lv_obj_set_style_text_color(rx_bubble_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_label_set_text(rx_bubble_label_, "Waiting for message..."); + lv_obj_set_style_text_color(rx_bubble_label_, lv_color_hex(0xAACCFF), LV_PART_MAIN | LV_STATE_DEFAULT); + } + } -static void lora_render_sent_popup(void) -{ - lora_ui_clear(); - if (g_title_label) { - lv_obj_clear_flag(g_title_label, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(g_title_label, "Received"); - } - if (g_rx_bubble_bg && g_rx_bubble_lbl) { - lv_obj_clear_flag(g_rx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(g_rx_bubble_lbl, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_pos(g_rx_bubble_bg, 4, 22); - lv_obj_set_size(g_rx_bubble_bg, 312, 86); - lv_obj_set_width(g_rx_bubble_lbl, 296); - lv_label_set_text(g_rx_bubble_lbl, safe_text(g_lora_info.last_rx, "")); - lv_obj_set_style_text_color(g_rx_bubble_lbl, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (g_info_status) { - lv_obj_clear_flag(g_info_status, LV_OBJ_FLAG_HIDDEN); - char text[96]; - snprintf(text, sizeof(text), "SNR %.1f RSSI %.0f", g_lora_info.snr, g_lora_info.rssi); - lv_label_set_text(g_info_status, text); - lv_obj_set_style_text_color(g_info_status, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); - } -} + if (tx_bubble_ && tx_bubble_label_) { + if (lora_info_.has_sent_message) { + lv_obj_clear_flag(tx_bubble_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(tx_bubble_label_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(tx_bubble_, 66, 68); + lv_obj_set_size(tx_bubble_, 250, 44); + lv_obj_set_width(tx_bubble_label_, 234); + lv_label_set_text(tx_bubble_label_, lora_app_detail::safe_text(lora_info_.last_tx)); + } else { + lv_obj_add_flag(tx_bubble_, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(tx_bubble_label_, LV_OBJ_FLAG_HIDDEN); + } + } -static void lora_render_current_view(void) -{ - if (g_lora_sent_popup_until_ms != 0 && lv_tick_elaps(g_lora_sent_popup_until_ms) < 2000) { - lora_render_sent_popup(); - return; - } - g_lora_sent_popup_until_ms = 0; - if (g_lora_view != LORA_VIEW_INFO && g_lora_view != LORA_VIEW_SEND) { - g_lora_view = LORA_VIEW_MESSAGES; + if (info_status_label_) { + lv_obj_clear_flag(info_status_label_, LV_OBJ_FLAG_HIDDEN); + char text[128]; + std::snprintf(text, sizeof(text), "RSSI: %.1fdBm | SNR: %.1fdB", lora_info_.rssi, lora_info_.snr); + lv_label_set_text(info_status_label_, text); + lv_obj_set_style_text_color(info_status_label_, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_hint_label_) { + lv_obj_clear_flag(info_hint_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_hint_label_, "Type to send | C/Right: Info | ESC: Back"); + lv_obj_set_style_text_color(info_hint_label_, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); + } } - if (g_lora_view == LORA_VIEW_INFO) lora_render_info_view(); - else if (g_lora_view == LORA_VIEW_SEND) lora_render_send_view(); - else lora_render_messages_view(); -} -static void lora_open_send_view(uint32_t first_key) -{ - g_lora_view = LORA_VIEW_SEND; - g_lora_sent_popup_until_ms = 0; - g_lora_tx_input[0] = '\0'; - char ch = lora_key_to_char(first_key); - if (ch != '\0') { - g_lora_tx_input[0] = ch; - g_lora_tx_input[1] = '\0'; + void render_info_view() + { + clear_view(); + if (title_label_) { + lv_obj_clear_flag(title_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(title_label_, "LoRa Info"); + } + if (info_pins_label_) { + lv_obj_clear_flag(info_pins_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_pins_label_, "Role: Client"); + lv_obj_set_style_text_color(info_pins_label_, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_device_label_) { + lv_obj_clear_flag(info_device_label_, LV_OBJ_FLAG_HIDDEN); + char text[192]; + std::snprintf(text, sizeof(text), "Device: %s | RX:%s TX:%s", + lora_app_detail::safe_text(lora_info_.spi_device, "n/a"), + lora_info_.hw_ready ? "ready" : "off", + lora_info_.tx_in_progress ? "busy" : "idle"); + lv_label_set_text(info_device_label_, text); + lv_obj_set_style_text_color(info_device_label_, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_mode_label_) { + lv_obj_clear_flag(info_mode_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_mode_label_, lora_app_detail::safe_text(lora_info_.probe_display, "Channel: 868MHz | BW:125kHz SF12")); + lv_obj_set_style_text_color(info_mode_label_, lv_color_hex(0xB8FF9C), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_status_label_) { + lv_obj_clear_flag(info_status_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_status_label_, lora_app_detail::safe_text(lora_info_.diag, "LoRa ready")); + lv_obj_set_style_text_color(info_status_label_, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_hint_label_) { + lv_obj_clear_flag(info_hint_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_hint_label_, "Z/Left: Messages | Type: Send | ESC: Back"); + lv_obj_set_style_text_color(info_hint_label_, lv_color_hex(0x8AA8FF), LV_PART_MAIN | LV_STATE_DEFAULT); + } } - lora_render_send_view(); -} - -static bool is_lora_text_key(uint32_t key) -{ - return (key >= 'A' && key <= 'Z') || - (key >= 'a' && key <= 'z') || - (key >= '0' && key <= '9') || - key == ' ' || key == '-' || key == '_' || key == '.' || key == ',' || - key == '!' || key == '?' || key == '#'; -} -static char lora_key_to_char(uint32_t key) -{ - if (key >= 'A' && key <= 'Z') return static_cast(key); - if (key >= 'a' && key <= 'z') return static_cast(key); - if ((key >= '0' && key <= '9') || key == ' ' || key == '-' || key == '_' || - key == '.' || key == ',' || key == '!' || key == '?' || key == '#') { - return static_cast(key); + void render_send_view() + { + clear_view(); + if (title_label_) { + lv_obj_clear_flag(title_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(title_label_, "Send"); + } + if (content_label_) { + lv_obj_clear_flag(content_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(content_label_, tx_input_[0] ? tx_input_ : "_"); + lv_obj_set_style_text_font(content_label_, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(content_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(content_label_, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_status_label_) { + lv_obj_clear_flag(info_status_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(info_status_label_, send_status_[0] ? send_status_ : "OK Send | DEL Delete | ESC Cancel"); + lv_obj_set_style_text_color(info_status_label_, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); + } } - return '\0'; -} -static bool is_menu_prev_key(uint32_t key) -{ - return key == LV_KEY_LEFT || key == LV_KEY_PREV || key == 'z' || key == 'Z'; -} + void render_received_popup() + { + clear_view(); + if (title_label_) { + lv_obj_clear_flag(title_label_, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(title_label_, "Received"); + } + if (rx_bubble_ && rx_bubble_label_) { + lv_obj_clear_flag(rx_bubble_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(rx_bubble_label_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(rx_bubble_, 4, 22); + lv_obj_set_size(rx_bubble_, 312, 86); + lv_obj_set_width(rx_bubble_label_, 296); + lv_label_set_text(rx_bubble_label_, lora_app_detail::safe_text(lora_info_.last_rx, "")); + lv_obj_set_style_text_color(rx_bubble_label_, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (info_status_label_) { + lv_obj_clear_flag(info_status_label_, LV_OBJ_FLAG_HIDDEN); + char text[96]; + std::snprintf(text, sizeof(text), "SNR %.1f RSSI %.0f", lora_info_.snr, lora_info_.rssi); + lv_label_set_text(info_status_label_, text); + lv_obj_set_style_text_color(info_status_label_, lv_color_hex(0xFFD24A), LV_PART_MAIN | LV_STATE_DEFAULT); + } + } -static bool is_menu_next_key(uint32_t key) -{ - return key == LV_KEY_RIGHT || key == LV_KEY_NEXT || key == 'c' || key == 'C'; -} + void render_current_view() + { + if (rx_popup_started_ms_ != 0 && lv_tick_elaps(rx_popup_started_ms_) < lora_app_detail::kRxPopupDurationMs) { + render_received_popup(); + return; + } -static bool handle_app_key(uint32_t key) -{ - bool key_was_z = (key == 'z' || key == 'Z'); - bool key_was_c = (key == 'c' || key == 'C'); + rx_popup_started_ms_ = 0; + if (current_view_ == View::Info) { + render_info_view(); + } else if (current_view_ == View::Send) { + render_send_view(); + } else { + current_view_ = View::Messages; + render_messages_view(); + } + } - if (key_was_z) key = LV_KEY_LEFT; - else if (key_was_c) key = LV_KEY_RIGHT; + void open_send_view(uint32_t first_key) + { + current_view_ = View::Send; + rx_popup_started_ms_ = 0; + send_status_[0] = '\0'; + tx_input_[0] = '\0'; + + char ch = lora_app_detail::key_to_ascii(first_key); + if (ch != '\0') { + tx_input_[0] = ch; + tx_input_[1] = '\0'; + } + render_send_view(); + } - if (g_lora_view == LORA_VIEW_SEND) { + bool handle_send_key(uint32_t key) + { if (key == LV_KEY_ESC) { - g_lora_view = LORA_VIEW_MESSAGES; - g_lora_tx_input[0] = '\0'; - lora_render_current_view(); + current_view_ = View::Messages; + tx_input_[0] = '\0'; + send_status_[0] = '\0'; + show_pending_popup_if_needed(); + render_current_view(); return true; } + if (key == LV_KEY_BACKSPACE || key == LV_KEY_DEL) { - size_t len = strlen(g_lora_tx_input); - if (len > 0) g_lora_tx_input[len - 1] = '\0'; - lora_render_send_view(); + size_t len = std::strlen(tx_input_); + if (len > 0) tx_input_[len - 1] = '\0'; + send_status_[0] = '\0'; + render_send_view(); return true; } + if (key == LV_KEY_ENTER) { - if (lora_api_call({"SendText", g_lora_tx_input}) == 0) { - refresh_lora_info(false); - g_lora_view = LORA_VIEW_MESSAGES; - g_lora_sent_popup_until_ms = 0; - lora_render_current_view(); - g_lora_tx_input[0] = '\0'; - } + send_current_text(); return true; } - if (is_lora_text_key(key)) { - size_t len = strlen(g_lora_tx_input); - if (len + 1 < sizeof(g_lora_tx_input)) { - g_lora_tx_input[len] = lora_key_to_char(key); - g_lora_tx_input[len + 1] = '\0'; - } - lora_render_send_view(); + + if (lora_app_detail::is_printable_ascii(key)) { + append_text_key(key); + render_send_view(); return true; } - return true; - } - if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE || key == LV_KEY_DEL) { - if (g_go_back_home_fn) g_go_back_home_fn(); return true; } - if (is_menu_prev_key(key) || key == LV_KEY_UP) { - g_lora_view = LORA_VIEW_MESSAGES; - g_lora_sent_popup_until_ms = 0; - lora_render_current_view(); - return true; - } else if (is_menu_next_key(key) || key == LV_KEY_DOWN) { - g_lora_view = LORA_VIEW_INFO; - g_lora_sent_popup_until_ms = 0; - lora_render_current_view(); - return true; - } else if (key == LV_KEY_ENTER) { - lora_render_current_view(); - return true; - } else if (is_lora_text_key(key) && !key_was_z && !key_was_c) { - lora_open_send_view(key); - return true; + bool handle_navigation_key(uint32_t key) + { + if (lora_app_detail::is_menu_prev_key(key) || key == LV_KEY_UP) { + current_view_ = View::Messages; + rx_popup_started_ms_ = 0; + render_current_view(); + return true; + } + if (lora_app_detail::is_menu_next_key(key) || key == LV_KEY_DOWN) { + current_view_ = View::Info; + rx_popup_started_ms_ = 0; + render_current_view(); + return true; + } + if (key == LV_KEY_ENTER) { + render_current_view(); + return true; + } + if (lora_app_detail::is_printable_ascii(key) && key != 'z' && key != 'Z' && key != 'c' && key != 'C') { + open_send_view(key); + return true; + } + return false; } - return false; -} + bool handle_key(uint32_t key) + { + if (current_view_ == View::Send) { + return handle_send_key(key); + } -static void lora_key_event_cb(lv_event_t *e) -{ - if (lv_event_get_code(e) != static_cast(LV_EVENT_KEYBOARD)) return; - auto *elm = static_cast(lv_event_get_param(e)); - if (!elm || elm->key_state == 0) return; + if (key == LV_KEY_ESC || key == LV_KEY_BACKSPACE || key == LV_KEY_DEL) { + if (navigate_home) navigate_home(); + return true; + } - uint32_t key = elm->key_code; - uint32_t cp = elm->codepoint; + return handle_navigation_key(key); + } - if ((cp >= 'a' && cp <= 'z') || (cp >= 'A' && cp <= 'Z') || - (cp >= '0' && cp <= '9') || cp == ' ' || cp == '-' || cp == '_' || - cp == '.' || cp == ',' || cp == '!' || cp == '?' || cp == '#') { - key = cp; + void append_text_key(uint32_t key) + { + size_t len = std::strlen(tx_input_); + if (len + 1 < sizeof(tx_input_)) { + tx_input_[len] = lora_app_detail::key_to_ascii(key); + tx_input_[len + 1] = '\0'; + } + send_status_[0] = '\0'; } - if (key == KEY_UP) key = LV_KEY_UP; - else if (key == KEY_DOWN) key = LV_KEY_DOWN; - else if (key == KEY_LEFT) key = LV_KEY_LEFT; - else if (key == KEY_RIGHT) key = LV_KEY_RIGHT; - else if (key == KEY_ENTER || key == KEY_KPENTER) key = LV_KEY_ENTER; - else if (key == KEY_ESC) key = LV_KEY_ESC; - else if (key == KEY_BACKSPACE) key = LV_KEY_BACKSPACE; - else if (key == KEY_DELETE) key = LV_KEY_DEL; + void send_current_text() + { + if (!tx_input_[0]) { + std::snprintf(send_status_, sizeof(send_status_), "Message is empty"); + render_send_view(); + return; + } - (void)handle_app_key(key); -} + if (lora_app_detail::call_lora_api({"SendText", tx_input_}) == 0) { + refresh_lora_info(false); + current_view_ = View::Messages; + rx_popup_started_ms_ = pending_rx_popup_ ? lv_tick_get() : 0; + pending_rx_popup_ = false; + render_current_view(); + tx_input_[0] = '\0'; + send_status_[0] = '\0'; + } else { + std::snprintf(send_status_, sizeof(send_status_), "Send failed"); + render_send_view(); + } + } -static void lora_timer_cb(lv_timer_t *timer) -{ - (void)timer; - if (!g_app_active) return; - refresh_lora_info(true); - if (g_lora_info.rx_event) { - g_lora_view = LORA_VIEW_MESSAGES; - g_lora_sent_popup_until_ms = lv_tick_get(); + void show_pending_popup_if_needed() + { + if (!pending_rx_popup_) return; + pending_rx_popup_ = false; + rx_popup_started_ms_ = lv_tick_get(); } - lora_render_current_view(); -} -void ui_app_lora_create(lv_obj_t* parent, lv_obj_t* root) -{ - if (!parent || !root) return; + uint32_t normalize_key(const key_item *key_event) const + { + uint32_t key = key_event->key_code; + uint32_t codepoint = key_event->codepoint; + + if (lora_app_detail::is_printable_ascii(codepoint)) { + key = codepoint; + } - if (g_lora_timer) { - lv_timer_delete(g_lora_timer); - g_lora_timer = nullptr; + if (key == KEY_UP) return LV_KEY_UP; + if (key == KEY_DOWN) return LV_KEY_DOWN; + if (key == KEY_LEFT) return LV_KEY_LEFT; + if (key == KEY_RIGHT) return LV_KEY_RIGHT; + if (key == KEY_ENTER || key == KEY_KPENTER) return LV_KEY_ENTER; + if (key == KEY_ESC) return LV_KEY_ESC; + if (key == KEY_BACKSPACE) return LV_KEY_BACKSPACE; + if (key == KEY_DELETE) return LV_KEY_DEL; + return key; } - g_ui_parent = parent; - g_ui_root = root; - g_app_active = true; - g_lora_view = LORA_VIEW_MESSAGES; - g_lora_sent_popup_until_ms = 0; - - g_title_label = lora_make_label(parent, "LoRa-1262", 0, 0, 320, 18, - &lv_font_montserrat_14, lv_color_hex(0x8D44FF), LV_TEXT_ALIGN_CENTER); - g_content_label = lora_make_label(parent, "", 8, 22, 304, 90, - &lv_font_montserrat_12, lv_color_hex(0xFFFFFF), LV_TEXT_ALIGN_LEFT); - lv_label_set_long_mode(g_content_label, LV_LABEL_LONG_WRAP); - g_info_pins = lora_make_label(parent, "", 8, 22, 304, 18, - &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); - g_info_device = lora_make_label(parent, "", 8, 42, 304, 18, - &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); - g_info_mode = lora_make_label(parent, "", 8, 62, 304, 18, - &lv_font_montserrat_12, lv_color_hex(0xB8FF9C), LV_TEXT_ALIGN_LEFT); - g_info_status = lora_make_label(parent, "", 8, 114, 304, 16, - &lv_font_montserrat_12, lv_color_hex(0xFFD24A), LV_TEXT_ALIGN_LEFT); - g_info_hint = lora_make_label(parent, "", 8, 132, 304, 14, - &lv_font_montserrat_10, lv_color_hex(0x8AA8FF), LV_TEXT_ALIGN_LEFT); - - g_rx_bubble_bg = lora_make_bubble(parent, 4, 20, 250, 44, lv_color_hex(0x3A7DFF)); - g_rx_bubble_lbl = lora_make_bubble_label(g_rx_bubble_bg, 234); - lv_obj_add_flag(g_rx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - g_tx_bubble_bg = lora_make_bubble(parent, 66, 68, 250, 44, lv_color_hex(0x00A854)); - g_tx_bubble_lbl = lora_make_bubble_label(g_tx_bubble_bg, 234); - lv_obj_add_flag(g_tx_bubble_bg, LV_OBJ_FLAG_HIDDEN); - - lv_obj_add_event_cb(root, lora_key_event_cb, static_cast(LV_EVENT_KEYBOARD), nullptr); - - (void)lora_api_call({"Init"}); - refresh_lora_info(false); - lora_render_page(); - g_lora_timer = lv_timer_create(lora_timer_cb, 100, nullptr); -} + void on_key_event(lv_event_t *event) + { + auto *key_event = static_cast(lv_event_get_param(event)); + if (!key_event || key_event->key_state == KBD_KEY_RELEASED) return; + (void)handle_key(normalize_key(key_event)); + } -void lora_app_task() -{ - refresh_lora_info(true); -} + void on_poll_timer() + { + if (!app_active_) return; -void ui_app_lora_destroy(void) -{ - if (g_lora_timer) { - lv_timer_delete(g_lora_timer); - g_lora_timer = nullptr; - } - g_app_active = false; - g_ui_parent = nullptr; - g_ui_root = nullptr; - g_title_label = nullptr; - g_content_label = nullptr; - g_info_pins = nullptr; - g_info_device = nullptr; - g_info_mode = nullptr; - g_info_status = nullptr; - g_info_hint = nullptr; - g_rx_bubble_bg = nullptr; - g_rx_bubble_lbl = nullptr; - g_tx_bubble_bg = nullptr; - g_tx_bubble_lbl = nullptr; -} + refresh_lora_info(true); + if (!lora_info_.hw_ready) { + render_boot_diag_view(); + return; + } -} // namespace Lora_APP + if (lora_info_.rx_event) { + if (current_view_ == View::Send) { + pending_rx_popup_ = true; + } else { + current_view_ = View::Messages; + rx_popup_started_ms_ = lv_tick_get(); + } + } + render_current_view(); + } -class UILoraPage : public AppPage -{ -public: - UILoraPage() : AppPage() + static void static_key_event_cb(lv_event_t *event) { - Lora_APP::ui_app_lora_set_go_back([this]() { - if (navigate_home) navigate_home(); - }); - Lora_APP::ui_app_lora_create(ui_APP_Container, root_screen_); + if (lv_event_get_code(event) != static_cast(LV_EVENT_KEYBOARD)) return; + auto *self = static_cast(lv_event_get_user_data(event)); + if (self) self->on_key_event(event); } - ~UILoraPage() + static void static_poll_timer_cb(lv_timer_t *timer) { - Lora_APP::ui_app_lora_set_go_back(nullptr); - Lora_APP::ui_app_lora_destroy(); + auto *self = static_cast(lv_timer_get_user_data(timer)); + if (self) self->on_poll_timer(); } }; From e5adf65c99246b246be68574878de3e4ae25133c Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 16 Jun 2026 16:04:30 +0800 Subject: [PATCH 67/70] Replace console app with ST terminal --- projects/APPLaunch/main/SConstruct | 8 +- projects/APPLaunch/main/ui/launch.cpp | 8 +- projects/APPLaunch/main/ui/lvgl/ftmodule.h | 33 + projects/APPLaunch/main/ui/lvgl/lv_freetype.c | 498 +++++ projects/APPLaunch/main/ui/lvgl/lv_freetype.h | 150 ++ .../main/ui/lvgl/lv_freetype_glyph.c | 255 +++ .../main/ui/lvgl/lv_freetype_image.c | 246 +++ .../APPLaunch/main/ui/lvgl/lv_sdl_keyboard.c | 0 .../main/ui/page_app/ui_app_console.hpp | 1280 ------------- .../APPLaunch/main/ui/page_app/ui_app_ssh.hpp | 12 +- .../APPLaunch/main/ui/page_app/ui_app_st.hpp | 1703 +++++++++++++++++ projects/APPLaunch/main/ui/ui.h | 6 +- projects/APPLaunch/main/ui/ui_launch_page.cpp | 21 +- 13 files changed, 2923 insertions(+), 1297 deletions(-) create mode 100644 projects/APPLaunch/main/ui/lvgl/ftmodule.h create mode 100644 projects/APPLaunch/main/ui/lvgl/lv_freetype.c create mode 100644 projects/APPLaunch/main/ui/lvgl/lv_freetype.h create mode 100644 projects/APPLaunch/main/ui/lvgl/lv_freetype_glyph.c create mode 100644 projects/APPLaunch/main/ui/lvgl/lv_freetype_image.c create mode 100644 projects/APPLaunch/main/ui/lvgl/lv_sdl_keyboard.c delete mode 100644 projects/APPLaunch/main/ui/page_app/ui_app_console.hpp create mode 100644 projects/APPLaunch/main/ui/page_app/ui_app_st.hpp diff --git a/projects/APPLaunch/main/SConstruct b/projects/APPLaunch/main/SConstruct index da31544d..6cb8d087 100644 --- a/projects/APPLaunch/main/SConstruct +++ b/projects/APPLaunch/main/SConstruct @@ -91,10 +91,16 @@ if 'CONFIG_BACKWARD_CPP_ENABLED' in os.environ: # custom lvgl component lvgl_component = list(filter(lambda x: x['target'] == 'lvgl_component', env['COMPONENTS']))[0] +local_lvgl_override_dir = Path('ui/lvgl') +local_lvgl_override_names = set( + path.name for path in local_lvgl_override_dir.iterdir() + if path.is_file() +) if local_lvgl_override_dir.exists() else set() lvgl_component['SRCS'] = list(filter( - lambda src: 'lv_sdl_keyboard.c' not in str(src), + lambda src: Path(str(src)).name not in local_lvgl_override_names, lvgl_component['SRCS'] )) +INCLUDE += [ADir('../../../SDK/github_source/lvgl/lvgl_9_5/lvgl/src/libs/freetype')] if 'CONFIG_APPLAUNCH_CROSS' in os.environ: diff --git a/projects/APPLaunch/main/ui/launch.cpp b/projects/APPLaunch/main/ui/launch.cpp index a6f76f10..12abd66e 100644 --- a/projects/APPLaunch/main/ui/launch.cpp +++ b/projects/APPLaunch/main/ui/launch.cpp @@ -65,7 +65,7 @@ constexpr BuiltinAppRegistration kBuiltinApps[] = { {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, {{"STORE", "store_100.png", "app_Store", false, true}, "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, - {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, {{"MATH", "math_100.png", "app_Math", true, false}, @@ -178,16 +178,16 @@ void Launch::launch_Exec_in_terminal(const std::string &exec, bool sysplause) { SLOGI("Launching terminal app: %s", exec.c_str()); /* Instant visual feedback; paint before the (potentially slow) - * Console page construction so the user sees it right away. */ + * ST page construction so the user sees it right away. */ ui_loading::show("Loading..."); lv_refr_now(NULL); - auto p = std::make_shared(); + auto p = std::make_shared(); app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); p->navigate_home = std::bind(&Launch::go_back_home, this); p->terminal_sysplause = sysplause; - /* Console page fully covers APP_Container; safe to hide now. + /* ST page fully covers APP_Container; safe to hide now. * The heavy exec() call below will still run while the terminal * page is on-screen — no overlay needed at that point. */ ui_loading::hide(); diff --git a/projects/APPLaunch/main/ui/lvgl/ftmodule.h b/projects/APPLaunch/main/ui/lvgl/ftmodule.h new file mode 100644 index 00000000..e3878f77 --- /dev/null +++ b/projects/APPLaunch/main/ui/lvgl/ftmodule.h @@ -0,0 +1,33 @@ +/* + * This file registers the FreeType modules compiled into the library. + * + * If you use GNU make, this file IS NOT USED! Instead, it is created in + * the objects directory (normally `/objs/`) based on information + * from `/modules.cfg`. + * + * Please read `docs/INSTALL.ANY` and `docs/CUSTOMIZE` how to compile + * FreeType without GNU make. + * + */ + +/* FT_USE_MODULE( FT_Module_Class, autofit_module_class ) */ +FT_USE_MODULE(FT_Driver_ClassRec, tt_driver_class) +/* FT_USE_MODULE( FT_Driver_ClassRec, t1_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, cff_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, t1cid_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, pfr_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, t42_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, winfnt_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, pcf_driver_class ) */ +/* FT_USE_MODULE( FT_Driver_ClassRec, bdf_driver_class ) */ +/* FT_USE_MODULE( FT_Module_Class, psaux_module_class ) */ +/* FT_USE_MODULE( FT_Module_Class, psnames_module_class ) */ +/* FT_USE_MODULE( FT_Module_Class, pshinter_module_class ) */ +FT_USE_MODULE(FT_Module_Class, sfnt_module_class) +FT_USE_MODULE(FT_Renderer_Class, ft_smooth_renderer_class) +FT_USE_MODULE( FT_Renderer_Class, ft_raster1_renderer_class ) +/* FT_USE_MODULE( FT_Renderer_Class, ft_sdf_renderer_class ) */ +/* FT_USE_MODULE( FT_Renderer_Class, ft_bitmap_sdf_renderer_class ) */ +/* FT_USE_MODULE( FT_Renderer_Class, ft_svg_renderer_class ) */ + +/* EOF */ diff --git a/projects/APPLaunch/main/ui/lvgl/lv_freetype.c b/projects/APPLaunch/main/ui/lvgl/lv_freetype.c new file mode 100644 index 00000000..b991dfb4 --- /dev/null +++ b/projects/APPLaunch/main/ui/lvgl/lv_freetype.c @@ -0,0 +1,498 @@ +/** + * @file lv_freetype.c + * + */ + +/********************* + * INCLUDES + *********************/ +#include "lv_freetype_private.h" + +#if LV_USE_FREETYPE + +#include "../../misc/lv_fs_private.h" +#include "../../core/lv_global.h" + +/********************* + * DEFINES + *********************/ + +#define ft_ctx LV_GLOBAL_DEFAULT()->ft_context +#define LV_FREETYPE_OUTLINE_REF_SIZE_DEF 128 + +/**< This value is from the FreeType's function `FT_GlyphSlot_Oblique` in `ftsynth.c` */ +#define LV_FREETYPE_OBLIQUE_SLANT_DEF 0x0366A + +#if LV_FREETYPE_CACHE_FT_GLYPH_CNT <= 0 + #error "LV_FREETYPE_CACHE_FT_GLYPH_CNT must be greater than 0" +#endif + +/********************** + * TYPEDEFS + **********************/ + +/* Use the pointer storing pathname as the unique request ID of the face */ +typedef struct { + char * pathname; + int ref_cnt; +} face_id_node_t; + +/********************** + * STATIC PROTOTYPES + **********************/ +static void lv_freetype_cleanup(lv_freetype_context_t * ctx); +static FTC_FaceID lv_freetype_req_face_id(lv_freetype_context_t * ctx, const char * pathname); +static void lv_freetype_drop_face_id(lv_freetype_context_t * ctx, FTC_FaceID face_id); +static bool freetype_on_font_create(lv_freetype_font_dsc_t * dsc, uint32_t max_glyph_cnt); +static void freetype_on_font_set_cbs(lv_freetype_font_dsc_t * dsc); + +static bool cache_node_cache_create_cb(lv_freetype_cache_node_t * node, void * user_data); +static void cache_node_cache_free_cb(lv_freetype_cache_node_t * node, void * user_data); +static lv_cache_compare_res_t cache_node_cache_compare_cb(const lv_freetype_cache_node_t * lhs, + const lv_freetype_cache_node_t * rhs); + +static lv_font_t * freetype_font_create_cb(const lv_font_info_t * info, const void * src); +static void freetype_font_delete_cb(lv_font_t * font); +static void * freetype_font_dup_src_cb(const void * src); +static void freetype_font_free_src_cb(void * src); + +/********************** + * STATIC VARIABLES + **********************/ + +const lv_font_class_t lv_freetype_font_class = { + .create_cb = freetype_font_create_cb, + .delete_cb = freetype_font_delete_cb, + .dup_src_cb = freetype_font_dup_src_cb, + .free_src_cb = freetype_font_free_src_cb, +}; + +/********************** + * MACROS + **********************/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +lv_result_t lv_freetype_init(uint32_t max_glyph_cnt) +{ + if(ft_ctx) { + LV_LOG_WARN("freetype already initialized"); + return LV_RESULT_INVALID; + } + + ft_ctx = lv_malloc_zeroed(sizeof(lv_freetype_context_t)); + LV_ASSERT_MALLOC(ft_ctx); + if(!ft_ctx) { + LV_LOG_ERROR("malloc failed for lv_freetype_context_t"); + return LV_RESULT_INVALID; + } + + lv_freetype_context_t * ctx = lv_freetype_get_context(); + + ctx->max_glyph_cnt = max_glyph_cnt; + + FT_Error error; + + error = FT_Init_FreeType(&ctx->library); + if(error) { + FT_ERROR_MSG("FT_Init_FreeType", error); + return LV_RESULT_INVALID; + } + + lv_ll_init(&ctx->face_id_ll, sizeof(face_id_node_t)); + + lv_cache_ops_t ops = { + .compare_cb = (lv_cache_compare_cb_t)cache_node_cache_compare_cb, + .create_cb = (lv_cache_create_cb_t)cache_node_cache_create_cb, + .free_cb = (lv_cache_free_cb_t)cache_node_cache_free_cb, + }; + ctx->cache_node_cache = lv_cache_create(&lv_cache_class_lru_rb_count, sizeof(lv_freetype_cache_node_t), INT32_MAX, ops); + lv_cache_set_name(ctx->cache_node_cache, "FREETYPE_CACHE_NODE"); + + return LV_RESULT_OK; +} + +void lv_freetype_uninit(void) +{ + lv_freetype_context_t * ctx = lv_freetype_get_context(); + if(!ctx) { + return; + } + + lv_freetype_cleanup(ctx); + + lv_free(ft_ctx); + ft_ctx = NULL; +} + +void lv_freetype_init_font_info(lv_font_info_t * font_info) +{ + LV_ASSERT_NULL(font_info); + lv_memzero(font_info, sizeof(lv_font_info_t)); + font_info->class_p = &lv_freetype_font_class; + font_info->render_mode = LV_FREETYPE_FONT_RENDER_MODE_BITMAP; + font_info->style = LV_FREETYPE_FONT_STYLE_NORMAL; + font_info->kerning = LV_FONT_KERNING_NONE; +} + +lv_font_t * lv_freetype_font_create_with_info(const lv_font_info_t * font_info) +{ + LV_ASSERT_NULL(font_info); + if(font_info->size == 0) { + LV_LOG_ERROR("font size can't be zero"); + return NULL; + } + + const char * pathname = font_info->name; + + size_t pathname_len = pathname ? lv_strlen(pathname) : 0; + if(pathname_len == 0) { + LV_LOG_ERROR("font pathname can't be empty"); + return NULL; + } + + lv_freetype_context_t * ctx = lv_freetype_get_context(); + + lv_freetype_cache_node_t search_key = { + .pathname = lv_freetype_req_face_id(ctx, pathname), + .style = font_info->style, + .render_mode = font_info->render_mode, + }; + + bool cache_hitting = true; + lv_cache_entry_t * cache_node_entry = lv_cache_acquire(ctx->cache_node_cache, &search_key, NULL); + if(cache_node_entry == NULL) { + cache_hitting = false; + cache_node_entry = lv_cache_acquire_or_create(ctx->cache_node_cache, &search_key, NULL); + if(cache_node_entry == NULL) { + lv_freetype_drop_face_id(ctx, (FTC_FaceID)search_key.pathname); + LV_LOG_ERROR("cache node creating failed"); + return NULL; + } + } + + lv_freetype_font_dsc_t * dsc = lv_malloc_zeroed(sizeof(lv_freetype_font_dsc_t)); + LV_ASSERT_MALLOC(dsc); + + dsc->face_id = (FTC_FaceID)search_key.pathname; + dsc->render_mode = font_info->render_mode; + dsc->context = ctx; + dsc->size = font_info->size; + dsc->style = font_info->style; + dsc->kerning = font_info->kerning; + dsc->magic_num = LV_FREETYPE_FONT_DSC_MAGIC_NUM; + dsc->cache_node = lv_cache_entry_get_data(cache_node_entry); + dsc->cache_node_entry = cache_node_entry; + + if(cache_hitting == false && freetype_on_font_create(dsc, ctx->max_glyph_cnt) == false) { + lv_cache_release(ctx->cache_node_cache, dsc->cache_node_entry, NULL); + lv_freetype_drop_face_id(ctx, dsc->face_id); + lv_free(dsc); + return NULL; + } + freetype_on_font_set_cbs(dsc); + + FT_Face face = dsc->cache_node->face; + FT_Error error; + if(FT_IS_SCALABLE(face)) { + error = FT_Set_Pixel_Sizes(face, 0, font_info->size); + } + else { + LV_LOG_WARN("font is not scalable, selecting available size"); + error = FT_Select_Size(face, 0); + } + if(error) { + FT_ERROR_MSG("FT_Set_Pixel_Sizes", error); + return NULL; + } + + if(dsc->kerning != LV_FONT_KERNING_NONE && !dsc->cache_node->face_has_kerning) { + LV_LOG_WARN("font: '%s' doesn't have kerning info", pathname); + } + + lv_font_t * font = &dsc->font; + font->dsc = dsc; + font->subpx = LV_FONT_SUBPX_NONE; + font->line_height = FT_F26DOT6_TO_INT(face->size->metrics.height); + font->base_line = -FT_F26DOT6_TO_INT(face->size->metrics.descender); + + FT_Fixed scale = face->size->metrics.y_scale; + int8_t thickness = FT_F26DOT6_TO_INT(FT_MulFix(scale, face->underline_thickness)); + font->underline_position = FT_F26DOT6_TO_INT(FT_MulFix(scale, face->underline_position)); + font->underline_thickness = thickness < 1 ? 1 : thickness; + + return font; +} + +lv_font_t * lv_freetype_font_create(const char * pathname, lv_freetype_font_render_mode_t render_mode, uint32_t size, + lv_freetype_font_style_t style) +{ + lv_font_info_t font_info; + lv_freetype_init_font_info(&font_info); + font_info.name = pathname; + font_info.size = size; + font_info.render_mode = render_mode; + font_info.style = style; + return lv_freetype_font_create_with_info(&font_info); +} + +void lv_freetype_font_delete(lv_font_t * font) +{ + LV_ASSERT_NULL(font); + lv_freetype_context_t * ctx = lv_freetype_get_context(); + if(!ctx) { + /* Freetype already torn down (e.g. static destruction order). Nothing to release. */ + return; + } + lv_freetype_font_dsc_t * dsc = (lv_freetype_font_dsc_t *)(font->dsc); + LV_ASSERT_FREETYPE_FONT_DSC(dsc); + + lv_cache_release(ctx->cache_node_cache, dsc->cache_node_entry, NULL); + if(lv_cache_entry_get_ref(dsc->cache_node_entry) == 0) { + lv_cache_drop(ctx->cache_node_cache, dsc->cache_node, NULL); + } + + lv_freetype_drop_face_id(dsc->context, dsc->face_id); + + /* invalidate magic number */ + lv_memzero(dsc, sizeof(lv_freetype_font_dsc_t)); + lv_free(dsc); +} + +lv_freetype_context_t * lv_freetype_get_context(void) +{ + return LV_GLOBAL_DEFAULT()->ft_context; +} + +void lv_freetype_italic_transform(FT_Face face) +{ + LV_ASSERT_NULL(face); + FT_Matrix matrix; + matrix.xx = FT_INT_TO_F16DOT16(1); + matrix.xy = LV_FREETYPE_OBLIQUE_SLANT_DEF; + matrix.yx = 0; + matrix.yy = FT_INT_TO_F16DOT16(1); + FT_Set_Transform(face, &matrix, NULL); +} + +int32_t lv_freetype_italic_transform_on_pos(lv_point_t point) +{ + return point.x + FT_F16DOT16_TO_INT(point.y * LV_FREETYPE_OBLIQUE_SLANT_DEF); +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +static bool freetype_on_font_create(lv_freetype_font_dsc_t * dsc, uint32_t max_glyph_cnt) +{ + /* + * Glyph info uses a small amount of memory, and uses glyph info more frequently, + * so it plans to use twice the maximum number of caches here to + * get a better info acquisition performance.*/ + lv_cache_t * glyph_cache = lv_freetype_create_glyph_cache(max_glyph_cnt * 2); + if(glyph_cache == NULL) { + LV_LOG_ERROR("glyph cache creating failed"); + return false; + } + dsc->cache_node->glyph_cache = glyph_cache; + + lv_cache_t * draw_data_cache = NULL; + if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP || + dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO) { + draw_data_cache = lv_freetype_create_draw_data_image(max_glyph_cnt); + } + else if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_OUTLINE) { + draw_data_cache = lv_freetype_create_draw_data_outline(max_glyph_cnt); + } + else { + LV_LOG_ERROR("unknown render mode"); + return false; + } + + if(draw_data_cache == NULL) { + LV_LOG_ERROR("draw data cache creating failed"); + return false; + } + + dsc->cache_node->draw_data_cache = draw_data_cache; + + return true; +} + +static void freetype_on_font_set_cbs(lv_freetype_font_dsc_t * dsc) +{ + lv_freetype_set_cbs_glyph(dsc); + if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP || + dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO) { + lv_freetype_set_cbs_image_font(dsc); + } + else if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_OUTLINE) { + lv_freetype_set_cbs_outline_font(dsc); + } +} + +static void lv_freetype_cleanup(lv_freetype_context_t * ctx) +{ + LV_ASSERT_NULL(ctx); + if(ctx->cache_node_cache) { + lv_cache_destroy(ctx->cache_node_cache, NULL); + ctx->cache_node_cache = NULL; + } + + if(ctx->library) { + FT_Done_FreeType(ctx->library); + ctx->library = NULL; + } +} + +static FTC_FaceID lv_freetype_req_face_id(lv_freetype_context_t * ctx, const char * pathname) +{ + size_t len = lv_strlen(pathname); + LV_ASSERT(len > 0); + + lv_ll_t * ll_p = &ctx->face_id_ll; + face_id_node_t * node; + + /* search cache */ + LV_LL_READ(ll_p, node) { + if(strcmp(node->pathname, pathname) == 0) { + node->ref_cnt++; + LV_LOG_INFO("reuse face_id: %s, ref_cnt = %d", node->pathname, node->ref_cnt); + return node->pathname; + } + } + + /* insert new cache */ + node = lv_ll_ins_tail(ll_p); + LV_ASSERT_MALLOC(node); + +#if LV_USE_FS_MEMFS + if(pathname[0] == LV_FS_MEMFS_LETTER) { +#if !LV_FREETYPE_USE_LVGL_PORT + LV_LOG_WARN("LV_FREETYPE_USE_LVGL_PORT is not enabled"); +#endif + node->pathname = lv_malloc(sizeof(lv_fs_path_ex_t)); + LV_ASSERT_MALLOC(node->pathname); + lv_memcpy(node->pathname, pathname, sizeof(lv_fs_path_ex_t)); + } + else +#endif + { + node->pathname = lv_strdup(pathname); + LV_ASSERT_NULL(node->pathname); + } + + LV_LOG_INFO("add face_id: %s", node->pathname); + + node->ref_cnt = 1; + return node->pathname; +} + +static void lv_freetype_drop_face_id(lv_freetype_context_t * ctx, FTC_FaceID face_id) +{ + lv_ll_t * ll_p = &ctx->face_id_ll; + face_id_node_t * node; + LV_LL_READ(ll_p, node) { + if(face_id == node->pathname) { + LV_LOG_INFO("found face_id: %s, ref_cnt = %d", node->pathname, node->ref_cnt); + node->ref_cnt--; + if(node->ref_cnt == 0) { + LV_LOG_INFO("drop face_id: %s", node->pathname); + lv_ll_remove(ll_p, node); + lv_free(node->pathname); + lv_free(node); + } + return; + } + } + + LV_ASSERT_MSG(false, "face_id not found"); +} + +/*----------------- + * Cache Node Cache Callbacks + *----------------*/ + +static bool cache_node_cache_create_cb(lv_freetype_cache_node_t * node, void * user_data) +{ + LV_UNUSED(user_data); + lv_freetype_context_t * ctx = lv_freetype_get_context(); + + /* Cache miss, load face */ + FT_Face face; + FT_Error error = FT_New_Face(ctx->library, node->pathname, 0, &face); + if(error) { + FT_ERROR_MSG("FT_New_Face", error); + return false; + } + + node->ref_size = LV_FREETYPE_OUTLINE_REF_SIZE_DEF; + + if(node->style & LV_FREETYPE_FONT_STYLE_ITALIC) { + lv_freetype_italic_transform(face); + } + + node->face = face; + node->face_has_kerning = FT_HAS_KERNING(face); + lv_mutex_init(&node->face_lock); + + return true; +} +static void cache_node_cache_free_cb(lv_freetype_cache_node_t * node, void * user_data) +{ + FT_Done_Face(node->face); + lv_mutex_delete(&node->face_lock); + + if(node->glyph_cache) { + lv_cache_destroy(node->glyph_cache, user_data); + node->glyph_cache = NULL; + } + if(node->draw_data_cache) { + lv_cache_destroy(node->draw_data_cache, user_data); + node->draw_data_cache = NULL; + } +} +static lv_cache_compare_res_t cache_node_cache_compare_cb(const lv_freetype_cache_node_t * lhs, + const lv_freetype_cache_node_t * rhs) +{ + if(lhs->render_mode != rhs->render_mode) { + return lhs->render_mode > rhs->render_mode ? 1 : -1; + } + if(lhs->style != rhs->style) { + return lhs->style > rhs->style ? 1 : -1; + } + + int32_t cmp_res = lv_strcmp(lhs->pathname, rhs->pathname); + if(cmp_res != 0) { + return cmp_res > 0 ? 1 : -1; + } + + return 0; +} + +static lv_font_t * freetype_font_create_cb(const lv_font_info_t * info, const void * src) +{ + lv_font_info_t font_info = *info; + font_info.name = src; + return lv_freetype_font_create_with_info(&font_info); +} + +static void freetype_font_delete_cb(lv_font_t * font) +{ + lv_freetype_font_delete(font); +} + +static void * freetype_font_dup_src_cb(const void * src) +{ + return lv_strdup(src); +} + +static void freetype_font_free_src_cb(void * src) +{ + lv_free(src); +} + +#endif /*LV_USE_FREETYPE*/ diff --git a/projects/APPLaunch/main/ui/lvgl/lv_freetype.h b/projects/APPLaunch/main/ui/lvgl/lv_freetype.h new file mode 100644 index 00000000..93c262bb --- /dev/null +++ b/projects/APPLaunch/main/ui/lvgl/lv_freetype.h @@ -0,0 +1,150 @@ +/** + * @file lv_freetype.h + * + */ +#ifndef LV_FREETYPE_H +#define LV_FREETYPE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "../../lv_conf_internal.h" + +#if LV_USE_FREETYPE + +#include "../../misc/lv_types.h" +#include "../../misc/lv_event.h" +#include "../../misc/lv_color.h" + +#include LV_STDBOOL_INCLUDE + +/********************* +* DEFINES +*********************/ + +#define LV_FREETYPE_F26DOT6_TO_INT(x) ((x) >> 6) +#define LV_FREETYPE_F26DOT6_TO_FLOAT(x) ((float)(x) / 64) + +#define FT_FONT_STYLE_NORMAL LV_FREETYPE_FONT_STYLE_NORMAL +#define FT_FONT_STYLE_ITALIC LV_FREETYPE_FONT_STYLE_ITALIC +#define FT_FONT_STYLE_BOLD LV_FREETYPE_FONT_STYLE_BOLD + +/********************** + * TYPEDEFS + **********************/ + +typedef enum { + LV_FREETYPE_FONT_STYLE_NORMAL = 0, + LV_FREETYPE_FONT_STYLE_ITALIC = 1 << 0, + LV_FREETYPE_FONT_STYLE_BOLD = 1 << 1, +} lv_freetype_font_style_t; + +typedef lv_freetype_font_style_t LV_FT_FONT_STYLE; + +typedef enum { + LV_FREETYPE_FONT_RENDER_MODE_BITMAP = 0, + LV_FREETYPE_FONT_RENDER_MODE_OUTLINE = 1, + LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO = 2, +} lv_freetype_font_render_mode_t; + +typedef void * lv_freetype_outline_t; + +typedef enum { + LV_FREETYPE_OUTLINE_END, + LV_FREETYPE_OUTLINE_MOVE_TO, + LV_FREETYPE_OUTLINE_LINE_TO, + LV_FREETYPE_OUTLINE_CUBIC_TO, + LV_FREETYPE_OUTLINE_CONIC_TO, + LV_FREETYPE_OUTLINE_BORDER_START, /* When line width > 0 the border glyph is drawn after the regular glyph */ +} lv_freetype_outline_type_t; + +/* Only path string is required */ +typedef const char lv_freetype_font_src_t; + +LV_ATTRIBUTE_EXTERN_DATA extern const lv_font_class_t lv_freetype_font_class; + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +/** + * Initialize the freetype library. + * @return LV_RESULT_OK on success, otherwise LV_RESULT_INVALID. + */ +lv_result_t lv_freetype_init(uint32_t max_glyph_cnt); + +/** + * Uninitialize the freetype library + */ +void lv_freetype_uninit(void); + +/** + * Initialize a font info structure. + * @param font_info font info structure to be initialized. + */ +void lv_freetype_init_font_info(lv_font_info_t * font_info); + +/** + * Create a freetype font with a font info structure. + * @param font_info font info structure. + * @return Created font, or NULL on failure. + */ +lv_font_t * lv_freetype_font_create_with_info(const lv_font_info_t * font_info); + +/** + * Create a freetype font. + * @param pathname font file path. + * @param render_mode font render mode(see @lv_freetype_font_render_mode_t for details). + * @param size font size. + * @param style font style(see lv_freetype_font_style_t for details). + * @return Created font, or NULL on failure. + */ +lv_font_t * lv_freetype_font_create(const char * pathname, lv_freetype_font_render_mode_t render_mode, uint32_t size, + lv_freetype_font_style_t style); + +/** + * Delete a freetype font. + * @param font freetype font to be deleted. + */ +void lv_freetype_font_delete(lv_font_t * font); + +/** + * Register a callback function to generate outlines for FreeType fonts. + * + * @param cb The callback function to be registered. + * @param user_data User data to be passed to the callback function. + * @return The ID of the registered callback function, or a negative value on failure. + */ +void lv_freetype_outline_add_event(lv_event_cb_t event_cb, lv_event_code_t filter, void * user_data); + +/** + * Get the scale of a FreeType font. + * + * @param font The FreeType font to get the scale of. + * @return The scale of the FreeType font. + */ +uint32_t lv_freetype_outline_get_scale(const lv_font_t * font); + +/** + * Check if the font is an outline font. + * + * @param font The FreeType font. + * @return Is outline font on success, otherwise false. + */ +bool lv_freetype_is_outline_font(const lv_font_t * font); + +/********************** + * MACROS + **********************/ + +#endif /*LV_USE_FREETYPE*/ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* LV_FREETYPE_H */ diff --git a/projects/APPLaunch/main/ui/lvgl/lv_freetype_glyph.c b/projects/APPLaunch/main/ui/lvgl/lv_freetype_glyph.c new file mode 100644 index 00000000..e75a4e0b --- /dev/null +++ b/projects/APPLaunch/main/ui/lvgl/lv_freetype_glyph.c @@ -0,0 +1,255 @@ +/** + * @file lv_freetype_glyph.c + * + */ + +/********************* + * INCLUDES + *********************/ + +#include "../../lvgl.h" +#include "lv_freetype_private.h" + +#if LV_USE_FREETYPE + +/********************* + * DEFINES + *********************/ + +#define CACHE_NAME "FREETYPE_GLYPH" + +/********************** + * TYPEDEFS + **********************/ +typedef struct _lv_freetype_glyph_cache_data_t { + uint32_t unicode; + uint32_t size; + + lv_font_glyph_dsc_t glyph_dsc; +} lv_freetype_glyph_cache_data_t; + +/********************** + * STATIC PROTOTYPES + **********************/ + +static bool freetype_get_glyph_dsc_cb(const lv_font_t * font, lv_font_glyph_dsc_t * g_dsc, uint32_t unicode_letter, + uint32_t unicode_letter_next); + +static bool freetype_glyph_create_cb(lv_freetype_glyph_cache_data_t * data, void * user_data); +static void freetype_glyph_free_cb(lv_freetype_glyph_cache_data_t * data, void * user_data); +static lv_cache_compare_res_t freetype_glyph_compare_cb(const lv_freetype_glyph_cache_data_t * lhs, + const lv_freetype_glyph_cache_data_t * rhs); +/********************** + * STATIC VARIABLES + **********************/ + +/********************** + * MACROS + **********************/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +lv_cache_t * lv_freetype_create_glyph_cache(uint32_t cache_size) +{ + lv_cache_ops_t ops = { + .create_cb = (lv_cache_create_cb_t)freetype_glyph_create_cb, + .free_cb = (lv_cache_free_cb_t)freetype_glyph_free_cb, + .compare_cb = (lv_cache_compare_cb_t)freetype_glyph_compare_cb, + }; + + lv_cache_t * glyph_cache = lv_cache_create(&lv_cache_class_lru_rb_count, sizeof(lv_freetype_glyph_cache_data_t), + cache_size, ops); + lv_cache_set_name(glyph_cache, CACHE_NAME); + + return glyph_cache; +} + +void lv_freetype_set_cbs_glyph(lv_freetype_font_dsc_t * dsc) +{ + LV_ASSERT_FREETYPE_FONT_DSC(dsc); + dsc->font.get_glyph_dsc = freetype_get_glyph_dsc_cb; +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +static bool freetype_get_glyph_dsc_cb(const lv_font_t * font, lv_font_glyph_dsc_t * g_dsc, uint32_t unicode_letter, + uint32_t unicode_letter_next) +{ + LV_ASSERT_NULL(font); + LV_ASSERT_NULL(g_dsc); + LV_PROFILER_FONT_BEGIN; + + if(unicode_letter < 0x20) { + g_dsc->adv_w = 0; + g_dsc->box_h = 0; + g_dsc->box_w = 0; + g_dsc->ofs_x = 0; + g_dsc->ofs_y = 0; + g_dsc->format = LV_FONT_GLYPH_FORMAT_NONE; + LV_PROFILER_FONT_END; + return true; + } + + lv_freetype_font_dsc_t * dsc = (lv_freetype_font_dsc_t *)font->dsc; + LV_ASSERT_FREETYPE_FONT_DSC(dsc); + + lv_freetype_glyph_cache_data_t search_key = { + .unicode = unicode_letter, + .size = dsc->size, + }; + + lv_cache_t * glyph_cache = dsc->cache_node->glyph_cache; + + lv_cache_entry_t * entry = lv_cache_acquire_or_create(glyph_cache, &search_key, dsc); + if(entry == NULL) { + LV_LOG_ERROR("glyph lookup failed for unicode = 0x%" LV_PRIx32, unicode_letter); + LV_PROFILER_FONT_END; + return false; + } + lv_freetype_glyph_cache_data_t * data = lv_cache_entry_get_data(entry); + *g_dsc = data->glyph_dsc; + + if((dsc->style & LV_FREETYPE_FONT_STYLE_ITALIC) && (unicode_letter_next == '\0')) { + g_dsc->adv_w = g_dsc->box_w + g_dsc->ofs_x; + } + + if(dsc->kerning == LV_FONT_KERNING_NORMAL && dsc->cache_node->face_has_kerning && unicode_letter_next != '\0') { + lv_mutex_lock(&dsc->cache_node->face_lock); + FT_Face face = dsc->cache_node->face; + if(FT_IS_SCALABLE(face)) { + FT_Error set_size_error = FT_Set_Pixel_Sizes(face, 0, dsc->size); + if(set_size_error) { + FT_ERROR_MSG("FT_Set_Pixel_Sizes", set_size_error); + } + } + FT_UInt glyph_index_next = FT_Get_Char_Index(face, unicode_letter_next); + FT_Vector kerning; + FT_Error error = FT_Get_Kerning(face, g_dsc->gid.index, glyph_index_next, FT_KERNING_DEFAULT, &kerning); + if(!error) { + g_dsc->adv_w += LV_FREETYPE_F26DOT6_TO_INT(kerning.x); + } + else { + FT_ERROR_MSG("FT_Get_Kerning", error); + } + lv_mutex_unlock(&dsc->cache_node->face_lock); + } + + g_dsc->entry = NULL; + + lv_cache_release(glyph_cache, entry, NULL); + LV_PROFILER_FONT_END; + return true; +} + +/*----------------- + * Cache Callbacks + *----------------*/ + +static bool freetype_glyph_create_cb(lv_freetype_glyph_cache_data_t * data, void * user_data) +{ + LV_PROFILER_FONT_BEGIN; + + FT_Error error; + lv_freetype_font_dsc_t * dsc = (lv_freetype_font_dsc_t *)user_data; + lv_font_glyph_dsc_t * dsc_out = &data->glyph_dsc; + + lv_mutex_lock(&dsc->cache_node->face_lock); + FT_Face face = dsc->cache_node->face; + FT_UInt glyph_index = FT_Get_Char_Index(face, data->unicode); + + if(FT_IS_SCALABLE(face)) { + error = FT_Set_Pixel_Sizes(face, 0, dsc->size); + } + else { + error = FT_Select_Size(face, 0); + } + if(error) { + FT_ERROR_MSG("FT_Set_Pixel_Sizes", error); + lv_mutex_unlock(&dsc->cache_node->face_lock); + return false; + } + + if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_OUTLINE) { + error = FT_Load_Glyph(face, glyph_index, FT_LOAD_COMPUTE_METRICS | FT_LOAD_NO_BITMAP | FT_LOAD_NO_AUTOHINT); + } + else if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP) { + error = FT_Load_Glyph(face, glyph_index, FT_LOAD_COMPUTE_METRICS | FT_LOAD_NO_AUTOHINT); + } + else if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO) { + error = FT_Load_Glyph(face, glyph_index, FT_LOAD_COMPUTE_METRICS | FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT); + if(!error) { + error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_MONO); + } + } + if(error) { + FT_ERROR_MSG("FT_Load_Glyph", error); + lv_mutex_unlock(&dsc->cache_node->face_lock); + LV_PROFILER_FONT_END; + return false; + } + + FT_GlyphSlot glyph = face->glyph; + + if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_OUTLINE) { + + dsc_out->adv_w = FT_F26DOT6_TO_INT(glyph->metrics.horiAdvance); + dsc_out->box_h = FT_F26DOT6_TO_INT(glyph->metrics.height); /*Height of the bitmap in [px]*/ + dsc_out->box_w = FT_F26DOT6_TO_INT(glyph->metrics.width); /*Width of the bitmap in [px]*/ + dsc_out->ofs_x = FT_F26DOT6_TO_INT(glyph->metrics.horiBearingX); /*X offset of the bitmap in [pf]*/ + dsc_out->ofs_y = FT_F26DOT6_TO_INT(glyph->metrics.horiBearingY - + glyph->metrics.height); /*Y offset of the bitmap measured from the as line*/ + dsc_out->format = LV_FONT_GLYPH_FORMAT_VECTOR; + + /*Transform the glyph to italic if required */ + if(dsc->style & LV_FREETYPE_FONT_STYLE_ITALIC) { + dsc_out->box_w = lv_freetype_italic_transform_on_pos((lv_point_t) { + dsc_out->box_w, dsc_out->box_h + }); + } + } + else if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP || + dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO) { + FT_Bitmap * glyph_bitmap = &face->glyph->bitmap; + + dsc_out->adv_w = FT_F26DOT6_TO_INT(glyph->advance.x); /*Width of the glyph in [pf]*/ + dsc_out->box_h = glyph_bitmap->rows; /*Height of the bitmap in [px]*/ + dsc_out->box_w = glyph_bitmap->width; /*Width of the bitmap in [px]*/ + dsc_out->ofs_x = glyph->bitmap_left; /*X offset of the bitmap in [pf]*/ + dsc_out->ofs_y = glyph->bitmap_top - + dsc_out->box_h; /*Y offset of the bitmap measured from the as line*/ + if(glyph->format == FT_GLYPH_FORMAT_BITMAP) + dsc_out->format = LV_FONT_GLYPH_FORMAT_IMAGE; + else + dsc_out->format = LV_FONT_GLYPH_FORMAT_A8; + } + + dsc_out->is_placeholder = glyph_index == 0; + dsc_out->gid.index = (uint32_t)glyph_index; + + lv_mutex_unlock(&dsc->cache_node->face_lock); + + LV_PROFILER_FONT_END; + return true; +} +static void freetype_glyph_free_cb(lv_freetype_glyph_cache_data_t * data, void * user_data) +{ + LV_UNUSED(data); + LV_UNUSED(user_data); +} +static lv_cache_compare_res_t freetype_glyph_compare_cb(const lv_freetype_glyph_cache_data_t * lhs, + const lv_freetype_glyph_cache_data_t * rhs) +{ + if(lhs->unicode != rhs->unicode) { + return lhs->unicode > rhs->unicode ? 1 : -1; + } + if(lhs->size != rhs->size) { + return lhs->size > rhs->size ? 1 : -1; + } + return 0; +} + +#endif /*LV_USE_FREETYPE*/ diff --git a/projects/APPLaunch/main/ui/lvgl/lv_freetype_image.c b/projects/APPLaunch/main/ui/lvgl/lv_freetype_image.c new file mode 100644 index 00000000..cf06e020 --- /dev/null +++ b/projects/APPLaunch/main/ui/lvgl/lv_freetype_image.c @@ -0,0 +1,246 @@ +/** + * @file lv_freetype_image.c + * + */ + +/********************* + * INCLUDES + *********************/ + +#include "../../lvgl.h" +#include "lv_freetype_private.h" + +#if LV_USE_FREETYPE + +#include "../../core/lv_global.h" + +#define font_draw_buf_handlers &(LV_GLOBAL_DEFAULT()->font_draw_buf_handlers) + +/********************* + * DEFINES + *********************/ + +#define CACHE_NAME "FREETYPE_IMAGE" + +/********************** + * TYPEDEFS + **********************/ + +typedef struct _lv_freetype_image_cache_data_t { + FT_UInt glyph_index; + uint32_t size; + + lv_draw_buf_t * draw_buf; +} lv_freetype_image_cache_data_t; + +/********************** + * STATIC PROTOTYPES + **********************/ +static const void * freetype_get_glyph_bitmap_cb(lv_font_glyph_dsc_t * g_dsc, lv_draw_buf_t * draw_buf); + +static bool freetype_image_create_cb(lv_freetype_image_cache_data_t * data, void * user_data); +static void freetype_image_free_cb(lv_freetype_image_cache_data_t * node, void * user_data); +static lv_cache_compare_res_t freetype_image_compare_cb(const lv_freetype_image_cache_data_t * lhs, + const lv_freetype_image_cache_data_t * rhs); + +static void freetype_image_release_cb(const lv_font_t * font, lv_font_glyph_dsc_t * g_dsc); +/********************** + * STATIC VARIABLES + **********************/ + +/********************** + * MACROS + **********************/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +lv_cache_t * lv_freetype_create_draw_data_image(uint32_t cache_size) +{ + lv_cache_ops_t ops = { + .compare_cb = (lv_cache_compare_cb_t)freetype_image_compare_cb, + .create_cb = (lv_cache_create_cb_t)freetype_image_create_cb, + .free_cb = (lv_cache_free_cb_t)freetype_image_free_cb, + }; + + lv_cache_t * draw_data_cache = lv_cache_create(&lv_cache_class_lru_rb_count, sizeof(lv_freetype_image_cache_data_t), + cache_size, ops); + lv_cache_set_name(draw_data_cache, CACHE_NAME); + + return draw_data_cache; +} + +void lv_freetype_set_cbs_image_font(lv_freetype_font_dsc_t * dsc) +{ + LV_ASSERT_FREETYPE_FONT_DSC(dsc); + dsc->font.get_glyph_bitmap = freetype_get_glyph_bitmap_cb; + dsc->font.release_glyph = freetype_image_release_cb; +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +static const void * freetype_get_glyph_bitmap_cb(lv_font_glyph_dsc_t * g_dsc, lv_draw_buf_t * draw_buf) +{ + LV_UNUSED(draw_buf); + LV_PROFILER_FONT_BEGIN; + const lv_font_t * font = g_dsc->resolved_font; + lv_freetype_font_dsc_t * dsc = (lv_freetype_font_dsc_t *)font->dsc; + LV_ASSERT_FREETYPE_FONT_DSC(dsc); + + FT_UInt glyph_index = (FT_UInt)g_dsc->gid.index; + + lv_cache_t * cache = dsc->cache_node->draw_data_cache; + + lv_freetype_image_cache_data_t search_key = { + .glyph_index = glyph_index, + .size = dsc->size, + }; + + lv_cache_entry_t * entry = lv_cache_acquire_or_create(cache, &search_key, dsc); + if(entry == NULL) { + LV_LOG_ERROR("glyph bitmap lookup failed for glyph_index = 0x%" LV_PRIx32, (uint32_t)glyph_index); + LV_PROFILER_FONT_END; + return NULL; + } + + g_dsc->entry = entry; + lv_freetype_image_cache_data_t * cache_node = lv_cache_entry_get_data(entry); + + LV_PROFILER_FONT_END; + return cache_node->draw_buf; +} + +static void freetype_image_release_cb(const lv_font_t * font, lv_font_glyph_dsc_t * g_dsc) +{ + LV_ASSERT_NULL(font); + lv_freetype_font_dsc_t * dsc = (lv_freetype_font_dsc_t *)font->dsc; + lv_cache_release(dsc->cache_node->draw_data_cache, g_dsc->entry, NULL); + g_dsc->entry = NULL; +} + +/*----------------- + * Cache Callbacks + *----------------*/ + +static bool freetype_image_create_cb(lv_freetype_image_cache_data_t * data, void * user_data) +{ + LV_PROFILER_FONT_BEGIN; + + lv_freetype_font_dsc_t * dsc = (lv_freetype_font_dsc_t *)user_data; + + FT_Error error; + + lv_mutex_lock(&dsc->cache_node->face_lock); + + FT_Face face = dsc->cache_node->face; + if(FT_IS_SCALABLE(face)) { + error = FT_Set_Pixel_Sizes(face, 0, dsc->size); + } + else { + error = FT_Select_Size(face, 0); + } + if(error) { + FT_ERROR_MSG("FT_Set_Pixel_Sizes", error); + lv_mutex_unlock(&dsc->cache_node->face_lock); + return false; + } + if(dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO) { + error = FT_Load_Glyph(face, data->glyph_index, FT_LOAD_TARGET_MONO | FT_LOAD_NO_AUTOHINT); + } + else { + error = FT_Load_Glyph(face, data->glyph_index, + FT_LOAD_COLOR | FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_AUTOHINT); + } + if(error) { + FT_ERROR_MSG("FT_Load_Glyph", error); + lv_mutex_unlock(&dsc->cache_node->face_lock); + LV_PROFILER_FONT_END; + return false; + } + error = FT_Render_Glyph(face->glyph, dsc->render_mode == LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO + ? FT_RENDER_MODE_MONO + : FT_RENDER_MODE_NORMAL); + if(error) { + FT_ERROR_MSG("FT_Render_Glyph", error); + lv_mutex_unlock(&dsc->cache_node->face_lock); + LV_PROFILER_FONT_END; + return false; + } + + FT_Glyph glyph; + error = FT_Get_Glyph(face->glyph, &glyph); + if(error) { + FT_ERROR_MSG("FT_Get_Glyph", error); + lv_mutex_unlock(&dsc->cache_node->face_lock); + LV_PROFILER_FONT_END; + return false; + } + + FT_BitmapGlyph glyph_bitmap = (FT_BitmapGlyph)glyph; + + uint16_t box_h = glyph_bitmap->bitmap.rows; /*Height of the bitmap in [px]*/ + uint16_t box_w = glyph_bitmap->bitmap.width; /*Width of the bitmap in [px]*/ + + lv_color_format_t col_format; + if(glyph_bitmap->bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + col_format = LV_COLOR_FORMAT_ARGB8888; + } + else { + col_format = LV_COLOR_FORMAT_A8; + } + int32_t pitch = glyph_bitmap->bitmap.pitch; + uint32_t pitch_abs = pitch < 0 ? (uint32_t)-pitch : (uint32_t)pitch; + uint32_t stride = lv_draw_buf_width_to_stride(box_w, col_format); + data->draw_buf = lv_draw_buf_create_ex(font_draw_buf_handlers, box_w, box_h, col_format, stride); + if(!data->draw_buf) { + LV_LOG_WARN("Could not create draw buffer"); + FT_Done_Glyph(glyph); + lv_mutex_unlock(&dsc->cache_node->face_lock); + LV_PROFILER_FONT_END; + return false; + } + lv_draw_buf_clear(data->draw_buf, NULL); + + for(int y = 0; y < box_h; ++y) { + const uint8_t * src = pitch < 0 + ? glyph_bitmap->bitmap.buffer + (box_h - 1 - y) * pitch_abs + : glyph_bitmap->bitmap.buffer + y * pitch_abs; + uint8_t * dst = (uint8_t *)(data->draw_buf->data) + y * stride; + + if(glyph_bitmap->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) { + for(int x = 0; x < box_w; ++x) { + dst[x] = (src[x >> 3] & (0x80 >> (x & 0x7))) ? 0xff : 0x00; + } + } + else { + lv_memcpy(dst, src, LV_MIN(stride, pitch_abs)); + } + } + + lv_draw_buf_flush_cache(data->draw_buf, NULL); + FT_Done_Glyph(glyph); + lv_mutex_unlock(&dsc->cache_node->face_lock); + LV_PROFILER_FONT_END; + return true; +} +static void freetype_image_free_cb(lv_freetype_image_cache_data_t * data, void * user_data) +{ + LV_UNUSED(user_data); + lv_draw_buf_destroy(data->draw_buf); +} +static lv_cache_compare_res_t freetype_image_compare_cb(const lv_freetype_image_cache_data_t * lhs, + const lv_freetype_image_cache_data_t * rhs) +{ + if(lhs->glyph_index != rhs->glyph_index) { + return lhs->glyph_index > rhs->glyph_index ? 1 : -1; + } + if(lhs->size != rhs->size) { + return lhs->size > rhs->size ? 1 : -1; + } + return 0; +} + +#endif /*LV_USE_FREETYPE*/ diff --git a/projects/APPLaunch/main/ui/lvgl/lv_sdl_keyboard.c b/projects/APPLaunch/main/ui/lvgl/lv_sdl_keyboard.c new file mode 100644 index 00000000..e69de29b diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp deleted file mode 100644 index 87d37a30..00000000 --- a/projects/APPLaunch/main/ui/page_app/ui_app_console.hpp +++ /dev/null @@ -1,1280 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ - -#pragma once -#include "sample_log.h" -#include "../ui_app_page.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "cp0_lvgl_app.h" - -// ============================================================ -// Terminal console UIConsolePage -// Screen resolution: 320 x 170 (top bar20px, ui_APP_Container 320x150) -// -// Features: -// - Full VT100/ANSI terminal emulation (ported from the src/vt100.c state machine) -// Supports cursor movement, erase, SGR attributes, insert/delete lines/characters, -// DEC private modes (DECTCEM, DECAWM, DECSCNMetc.), -// SCP/RCP cursor save/restore, scrolling region, device attributesetc. -// - PTY child-process management (fork + openpty) -// - line-level dirty rendering to reduce LVGL refresh overhead -// - cursor blink (500ms timer) -// - keyboard input forwarded to PTY (evdev keycode + utf8 / LV_KEY_*) -// -// Public API: -// exec(std::string cmd) — start a command (supports command strings with arguments) -// ============================================================ -class UIConsolePage : public AppPage -{ - /* ------------------------------------------------------------------ */ - /* Terminal geometry */ - /* ------------------------------------------------------------------ */ - static constexpr int TERM_W = 320; - static constexpr int TERM_H = 150; - static constexpr int CHAR_W = 7; - static constexpr int CHAR_H = 12; - static constexpr int COLS = TERM_W / CHAR_W; /* 45 */ - static constexpr int ROWS = TERM_H / CHAR_H; /* 12 */ - - /* green text on black background */ - static constexpr uint32_t FIXED_FG = 0x00FF00u; - static constexpr uint32_t FIXED_BG = 0x000000u; - - /* ------------------------------------------------------------------ */ - /* UI objects */ - /* ------------------------------------------------------------------ */ - lv_obj_t *terminal_container = nullptr; - lv_obj_t *term_canvas = nullptr; - - /* Full-row rendering: ROWS labels instead of many cell labels */ - lv_obj_t *row_labels[ROWS] = {}; - /* Cursor: a separate inverted label; style is set only once at creation */ - lv_obj_t *cursor_label = nullptr; - - /* line-level dirty comparison cache (including trailing '\0') */ - char row_rendered[ROWS][COLS + 1] = {}; - -public: - bool terminal_sysplause = true; - -public: - UIConsolePage() : AppPage() - { - console_data_init(); - creat_console_UI(); - event_handler_init(); - } - - ~UIConsolePage() - { - terminal_active = false; - if (poll_timer) - { - lv_timer_delete(poll_timer); - poll_timer = nullptr; - } - if (cursor_timer) - { - lv_timer_delete(cursor_timer); - cursor_timer = nullptr; - } - stop_pty(); - } - - // ==================== Public API ==================== - - /** - * Start a command. - * The command string is split on spaces; the first token is the executable path and the rest are arguments. - * If a child process is already running, terminate it before restarting. - */ - void exec(std::string cmd) - { - if (!pty_handle.empty()) - stop_pty(); - - terminal_active = true; - vt100_cur_row = 0; - vt100_cur_col = 0; - vt100_cur_attr = 0; - vt100_cur_fg = 7; - vt100_cur_bg = 0; - vt100_saved_row = 0; vt100_saved_col = 0; - vt100_saved_attr = 0; vt100_saved_fg = 7; vt100_saved_bg = 0; - vt100_auto_wrap = true; - vt100_cursor_visible_flag = true; - vt100_decckm = false; - vt100_esc_state = VT100_ST_NORMAL; - vt100_esc_len = 0; - vt100_param_count = 0; - vt100_param_val = 0; - vt100_priv_mode = false; - vt100_sec_mode = false; - vt100_skip_until_st = false; - memset(vt100_params, 0, sizeof(vt100_params)); - waiting_key_to_exit = false; - - vt100_screen_clear_all(); - /* Force the first full render */ - memset(row_rendered, 0, sizeof(row_rendered)); - vt100_render_all(); - - /* Split the command string on spaces */ - std::vector tokens; - std::istringstream iss(cmd); - std::string token; - while (iss >> token) - tokens.push_back(token); - - if (tokens.empty()) - { - const char *err = "Error: empty command\r\n"; - vt100_process_bytes(err, (int)strlen(err)); - vt100_render_all(); - terminal_active = false; - return; - } - - std::string executable = tokens[0]; - std::vector args(tokens.begin() + 1, tokens.end()); - - if (!start_pty(executable, args)) - { - const char *err = "Error: openpty/fork failed\r\n"; - vt100_process_bytes(err, (int)strlen(err)); - vt100_render_all(); - terminal_active = false; - return; - } - - if (!poll_timer) - poll_timer = lv_timer_create(UIConsolePage::s_poll_cb, 30, this); - if (!cursor_timer) - cursor_timer = lv_timer_create(UIConsolePage::s_cursor_blink_cb, 500, this); - set_page_title(executable); - } - -private: - /* ================================================================== */ - /* VT100 character grid state */ - /* ================================================================== */ - char vt100_screen[ROWS][COLS] = {}; - - /* Cursor */ - int vt100_cur_row = 0; - int vt100_cur_col = 0; - - /* Current SGR attributes (tracked by parser; rendering remains monochrome) */ - int vt100_cur_attr = 0; /* ATTR_BOLD=1, ATTR_UNDERLINE=2, ATTR_BLINK=4, ATTR_REVERSE=8 */ - int vt100_cur_fg = 7; /* ANSI 8colors: 0-7, COLOR_DEFAULT=9 */ - int vt100_cur_bg = 0; - - /* Cursor save/restore (SCP/RCP, DECSC/DECRC) */ - int vt100_saved_row = 0, vt100_saved_col = 0; - int vt100_saved_attr = 0, vt100_saved_fg = 7, vt100_saved_bg = 0; - - /* DECAWM — automatic wrap */ - bool vt100_auto_wrap = true; - - /* DECTCEM — cursor visibility */ - bool vt100_cursor_visible_flag = true; - - /* DECCKM — application cursor key mode (affects what sequence keyboard sends) */ - bool vt100_decckm = false; - - /* ── Full VT100 state machine ─────────────────────────────────── */ - enum vt_state { - VT100_ST_NORMAL, /* print characters */ - VT100_ST_ESC, /* received ESC */ - VT100_ST_CSI, /* ESC [ — collect parameters */ - VT100_ST_CSI_QM, /* ESC [? — DEC private modes */ - VT100_ST_CSI_GT, /* ESC [> — Secondary DA / xterm extension */ - VT100_ST_OSC, /* ESC ] — operating system command */ - VT100_ST_DCS, /* ESC P — device control string */ - }; - vt_state vt100_esc_state = VT100_ST_NORMAL; - char vt100_esc_buf[64] = {}; - int vt100_esc_len = 0; - - /* CSI parameters */ - static constexpr int VT100_MAX_PARAMS = 16; - int vt100_params[VT100_MAX_PARAMS] = {}; - int vt100_param_count = 0; - int vt100_param_val = 0; - bool vt100_priv_mode = false; - bool vt100_sec_mode = false; - bool vt100_skip_until_st = false; - - /* ── PTY ──────────────────────────────────────────────── */ - std::string pty_handle; - - lv_timer_t *poll_timer = nullptr; - lv_timer_t *cursor_timer = nullptr; - bool vt100_cursor_vis = false; - bool terminal_active = false; - bool waiting_key_to_exit = false; - - /* ================================================================== */ - /* Initialize */ - /* ================================================================== */ - void console_data_init() - { - memset(vt100_screen, ' ', sizeof(vt100_screen)); - memset(row_rendered, 0, sizeof(row_rendered)); - memset(row_labels, 0, sizeof(row_labels)); - memset(vt100_esc_buf, 0, sizeof(vt100_esc_buf)); - memset(vt100_params, 0, sizeof(vt100_params)); - } - - /* ================================================================== */ - /* UI construction(terminal area mounted under ui_APP_Container) */ - /* ================================================================== */ - void creat_console_UI() - { - terminal_container = lv_obj_create(ui_APP_Container); - lv_obj_remove_style_all(terminal_container); - lv_obj_set_size(terminal_container, TERM_W, TERM_H); - lv_obj_set_pos(terminal_container, 0, 0); - lv_obj_set_style_bg_color(terminal_container, lv_color_hex(FIXED_BG), 0); - lv_obj_set_style_bg_opa(terminal_container, LV_OPA_COVER, 0); - lv_obj_clear_flag(terminal_container, - (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - - term_canvas = lv_obj_create(terminal_container); - lv_obj_set_size(term_canvas, TERM_W, TERM_H); - lv_obj_set_pos(term_canvas, 0, 0); - lv_obj_set_style_bg_color(term_canvas, lv_color_hex(FIXED_BG), 0); - lv_obj_set_style_bg_opa(term_canvas, LV_OPA_COVER, 0); - lv_obj_set_style_border_width(term_canvas, 0, 0); - lv_obj_set_style_pad_all(term_canvas, 0, 0); - lv_obj_set_style_radius(term_canvas, 0, 0); - lv_obj_remove_flag(term_canvas, - (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); - - /* --------------------- line-level label ------------------------ */ - const lv_font_t *mono_font = launcher_fonts().get("LiberationMono-Regular.ttf", 12, LV_FREETYPE_FONT_STYLE_NORMAL); - for (int r = 0; r < ROWS; r++) - { - lv_obj_t *lbl = lv_label_create(term_canvas); - lv_obj_set_style_text_font(lbl, mono_font, 0); - lv_obj_set_style_text_color(lbl, lv_color_hex(FIXED_FG), 0); - lv_obj_set_style_bg_opa(lbl, LV_OPA_TRANSP, 0); - lv_obj_set_style_pad_all(lbl, 0, 0); - lv_obj_set_style_text_letter_space(lbl, 0, 0); - lv_obj_set_style_text_line_space(lbl, 0, 0); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); - lv_obj_set_size(lbl, TERM_W, CHAR_H); - lv_obj_set_pos(lbl, 0, r * CHAR_H); - lv_label_set_text(lbl, ""); - row_labels[r] = lbl; - } - memset(row_rendered, 0, sizeof(row_rendered)); - - /* --------------------- cursor label (inverted block)---------------- */ - cursor_label = lv_label_create(term_canvas); - lv_obj_set_style_text_font(cursor_label, mono_font, 0); - lv_obj_set_style_text_color(cursor_label, lv_color_hex(FIXED_BG), 0); - lv_obj_set_style_bg_color(cursor_label, lv_color_hex(FIXED_FG), 0); - lv_obj_set_style_bg_opa(cursor_label, LV_OPA_COVER, 0); - lv_obj_set_style_pad_all(cursor_label, 0, 0); - lv_obj_set_style_text_letter_space(cursor_label, 0, 0); - lv_label_set_long_mode(cursor_label, LV_LABEL_LONG_CLIP); - lv_obj_set_size(cursor_label, CHAR_W, CHAR_H); - lv_label_set_text(cursor_label, " "); - lv_obj_set_pos(cursor_label, 0, 0); - lv_obj_add_flag(cursor_label, LV_OBJ_FLAG_HIDDEN); - } - - /* ================================================================== */ - /* Event binding */ - /* ================================================================== */ - void event_handler_init() - { - lv_obj_add_event_cb(root_screen_, UIConsolePage::static_lvgl_handler, LV_EVENT_ALL, this); - } - - static void static_lvgl_handler(lv_event_t *e) - { - UIConsolePage *self = static_cast(lv_event_get_user_data(e)); - if (self) - self->event_handler(e); - } - - void event_handler(lv_event_t *e) - { - if (lv_event_get_code(e) == LV_EVENT_KEYBOARD) - { - struct key_item *elm = (struct key_item *)lv_event_get_param(e); - SLOGI("[CONSOLE] code=%u state=%s sym=%s utf8_len=%zu pty_active=%d waiting_exit=%d", - elm->key_code, kbd_state_name(elm->key_state), elm->sym_name, - strlen(elm->utf8), (int)terminal_active, (int)waiting_key_to_exit); - if (waiting_key_to_exit && (elm->key_state == 0)) - { - if (terminal_sysplause) - { - terminal_sysplause = false; - } - else - { - waiting_key_to_exit = false; - if (navigate_home) - navigate_home(); - } - } - else - { - if (!pty_handle.empty() && terminal_active) - { - if (elm->key_state) { - SLOGI("[CONSOLE] -> PTY write (state=%s)", kbd_state_name(elm->key_state)); - write_key_to_pty(elm->key_code, elm->utf8); - } - } - } - } - } - - /* ================================================================== */ - /* LVGL timer static wrapper */ - /* ================================================================== */ - static void s_poll_cb(lv_timer_t *t) - { - auto self = (UIConsolePage *)lv_timer_get_user_data(t); - if (self) - self->vt100_poll_cb(t); - } - - static void s_cursor_blink_cb(lv_timer_t *t) - { - auto self = (UIConsolePage *)lv_timer_get_user_data(t); - if (self) - self->vt100_cursor_blink_cb(t); - - static int end_status = 0; - static std::chrono::time_point start_time; - static std::chrono::time_point end_time; - pid_t pid_ret; - if (end_status == 0) - { - if (LVGL_HOME_KEY_FLAG) - { - end_status = 1; - start_time = std::chrono::steady_clock::now(); - } - } - if (end_status == 1) - { - if (LVGL_HOME_KEY_FLAG) - { - end_time = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(end_time - start_time).count() >= 5) - { - end_status = 0; - SLOGI("[CONSOLE] ESC held 5s -> kill PTY and go back home"); - self->stop_pty(); - self->terminal_active = false; - if (self->navigate_home) - self->navigate_home(); - } - } - else - { - end_status = 0; - } - } - } - - /* ================================================================== */ - /* VT100 helper functions */ - /* ================================================================== */ - - static int clamp(int v, int lo, int hi) - { - if (v < lo) return lo; - if (v > hi) return hi; - return v; - } - - /** defparam: Default parameters or explicit 0 both return the default value. - * idx >= param_count -> parameter missing -> dfl - * params[idx] == 0 -> explicit 0 or missing -> dfl - */ - int defparam(int idx, int dfl) - { - return (idx >= vt100_param_count || vt100_params[idx] == 0) ? dfl : vt100_params[idx]; - } - - /* ================================================================== */ - /* Character grid operations */ - /* ================================================================== */ - void vt100_screen_clear_all() - { - for (int r = 0; r < ROWS; r++) - memset(vt100_screen[r], ' ', COLS); - } - - void vt100_clear_row_from(int row, int from_col) - { - if (row < 0 || row >= ROWS) return; - if (from_col < 0) from_col = 0; - if (from_col >= COLS) return; - memset(&vt100_screen[row][from_col], ' ', COLS - from_col); - } - - void vt100_scroll_up(int top, int bot, int n) - { - if (top < 0) top = 0; - if (bot >= ROWS) bot = ROWS - 1; - if (n <= 0 || n > bot - top + 1) return; - for (int r = top; r <= bot - n; r++) - memcpy(vt100_screen[r], vt100_screen[r + n], COLS); - for (int r = bot - n + 1; r <= bot; r++) - memset(vt100_screen[r], ' ', COLS); - } - - void vt100_scroll_down(int top, int bot, int n) - { - if (top < 0) top = 0; - if (bot >= ROWS) bot = ROWS - 1; - if (n <= 0 || n > bot - top + 1) return; - for (int r = bot; r >= top + n; r--) - memcpy(vt100_screen[r], vt100_screen[r - n], COLS); - for (int r = top; r < top + n; r++) - memset(vt100_screen[r], ' ', COLS); - } - - void vt100_erase_display(int mode) - { - if (mode == 0) { - vt100_clear_row_from(vt100_cur_row, vt100_cur_col); - for (int r = vt100_cur_row + 1; r < ROWS; r++) - vt100_clear_row_from(r, 0); - } else if (mode == 1) { - for (int r = 0; r < vt100_cur_row; r++) - vt100_clear_row_from(r, 0); - for (int c = 0; c <= vt100_cur_col && c < COLS; c++) - vt100_screen[vt100_cur_row][c] = ' '; - } else { - vt100_screen_clear_all(); - } - } - - void vt100_erase_line(int mode) - { - int start, end; - if (mode == 0) { - start = vt100_cur_col; end = COLS - 1; - } else if (mode == 1) { - start = 0; end = vt100_cur_col; - } else { - start = 0; end = COLS - 1; - } - for (int c = start; c <= end && c < COLS; c++) - vt100_screen[vt100_cur_row][c] = ' '; - } - - void vt100_insert_lines(int n) - { - if (n < 1) n = 1; - vt100_scroll_down(vt100_cur_row, ROWS - 1, n); - } - - void vt100_delete_lines(int n) - { - if (n < 1) n = 1; - vt100_scroll_up(vt100_cur_row, ROWS - 1, n); - } - - void vt100_insert_chars(int n) - { - if (n < 1) n = 1; - for (int i = COLS - 1; i >= vt100_cur_col + n; i--) - vt100_screen[vt100_cur_row][i] = vt100_screen[vt100_cur_row][i - n]; - for (int i = 0; i < n && vt100_cur_col + i < COLS; i++) - vt100_screen[vt100_cur_row][vt100_cur_col + i] = ' '; - } - - void vt100_delete_chars(int n) - { - if (n < 1) n = 1; - for (int i = vt100_cur_col; i < COLS - n; i++) - vt100_screen[vt100_cur_row][i] = vt100_screen[vt100_cur_row][i + n]; - for (int i = COLS - n; i < COLS; i++) - vt100_screen[vt100_cur_row][i] = ' '; - } - - /* ================================================================== */ - /* Character output (including DECAWM automatic wrap) */ - /* ================================================================== */ - void vt100_put_char(char ch) - { - if (ch == '\r') - { - vt100_cur_col = 0; - return; - } - if (ch == '\n') - { - vt100_cur_col = 0; - if (++vt100_cur_row >= ROWS) - { - vt100_scroll_up(0, ROWS - 1, 1); - vt100_cur_row = ROWS - 1; - } - return; - } - if (ch == '\b') - { - if (vt100_cur_col > 0) - vt100_cur_col--; - return; - } - if (ch == '\t') - { - int next_tab = (vt100_cur_col / 8 + 1) * 8; - if (next_tab >= COLS) next_tab = COLS - 1; - while (vt100_cur_col < next_tab) - vt100_put_char(' '); - return; - } - /* C0 controls (0x00-0x1F) — ignore */ - if ((unsigned char)ch < 0x20) - return; - /* DEL (0x7F) */ - if ((unsigned char)ch == 0x7F) - return; - - /* ── DECAWM automatic-wrap check ── */ - if (vt100_cur_col >= COLS) - { - if (vt100_auto_wrap) - { - vt100_cur_col = 0; - if (++vt100_cur_row >= ROWS) - { - vt100_scroll_up(0, ROWS - 1, 1); - vt100_cur_row = ROWS - 1; - } - } - else - { - vt100_cur_col = COLS - 1; /* overwrite the last column */ - } - } - vt100_screen[vt100_cur_row][vt100_cur_col++] = ch; - } - - /* ================================================================== */ - /* ESC sequence handling (non-CSI) */ - /* ================================================================== */ - void vt100_handle_esc(char c) - { - switch (c) { - case 'D': /* IND — Index: move cursor down and scroll if needed */ - if (vt100_cur_row == ROWS - 1) - vt100_scroll_up(0, ROWS - 1, 1); - else - vt100_cur_row++; - break; - case 'M': /* RI — Reverse Index: move cursor up */ - if (vt100_cur_row == 0) - vt100_scroll_down(0, ROWS - 1, 1); - else - vt100_cur_row--; - break; - case 'E': /* NEL — Next Line */ - vt100_cur_col = 0; - if (vt100_cur_row == ROWS - 1) - vt100_scroll_up(0, ROWS - 1, 1); - else - vt100_cur_row++; - break; - case 'H': /* HTS — Horizontal Tab Set (acknowledge) */ - break; - case '7': /* DECSC — Save Cursor */ - vt100_saved_row = vt100_cur_row; - vt100_saved_col = vt100_cur_col; - vt100_saved_attr = vt100_cur_attr; - vt100_saved_fg = vt100_cur_fg; - vt100_saved_bg = vt100_cur_bg; - break; - case '8': /* DECRC — Restore Cursor */ - vt100_cur_row = clamp(vt100_saved_row, 0, ROWS - 1); - vt100_cur_col = clamp(vt100_saved_col, 0, COLS - 1); - vt100_cur_attr = vt100_saved_attr; - vt100_cur_fg = vt100_saved_fg; - vt100_cur_bg = vt100_saved_bg; - break; - case 'c': /* RIS — Reset to Initial State */ - vt100_screen_clear_all(); - vt100_cur_row = 0; - vt100_cur_col = 0; - vt100_cur_attr = 0; - vt100_cur_fg = 7; - vt100_cur_bg = 0; - vt100_auto_wrap = true; - vt100_cursor_visible_flag = true; - vt100_decckm = false; - break; - case '=': /* DECKPAM — Keypad Application Mode */ - case '>': /* DECKPNM — Keypad Numeric Mode */ - break; - case '(': /* SCS — Designate G0 charset */ - case ')': /* SCS — Designate G1 charset */ - case '*': /* SCS — Designate G2 charset */ - case '+': /* SCS — Designate G3 charset */ - case '#': /* DEC line attributes */ - /* consume next byte; we ignore */ - vt100_esc_state = VT100_ST_NORMAL; - break; - default: - break; - } - } - - /* ================================================================== */ - /* CSI sequence dispatch (complete VT100 instruction set) */ - /* ================================================================== */ - void vt100_handle_csi(char final) - { - /* ── DEC private modes (ESC [? ... h/l) ── */ - if (vt100_priv_mode) { - switch (final) { - case 'h': /* DECSET */ - if (vt100_params[0] == 1) { vt100_decckm = true; fprintf(stderr, "[VT100-DBG] DECCKM ON (app cursor keys)\n"); } /* DECCKM */ - if (vt100_params[0] == 5) {} /* DECSCNM — reverse video */ - if (vt100_params[0] == 6) {} /* DECOM — origin mode */ - if (vt100_params[0] == 7) { vt100_auto_wrap = true; } /* DECAWM */ - if (vt100_params[0] == 25) { vt100_cursor_visible_flag = true; } /* DECTCEM */ - if (vt100_params[0] == 1049) {} /* alt screen */ - break; - case 'l': /* DECRST */ - if (vt100_params[0] == 1) { vt100_decckm = false; } - if (vt100_params[0] == 5) {} - if (vt100_params[0] == 6) {} - if (vt100_params[0] == 7) { vt100_auto_wrap = false; } /* DECAWM off */ - if (vt100_params[0] == 25) { vt100_cursor_visible_flag = false; } /* DECTCEM hide */ - if (vt100_params[0] == 1049) {} - break; - default: - break; - } - return; - } - - /* ── Secondary DA / xterm query (ESC [> ... c) ── */ - if (vt100_sec_mode) { - fprintf(stderr, "[VT100-DBG] handle_csi SEC_MODE final='%c'(0x%02X) param[0]=%d\n", - final, (unsigned char)final, vt100_params[0]); - switch (final) { - case 'c': /* Secondary Device Attributes */ - /* Reply: VT100 (type 0), firmware v10, no options */ - fprintf(stderr, "[VT100-DBG] SDA reply: \\033[>0;10;0c\n"); - if (!pty_handle.empty()) - pty_write( "\033[>0;10;0c", 10); - break; - case 'm': /* xterm set-modifyOtherKeys — ignore */ - fprintf(stderr, "[VT100-DBG] SDA: set-modifyOtherKeys ignored\n"); - break; - default: - fprintf(stderr, "[VT100-DBG] SDA: unhandled final byte\n"); - break; - } - return; - } - - switch (final) { - /* ── Cursor movement ───────────────────── */ - case 'A': /* CUU */ - vt100_cur_row -= defparam(0, 1); - if (vt100_cur_row < 0) vt100_cur_row = 0; - break; - case 'B': /* CUD */ - vt100_cur_row += defparam(0, 1); - if (vt100_cur_row >= ROWS) vt100_cur_row = ROWS - 1; - break; - case 'C': /* CUF */ - vt100_cur_col += defparam(0, 1); - if (vt100_cur_col >= COLS) vt100_cur_col = COLS - 1; - break; - case 'D': /* CUB */ - vt100_cur_col -= defparam(0, 1); - if (vt100_cur_col < 0) vt100_cur_col = 0; - break; - case 'H': /* CUP — Cursor Position */ - case 'f': /* HVP — Horizontal Vertical Position */ - vt100_cur_row = defparam(0, 1) - 1; /* 1-based -> 0-based */ - vt100_cur_col = defparam(1, 1) - 1; - if (vt100_cur_row < 0) vt100_cur_row = 0; - if (vt100_cur_row >= ROWS) vt100_cur_row = ROWS - 1; - if (vt100_cur_col < 0) vt100_cur_col = 0; - if (vt100_cur_col >= COLS) vt100_cur_col = COLS - 1; - break; - case 'G': /* CHA — Cursor Horizontal Absolute */ - vt100_cur_col = defparam(0, 1) - 1; - if (vt100_cur_col < 0) vt100_cur_col = 0; - if (vt100_cur_col >= COLS) vt100_cur_col = COLS - 1; - break; - case 'd': /* VPA — Vertical Position Absolute */ - vt100_cur_row = defparam(0, 1) - 1; - if (vt100_cur_row < 0) vt100_cur_row = 0; - if (vt100_cur_row >= ROWS) vt100_cur_row = ROWS - 1; - break; - case 's': /* SCP — Save Cursor Position (ANSI) */ - vt100_saved_row = vt100_cur_row; - vt100_saved_col = vt100_cur_col; - vt100_saved_attr = vt100_cur_attr; - vt100_saved_fg = vt100_cur_fg; - vt100_saved_bg = vt100_cur_bg; - break; - case 'u': /* RCP — Restore Cursor Position (ANSI) */ - vt100_cur_row = clamp(vt100_saved_row, 0, ROWS - 1); - vt100_cur_col = clamp(vt100_saved_col, 0, COLS - 1); - vt100_cur_attr = vt100_saved_attr; - vt100_cur_fg = vt100_saved_fg; - vt100_cur_bg = vt100_saved_bg; - break; - - /* ── Erase ───────────────────────── */ - case 'J': /* ED — Erase Display */ - vt100_erase_display(defparam(0, 0)); - break; - case 'K': /* EL — Erase Line */ - vt100_erase_line(defparam(0, 0)); - break; - - /* ── Insert / delete ────────────────── */ - case 'L': /* IL — Insert Lines */ - vt100_insert_lines(defparam(0, 1)); - break; - case 'M': /* DL — Delete Lines (note: CSI M ≠ ESC M) */ - vt100_delete_lines(defparam(0, 1)); - break; - case '@': /* ICH — Insert Characters */ - vt100_insert_chars(defparam(0, 1)); - break; - case 'P': /* DCH — Delete Characters */ - vt100_delete_chars(defparam(0, 1)); - break; - - /* ── SGR — Select Graphic Rendition ── */ - case 'm': - for (int i = 0; i < vt100_param_count; i++) { - int val = vt100_params[i]; - if (val == 0) { - vt100_cur_attr = 0; - vt100_cur_fg = 7; /* white (rendered as green) */ - vt100_cur_bg = 0; - } else if (val == 1) { - vt100_cur_attr |= 1; /* ATTR_BOLD */ - } else if (val == 4) { - vt100_cur_attr |= 2; /* ATTR_UNDERLINE */ - } else if (val == 5) { - vt100_cur_attr |= 4; /* ATTR_BLINK */ - } else if (val == 7) { - vt100_cur_attr |= 8; /* ATTR_REVERSE */ - } else if (val == 22) { - vt100_cur_attr &= ~1; - } else if (val == 24) { - vt100_cur_attr &= ~2; - } else if (val == 25) { - vt100_cur_attr &= ~4; - } else if (val == 27) { - vt100_cur_attr &= ~8; - } else if (30 <= val && val <= 37) { - vt100_cur_fg = val - 30; - } else if (40 <= val && val <= 47) { - vt100_cur_bg = val - 40; - } else if (val == 39) { - vt100_cur_fg = 7; - } else if (val == 49) { - vt100_cur_bg = 0; - } - } - break; - - /* ── scrolling region ────────────────────── */ - case 'r': /* DECSTBM — Set Top and Bottom Margins */ - /* acknowledged but not fully implemented */ - break; - - /* ── Device control ────────────────────── */ - case 'c': /* DA — Device Attributes: reply with \033[?1;0c (VT100) */ - if (!pty_handle.empty()) { - const char *reply = "\033[?1;0c"; - pty_write( reply, strlen(reply)); - } - break; - case 'n': /* DSR — Device Status Report */ - fprintf(stderr, "[VT100-DBG] DSR query param[0]=%d\n", vt100_params[0]); - if (vt100_params[0] == 5) { - fprintf(stderr, "[VT100-DBG] DSR 5: reply \\033[0n (OK)\n"); - if (!pty_handle.empty()) pty_write( "\033[0n", 4); - } else if (vt100_params[0] == 6) { - /* Cursor Position Report */ - char buf[32]; - int len = snprintf(buf, sizeof(buf), "\033[%d;%dR", - vt100_cur_row + 1, vt100_cur_col + 1); - fprintf(stderr, "[VT100-DBG] DSR 6: cursor=(%d,%d) reply=%s\n", - vt100_cur_row + 1, vt100_cur_col + 1, buf); - if (!pty_handle.empty()) pty_write( buf, len); - } - break; - - /* ── Mode settings ────────────────────── */ - case 'h': /* SM — Set Mode (non-private) */ - case 'l': /* RM — Reset Mode (non-private) */ - break; - - default: - break; - } - } - - /* ================================================================== */ - /* Main byte-stream parser (full VT100 state machine, ported from src/vt100.c) */ - /* ================================================================== */ - void vt100_process_bytes(const char *data, int len) - { - /* ── DEBUG: dump raw PTY data ── */ - if (len > 0 && len < 256) { - fprintf(stderr, "[VT100-DBG] process_bytes len=%d hex=", len); - for (int di = 0; di < len && di < 80; di++) - fprintf(stderr, "%02X ", (unsigned char)data[di]); - fprintf(stderr, "\n"); - } - for (int i = 0; i < len; i++) - { - unsigned char c = (unsigned char)data[i]; - - /* ── string terminator detection ───────────── */ - /* skip_until_st during vt100_esc_state remains unchanged. - * OSC: BEL(0x07), ST(0x9C), or a bare backslash can terminate. - * DCS: only BEL or ST terminates; backslash is not a DCS terminator - * (DCS uses a two-byte ESC \ sequence). - * Therefore the vt100_esc_state == VT100_ST_OSC guard is required. */ - if (vt100_skip_until_st) { - if (c == 0x07 || c == 0x9C || - (c == '\\' && vt100_esc_state == VT100_ST_OSC)) { - vt100_skip_until_st = false; - vt100_esc_state = VT100_ST_NORMAL; - } - continue; - } - - switch (vt100_esc_state) { - - case VT100_ST_NORMAL: - if (c == 0x1B) { - vt100_esc_state = VT100_ST_ESC; - vt100_param_count = 0; - vt100_param_val = 0; - vt100_priv_mode = false; - vt100_sec_mode = false; - memset(vt100_params, 0, sizeof(vt100_params)); - } else { - vt100_put_char((char)c); - } - break; - - case VT100_ST_ESC: - if (c == '[') { - vt100_esc_state = VT100_ST_CSI; - } else if (c == ']') { - vt100_esc_state = VT100_ST_OSC; - vt100_skip_until_st = true; - } else if (c == 'P') { - vt100_esc_state = VT100_ST_DCS; - vt100_skip_until_st = true; - } else { - vt100_handle_esc((char)c); - vt100_esc_state = VT100_ST_NORMAL; - } - break; - - case VT100_ST_CSI: - if (c == '?') { - vt100_priv_mode = true; - vt100_esc_state = VT100_ST_CSI_QM; - } else if (c == '>') { - fprintf(stderr, "[VT100-DBG] CSI '>' prefix detected! sec_mode=1\n"); - vt100_sec_mode = true; - vt100_esc_state = VT100_ST_CSI_GT; - } else if (c >= '0' && c <= '9') { - vt100_param_val = vt100_param_val * 10 + (c - '0'); - } else if (c == ';') { - if (vt100_param_count < VT100_MAX_PARAMS) - vt100_params[vt100_param_count++] = vt100_param_val; - vt100_param_val = 0; - } else if (c >= 0x20 && c <= 0x2F) { - /* Intermediate byte — ignored */ - } else { - /* Final byte (0x40-0x7E) */ - if (vt100_param_count < VT100_MAX_PARAMS) - vt100_params[vt100_param_count++] = vt100_param_val; - vt100_handle_csi((char)c); - vt100_esc_state = VT100_ST_NORMAL; - } - break; - - case VT100_ST_CSI_QM: - if (c >= '0' && c <= '9') { - vt100_param_val = vt100_param_val * 10 + (c - '0'); - } else if (c == ';') { - if (vt100_param_count < VT100_MAX_PARAMS) - vt100_params[vt100_param_count++] = vt100_param_val; - vt100_param_val = 0; - } else if (c >= 0x20 && c <= 0x2F) { - /* intermediate */ - } else { - /* Final byte */ - if (vt100_param_count < VT100_MAX_PARAMS) - vt100_params[vt100_param_count++] = vt100_param_val; - vt100_handle_csi((char)c); - vt100_esc_state = VT100_ST_NORMAL; - } - break; - - case VT100_ST_CSI_GT: - /* ESC [> — Secondary DA / xterm extensions */ - if (c >= '0' && c <= '9') { - vt100_param_val = vt100_param_val * 10 + (c - '0'); - } else if (c == ';') { - if (vt100_param_count < VT100_MAX_PARAMS) - vt100_params[vt100_param_count++] = vt100_param_val; - vt100_param_val = 0; - } else if (c >= 0x20 && c <= 0x2F) { - /* intermediate */ - } else { - /* Final byte */ - if (vt100_param_count < VT100_MAX_PARAMS) - vt100_params[vt100_param_count++] = vt100_param_val; - vt100_handle_csi((char)c); - vt100_esc_state = VT100_ST_NORMAL; - } - break; - - case VT100_ST_OSC: - case VT100_ST_DCS: - /* Handled by skip_until_st at top of loop */ - vt100_esc_state = VT100_ST_NORMAL; - break; - - default: - vt100_esc_state = VT100_ST_NORMAL; - break; - } - } - } - - /* ================================================================== */ - /* Line-level rendering: call lv_label_set_text only when that row changes */ - /* ================================================================== */ - static inline char sanitize_ch(char ch) - { - unsigned char c = (unsigned char)ch; - if (c < 32 || c > 126) - return ' '; - return (char)c; - } - - void vt100_render_row(int r) - { - if (r < 0 || r >= ROWS) - return; - - char buf[COLS + 1]; - for (int c = 0; c < COLS; c++) - buf[c] = sanitize_ch(vt100_screen[r][c]); - buf[COLS] = '\0'; - - if (memcmp(buf, row_rendered[r], COLS + 1) == 0) - return; /* row unchanged, skip */ - - memcpy(row_rendered[r], buf, COLS + 1); - lv_label_set_text(row_labels[r], buf); - } - - void vt100_render_all() - { - for (int r = 0; r < ROWS; r++) - vt100_render_row(r); - update_cursor_position_only(); - } - - /** Only update the cursor label position and character; do not change show/hide state */ - void update_cursor_position_only() - { - if (!cursor_label) - return; - int row = vt100_cur_row; - int col = vt100_cur_col; - if (row < 0) row = 0; - if (row >= ROWS) row = ROWS - 1; - if (col < 0) col = 0; - if (col >= COLS) col = COLS - 1; - - char under = sanitize_ch(vt100_screen[row][col]); - char s[2] = {under == ' ' ? ' ' : under, '\0'}; - const char *old = lv_label_get_text(cursor_label); - if (!old || old[0] != s[0] || old[1] != s[1]) - lv_label_set_text(cursor_label, s); - - lv_obj_set_pos(cursor_label, col * CHAR_W, row * CHAR_H); - } - - void show_cursor(bool show) - { - if (!cursor_label) - return; - if (show) - { - if (lv_obj_has_flag(cursor_label, LV_OBJ_FLAG_HIDDEN)) - lv_obj_clear_flag(cursor_label, LV_OBJ_FLAG_HIDDEN); - } - else - { - if (!lv_obj_has_flag(cursor_label, LV_OBJ_FLAG_HIDDEN)) - lv_obj_add_flag(cursor_label, LV_OBJ_FLAG_HIDDEN); - } - vt100_cursor_vis = show; - } - - /* ================================================================== */ - /* PTY management */ - /* ================================================================== */ - std::string pty_open(const std::string &cmd, const std::vector &args) - { - int code = -1; - std::string handle; - std::list api_args = { - "Open", - cmd, - std::to_string(COLS), - std::to_string(ROWS), - cmd, - }; - for (const auto &arg : args) - api_args.push_back(arg); - - cp0_signal_pty_api(std::move(api_args), [&](int c, std::string data) { - code = c; - if (code == 0) handle = std::move(data); - }); - return handle; - } - - int pty_read(char *buf, size_t buf_size) - { - int code = -1; - std::string data; - cp0_signal_pty_api({"Read", pty_handle, std::to_string(buf_size)}, [&](int c, std::string d) { - code = c; - data = std::move(d); - }); - if (code < 0) return -1; - - size_t n = data.size() < buf_size ? data.size() : buf_size; - if (n > 0 && buf) - memcpy(buf, data.data(), n); - return (int)n; - } - - int pty_write(const char *buf, size_t len) - { - if (pty_handle.empty() || !buf) return -1; - int code = -1; - cp0_signal_pty_api({"Write", pty_handle, std::string(buf, len)}, [&](int c, std::string) { - code = c; - }); - return code; - } - - int pty_check_child(int *status) - { - int code = -1; - std::string data; - cp0_signal_pty_api({"CheckChild", pty_handle}, [&](int c, std::string d) { - code = c; - data = std::move(d); - }); - if (status) *status = atoi(data.c_str()); - return code; - } - - bool start_pty(const std::string &cmd, const std::vector &args = {}) - { - stop_pty(); - pty_handle = pty_open(cmd, args); - return !pty_handle.empty(); - } - - void stop_pty() - { - if (!pty_handle.empty()) - { - cp0_signal_pty_api({"Close", pty_handle}, nullptr); - pty_handle.clear(); - } - } - - /* ================================================================== */ - /* Timer callback */ - /* ================================================================== */ - void vt100_poll_cb(lv_timer_t *t) - { - (void)t; - if (pty_handle.empty() || !terminal_active) - return; - - char buf[1024]; - int n; - bool changed = false; - - while ((n = pty_read( buf, sizeof(buf))) > 0) - { - vt100_process_bytes(buf, n); - changed = true; - } - - if (changed) - vt100_render_all(); - - bool child_exited = false; - if (n < 0) - { - child_exited = true; - } - else if (!pty_handle.empty()) - { - int status = 0; - if (pty_check_child( &status) == 1) - child_exited = true; - } - - if (child_exited) - { - terminal_active = false; - const char *hint = "\r\n-- Press any key to exit --"; - vt100_process_bytes(hint, (int)strlen(hint)); - vt100_render_all(); - waiting_key_to_exit = true; - stop_pty(); - } - } - - void vt100_cursor_blink_cb(lv_timer_t *t) - { - (void)t; - if (!cursor_label) - return; - - update_cursor_position_only(); - - if (!terminal_active) - { - show_cursor(false); - return; - } - - /* Honor the DECTCEM cursor visibility setting */ - if (!vt100_cursor_visible_flag) - { - show_cursor(false); - return; - } - - show_cursor(!vt100_cursor_vis); - } - - /* ================================================================== */ - /* Key handling */ - /* ================================================================== */ - - /** - * Convert physical keyboard input (evdev keycode + utf8) to terminal byte sequences and write them to the PTY. - * evdev keycode reference (linux/input-event-codes.h): - * KEY_ESC=1 KEY_BACKSPACE=14 KEY_ENTER=28 - * KEY_UP=103 KEY_LEFT=105 KEY_RIGHT=106 KEY_DOWN=108 - */ - void write_key_to_pty(uint32_t evdev_key, const char *utf8_str) - { - if (!terminal_active || pty_handle.empty()) - return; - char buf[8]; - int len = 0; - - switch (evdev_key) - { - case 28: - buf[0] = '\r'; len = 1; break; /* KEY_ENTER */ - case 14: - buf[0] = 0x7f; len = 1; break; /* KEY_BACKSPACE */ - case 1: - buf[0] = 0x1b; len = 1; break; /* KEY_ESC */ - case 103: - /* KEY_UP: normal=\033[A, application=\033OA */ - if (vt100_decckm) { - buf[0]=0x1b; buf[1]='O'; buf[2]='A'; len=3; - } else { - buf[0]=0x1b; buf[1]='['; buf[2]='A'; len=3; - } - break; - case 108: - if (vt100_decckm) { - buf[0]=0x1b; buf[1]='O'; buf[2]='B'; len=3; - } else { - buf[0]=0x1b; buf[1]='['; buf[2]='B'; len=3; - } - break; - case 106: - if (vt100_decckm) { - buf[0]=0x1b; buf[1]='O'; buf[2]='C'; len=3; - } else { - buf[0]=0x1b; buf[1]='['; buf[2]='C'; len=3; - } - break; - case 105: - if (vt100_decckm) { - buf[0]=0x1b; buf[1]='O'; buf[2]='D'; len=3; - } else { - buf[0]=0x1b; buf[1]='['; buf[2]='D'; len=3; - } - break; - default: - len = (int)strlen(utf8_str); - if (len > 0 && len <= (int)sizeof(buf)) - memcpy(buf, utf8_str, (size_t)len); - else - len = 0; - break; - } - - if (len > 0) { - fprintf(stderr, "[VT100-DBG] write_key evdev=%u decckm=%d len=%d hex=", - evdev_key, vt100_decckm, len); - for (int ki = 0; ki < len; ki++) - fprintf(stderr, "%02X ", (unsigned char)buf[ki]); - fprintf(stderr, "\n"); - pty_write( buf, (size_t)len); - } - } - -}; diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp index 92fbc75f..01d1f88d 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp @@ -7,7 +7,7 @@ #pragma once #include "sample_log.h" #include "../ui_app_page.hpp" -#include "ui_app_console.hpp" +#include "ui_app_st.hpp" #include "compat/input_keys.h" #include #include @@ -21,7 +21,7 @@ // // Views: // VIEW_INPUT -- Host/Port/User input fields -// VIEW_TERMINAL -- Embedded UIConsolePage running ssh +// VIEW_TERMINAL -- Embedded UISTPage running ssh // ============================================================ class UISSHPage : public AppPage @@ -57,7 +57,7 @@ class UISSHPage : public AppPage std::vector fields_; int active_field_ = 0; ViewState view_state_ = ViewState::INPUT; - std::shared_ptr console_page_; + std::shared_ptr console_page_; // ==================== keycode to char ==================== static char keycode_to_char(uint32_t key) @@ -229,8 +229,8 @@ class UISSHPage : public AppPage SLOGI("[SSH] Launching: %s", cmd.c_str()); - // Create console page - console_page_ = std::make_shared(); + // Create terminal page + console_page_ = std::make_shared(); // Restore the SSH input view when the embedded console exits. console_page_->navigate_home = [this]() { @@ -265,7 +265,7 @@ class UISSHPage : public AppPage void event_handler(lv_event_t *e) { - // Only handle input view events; terminal view is handled by UIConsolePage + // Only handle input view events; terminal view is handled by UISTPage if (view_state_ != ViewState::INPUT) return; if (launcher_ui::events::is_key_released(e)) diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_st.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_st.hpp new file mode 100644 index 00000000..6ceb33b4 --- /dev/null +++ b/projects/APPLaunch/main/ui/page_app/ui_app_st.hpp @@ -0,0 +1,1703 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "sample_log.h" +#include "../ui_app_page.hpp" +#include "cp0_lvgl_app.h" +#include "compat/input_keys.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +LV_FONT_DECLARE(ui_font_liberation_mono_11) +} + +// ============================================================ +// ST terminal UISTPage +// Screen resolution: 320 x 170 (top bar 20px, ui_APP_Container 320x150) +// +// This is a standalone terminal page. It does not inherit from or include +// the existing console page. The structure follows suckless st's split: +// - terminal state: glyph grid, cursor, modes, scroll region +// - byte stream parser: ESC/CSI/OSC handling +// - frontend: LVGL row renderer and keyboard -> PTY writer +// ============================================================ +class UISTPage : public AppPage +{ + static constexpr int TERM_W = 320; + static constexpr int TERM_H = 150; + static constexpr int CHAR_W = 7; + static constexpr int CHAR_H = 15; + static constexpr int TEXT_Y_PAD = 2; + static constexpr int NORMAL_COLS = TERM_W / CHAR_W; + static constexpr int NORMAL_ROWS = TERM_H / CHAR_H; + static constexpr int BIG_COLS = 80; + static constexpr int BIG_ROWS = 24; + static constexpr int BIG_BOTTOM_H = 15; + static constexpr int BIG_VIEW_ROWS = (TERM_H - BIG_BOTTOM_H) / CHAR_H; + static constexpr int MAX_COLS = BIG_COLS; + static constexpr int MAX_ROWS = BIG_ROWS; + static constexpr int COLS = NORMAL_COLS; + static constexpr int ROWS = NORMAL_ROWS; + static constexpr int SCROLLBACK_MAX_ROWS = 200; + static constexpr int SCROLLBAR_W = 3; + static constexpr int BOTTOM_BAR_SLOTS = 5; + + static constexpr uint32_t DEFAULT_FG = 7; + static constexpr uint32_t DEFAULT_BG = 0; + + enum GlyphAttr : uint16_t { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, + ATTR_FAINT = 1 << 1, + ATTR_UNDERLINE = 1 << 2, + ATTR_BLINK = 1 << 3, + ATTR_REVERSE = 1 << 4, + ATTR_INVISIBLE = 1 << 5, + }; + + enum TermMode : uint16_t { + MODE_WRAP = 1 << 0, + MODE_INSERT = 1 << 1, + MODE_APPCURSOR = 1 << 2, + }; + + enum class ParseState { + Normal, + Esc, + Csi, + Osc, + Charset, + }; + + struct Glyph { + uint32_t u = ' '; + uint16_t attr = ATTR_NULL; + uint32_t fg = DEFAULT_FG; + uint32_t bg = DEFAULT_BG; + }; + + struct Cursor { + int x = 0; + int y = 0; + Glyph attr; + }; + + struct RenderSegment { + lv_obj_t *label = nullptr; + std::string text; + int x = 0; + int width = 0; + uint32_t fg = UINT32_MAX; + uint32_t bg = UINT32_MAX; + bool hidden = true; + }; + + struct SegmentData { + std::string text; + int x = 0; + uint32_t fg = DEFAULT_FG; + uint32_t bg = DEFAULT_BG; + }; + +public: + bool terminal_sysplause = true; + + UISTPage() : AppPage() + { + set_page_title("ST"); + reset_terminal(); + create_ui(); + bind_events(); + start_shell(); + } + + ~UISTPage() + { + terminal_active_ = false; + stop_timers(); + stop_pty(); + } + + void exec(std::string cmd) + { + stop_timers(); + stop_pty(); + + terminal_active_ = true; + waiting_key_to_exit_ = false; + big_mode_ = false; + term_cols_ = NORMAL_COLS; + term_rows_ = NORMAL_ROWS; + viewport_x_ = 0; + viewport_y_ = 0; + big_view_locked_ = false; + reset_terminal(); + update_big_mode_ui(); + render_all(); + + std::vector tokens; + std::istringstream iss(cmd); + std::string token; + while (iss >> token) + tokens.push_back(token); + + if (tokens.empty()) { + const char *err = "Error: empty command\r\n"; + process_bytes(err, (int)strlen(err)); + render_all(); + terminal_active_ = false; + waiting_key_to_exit_ = true; + return; + } + + std::list args; + for (size_t i = 1; i < tokens.size(); ++i) + args.push_back(tokens[i]); + + start_command(tokens[0], args, tokens[0].c_str(), "Error: openpty/fork failed\r\n"); + } + +private: + std::array, MAX_ROWS> screen_{}; + std::array, ROWS> row_segments_{}; + std::array dirty_{}; + std::vector> scrollback_; + int scrollback_offset_ = 0; + int term_cols_ = NORMAL_COLS; + int term_rows_ = NORMAL_ROWS; + int viewport_x_ = 0; + int viewport_y_ = 0; + bool big_view_locked_ = false; + Cursor cursor_{}; + Cursor saved_cursor_{}; + int scroll_top_ = 0; + int scroll_bot_ = NORMAL_ROWS - 1; + uint16_t mode_ = MODE_WRAP; + + ParseState parse_state_ = ParseState::Normal; + bool csi_private_ = false; + bool csi_secondary_ = false; + int csi_params_[16] = {}; + int csi_param_count_ = 0; + int csi_param_value_ = 0; + bool csi_have_value_ = false; + + lv_obj_t *terminal_container_ = nullptr; + lv_obj_t *term_canvas_ = nullptr; + lv_obj_t *scrollbar_track_ = nullptr; + lv_obj_t *scrollbar_thumb_ = nullptr; + lv_obj_t *hscrollbar_track_ = nullptr; + lv_obj_t *hscrollbar_thumb_ = nullptr; + std::array bottom_labels_{}; + std::array bottom_indicators_{}; + const lv_font_t *mono_font_ = nullptr; + lv_obj_t *cursor_label_ = nullptr; + lv_timer_t *poll_timer_ = nullptr; + lv_timer_t *cursor_timer_ = nullptr; + + std::string pty_handle_; + bool terminal_active_ = false; + bool waiting_key_to_exit_ = false; + bool cursor_blink_visible_ = false; + bool cursor_visible_mode_ = true; + bool shift_down_ = false; + bool big_mode_ = false; + int home_hold_status_ = 0; + std::chrono::time_point home_hold_start_{}; + + static int clamp(int v, int lo, int hi) + { + if (v < lo) + return lo; + if (v > hi) + return hi; + return v; + } + + static char printable(uint32_t u) + { + if (u < 32 || u == 127) + return ' '; + if (u > 126) + return '?'; + return (char)u; + } + + static lv_color_t palette(uint32_t color) + { + static const uint32_t colors[] = { + 0x0D1117, 0xFF5F56, 0x27C93F, 0xFFBD2E, + 0x2F81F7, 0xBC8CFF, 0x39C5CF, 0xF0F6FC, + 0x6E7681, 0xFFA198, 0x56D364, 0xE3B341, + 0x79C0FF, 0xD2A8FF, 0x56D4DD, 0xFFFFFF, + }; + return lv_color_hex(colors[color < 16 ? color : DEFAULT_FG]); + } + + static const lv_font_t *terminal_font() + { + return &ui_font_liberation_mono_11; + } + + static uint32_t xterm256_to_palette(int color) + { + color = clamp(color, 0, 255); + if (color < 16) + return (uint32_t)color; + if (color >= 232) + return color >= 244 ? 15 : 8; + + int idx = color - 16; + int r = idx / 36; + int g = (idx / 6) % 6; + int b = idx % 6; + if (r >= g && r >= b) + return r >= 3 ? 9 : 1; + if (g >= r && g >= b) + return g >= 3 ? 10 : 2; + return b >= 3 ? 12 : 4; + } + + static uint32_t rgb_to_palette(int r, int g, int b) + { + r = clamp(r, 0, 255); + g = clamp(g, 0, 255); + b = clamp(b, 0, 255); + int maxc = std::max(r, std::max(g, b)); + int minc = std::min(r, std::min(g, b)); + if (maxc < 80) + return 0; + if (maxc - minc < 35) + return maxc > 180 ? 15 : 8; + if (r >= g && r >= b) + return r > 180 ? 9 : 1; + if (g >= r && g >= b) + return g > 180 ? 10 : 2; + return b > 180 ? 12 : 4; + } + + Glyph blank_glyph() const + { + Glyph g; + g.u = ' '; + g.attr = cursor_.attr.attr; + g.fg = cursor_.attr.fg; + g.bg = cursor_.attr.bg; + return g; + } + + void dirty_row(int row) + { + if (big_mode_) { + int view_row = row - viewport_y_; + if (view_row >= 0 && view_row < visible_rows()) + dirty_[view_row] = true; + return; + } + if (row >= 0 && row < ROWS) + dirty_[row] = true; + } + + void dirty_all() + { + dirty_.fill(true); + for (auto &segments : row_segments_) { + for (auto &segment : segments) { + segment.text.clear(); + segment.fg = UINT32_MAX; + segment.bg = UINT32_MAX; + segment.hidden = true; + if (segment.label) + lv_obj_add_flag(segment.label, LV_OBJ_FLAG_HIDDEN); + } + } + } + + int visible_cols() const + { + return NORMAL_COLS; + } + + int visible_rows() const + { + return big_mode_ ? BIG_VIEW_ROWS : NORMAL_ROWS; + } + + int visible_h() const + { + return visible_rows() * CHAR_H; + } + + int max_viewport_x() const + { + return std::max(0, term_cols_ - visible_cols()); + } + + int max_viewport_y() const + { + return std::max(0, term_rows_ - visible_rows()); + } + + void append_scrollback_row(const std::array &row) + { + scrollback_.push_back(row); + if ((int)scrollback_.size() > SCROLLBACK_MAX_ROWS) { + int drop = (int)scrollback_.size() - SCROLLBACK_MAX_ROWS; + scrollback_.erase(scrollback_.begin(), scrollback_.begin() + drop); + scrollback_offset_ = std::max(0, scrollback_offset_ - drop); + } + } + + const std::array &display_row(int r) const + { + static const std::array empty_row{}; + if (big_mode_) { + int y = clamp(viewport_y_ + r, 0, term_rows_ - 1); + return screen_[(size_t)y]; + } + int history_rows = (int)scrollback_.size(); + int total_rows = history_rows + term_rows_; + int top = total_rows - term_rows_ - scrollback_offset_; + int idx = top + r; + if (idx < 0 || idx >= total_rows) + return empty_row; + if (idx < history_rows) + return scrollback_[(size_t)idx]; + return screen_[(size_t)(idx - history_rows)]; + } + + void scrollback_page(int direction) + { + int old_offset = scrollback_offset_; + int page = std::max(1, visible_rows() - 1); + if (direction > 0) + scrollback_offset_ = std::min((int)scrollback_.size(), scrollback_offset_ + page); + else + scrollback_offset_ = std::max(0, scrollback_offset_ - page); + if (scrollback_offset_ != old_offset) + dirty_all(); + } + + void update_scrollbar() + { + if (!scrollbar_track_ || !scrollbar_thumb_) + return; + + if (big_mode_) { + int max_y = max_viewport_y(); + lv_obj_clear_flag(scrollbar_track_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(scrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_size(scrollbar_track_, SCROLLBAR_W, visible_h()); + lv_obj_set_pos(scrollbar_track_, TERM_W - SCROLLBAR_W - 1, 0); + + int thumb_h = std::max(8, visible_h() * visible_rows() / std::max(term_rows_, 1)); + thumb_h = std::min(visible_h(), thumb_h); + int range = visible_h() - thumb_h; + int thumb_y = max_y > 0 ? viewport_y_ * range / max_y : 0; + lv_obj_set_size(scrollbar_thumb_, SCROLLBAR_W, thumb_h); + lv_obj_set_pos(scrollbar_thumb_, TERM_W - SCROLLBAR_W - 1, thumb_y); + lv_obj_move_foreground(scrollbar_track_); + lv_obj_move_foreground(scrollbar_thumb_); + return; + } + + int history_rows = (int)scrollback_.size(); + if (history_rows <= 0) { + lv_obj_add_flag(scrollbar_track_, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(scrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + return; + } + + lv_obj_clear_flag(scrollbar_track_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(scrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_size(scrollbar_track_, SCROLLBAR_W, TERM_H); + lv_obj_set_pos(scrollbar_track_, TERM_W - SCROLLBAR_W - 1, 0); + + int total_rows = history_rows + term_rows_; + int thumb_h = std::max(8, TERM_H * term_rows_ / std::max(total_rows, 1)); + thumb_h = std::min(TERM_H, thumb_h); + int range = TERM_H - thumb_h; + int max_offset = std::max(1, history_rows); + int thumb_y = range - (scrollback_offset_ * range / max_offset); + + lv_obj_set_size(scrollbar_thumb_, SCROLLBAR_W, thumb_h); + lv_obj_set_pos(scrollbar_thumb_, TERM_W - SCROLLBAR_W - 1, thumb_y); + lv_obj_move_foreground(scrollbar_track_); + lv_obj_move_foreground(scrollbar_thumb_); + } + + void update_hscrollbar() + { + if (!hscrollbar_track_ || !hscrollbar_thumb_) + return; + if (!big_mode_) { + lv_obj_add_flag(hscrollbar_track_, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(hscrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + return; + } + + lv_obj_clear_flag(hscrollbar_track_, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(hscrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + int y = visible_h(); + int width = TERM_W - SCROLLBAR_W - 2; + lv_obj_set_size(hscrollbar_track_, width, 3); + lv_obj_set_pos(hscrollbar_track_, 0, y); + + int max_x = max_viewport_x(); + int thumb_w = std::max(18, width * visible_cols() / std::max(term_cols_, 1)); + thumb_w = std::min(width, thumb_w); + int range = width - thumb_w; + int thumb_x = max_x > 0 ? viewport_x_ * range / max_x : 0; + lv_obj_set_size(hscrollbar_thumb_, thumb_w, 3); + lv_obj_set_pos(hscrollbar_thumb_, thumb_x, y); + lv_obj_move_foreground(hscrollbar_track_); + lv_obj_move_foreground(hscrollbar_thumb_); + } + + void set_bottom_label(int idx, const char *text) + { + if (idx < 0 || idx >= BOTTOM_BAR_SLOTS || !bottom_labels_[(size_t)idx]) + return; + lv_label_set_text(bottom_labels_[(size_t)idx], text); + } + + void update_big_mode_ui() + { + bool show = big_mode_; + for (auto *label : bottom_labels_) { + if (!label) + continue; + if (show) + lv_obj_clear_flag(label, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); + } + for (auto *label : bottom_indicators_) { + if (!label) + continue; + if (show) + lv_obj_clear_flag(label, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); + } + update_hscrollbar(); + update_scrollbar(); + } + + void switch_big_mode(bool enable) + { + if (big_mode_ == enable) + return; + + terminal_active_ = false; + stop_timers(); + stop_pty(); + big_mode_ = enable; + term_cols_ = big_mode_ ? BIG_COLS : NORMAL_COLS; + term_rows_ = big_mode_ ? BIG_ROWS : NORMAL_ROWS; + viewport_x_ = 0; + viewport_y_ = 0; + big_view_locked_ = false; + reset_terminal(); + update_big_mode_ui(); + start_shell(); + } + + void leave_scrollback() + { + if (scrollback_offset_ == 0) + return; + scrollback_offset_ = 0; + dirty_all(); + } + + void pan_big_view(int dx, int dy) + { + if (!big_mode_) + return; + big_view_locked_ = true; + int old_x = viewport_x_; + int old_y = viewport_y_; + viewport_x_ = clamp(viewport_x_ + dx, 0, max_viewport_x()); + viewport_y_ = clamp(viewport_y_ + dy, 0, max_viewport_y()); + if (viewport_x_ != old_x || viewport_y_ != old_y) + dirty_all(); + } + + void follow_cursor_in_big_mode() + { + if (!big_mode_ || big_view_locked_) + return; + int old_x = viewport_x_; + int old_y = viewport_y_; + if (cursor_.x < viewport_x_) + viewport_x_ = cursor_.x; + else if (cursor_.x >= viewport_x_ + visible_cols()) + viewport_x_ = cursor_.x - visible_cols() + 1; + + if (cursor_.y < viewport_y_) + viewport_y_ = cursor_.y; + else if (cursor_.y >= viewport_y_ + visible_rows()) + viewport_y_ = cursor_.y - visible_rows() + 1; + + viewport_x_ = clamp(viewport_x_, 0, max_viewport_x()); + viewport_y_ = clamp(viewport_y_, 0, max_viewport_y()); + if (viewport_x_ != old_x || viewport_y_ != old_y) + dirty_all(); + } + + void reset_terminal() + { + cursor_ = Cursor{}; + cursor_.attr.fg = DEFAULT_FG; + cursor_.attr.bg = DEFAULT_BG; + saved_cursor_ = cursor_; + scroll_top_ = 0; + scroll_bot_ = term_rows_ - 1; + scrollback_.clear(); + scrollback_offset_ = 0; + mode_ = MODE_WRAP; + parse_state_ = ParseState::Normal; + cursor_visible_mode_ = true; + csi_reset(); + for (auto &row : screen_) + for (auto &cell : row) + cell = blank_glyph(); + dirty_all(); + } + + void clear_region(int x1, int y1, int x2, int y2) + { + x1 = clamp(x1, 0, term_cols_ - 1); + x2 = clamp(x2, 0, term_cols_ - 1); + y1 = clamp(y1, 0, term_rows_ - 1); + y2 = clamp(y2, 0, term_rows_ - 1); + if (x1 > x2) + std::swap(x1, x2); + if (y1 > y2) + std::swap(y1, y2); + + Glyph blank = blank_glyph(); + for (int y = y1; y <= y2; ++y) { + for (int x = x1; x <= x2; ++x) + screen_[y][x] = blank; + dirty_row(y); + } + } + + void move_to(int x, int y) + { + cursor_.x = clamp(x, 0, term_cols_ - 1); + cursor_.y = clamp(y, 0, term_rows_ - 1); + } + + void scroll_up(int top, int bot, int n) + { + top = clamp(top, 0, term_rows_ - 1); + bot = clamp(bot, 0, term_rows_ - 1); + if (top > bot || n <= 0) + return; + n = std::min(n, bot - top + 1); + if (!big_mode_ && top == 0 && bot == term_rows_ - 1) { + for (int y = 0; y < n; ++y) + append_scrollback_row(screen_[y]); + } + for (int y = top; y <= bot - n; ++y) { + screen_[y] = screen_[y + n]; + dirty_row(y); + } + Glyph blank = blank_glyph(); + for (int y = bot - n + 1; y <= bot; ++y) { + for (auto &cell : screen_[y]) + cell = blank; + dirty_row(y); + } + } + + void scroll_down(int top, int bot, int n) + { + top = clamp(top, 0, term_rows_ - 1); + bot = clamp(bot, 0, term_rows_ - 1); + if (top > bot || n <= 0) + return; + n = std::min(n, bot - top + 1); + for (int y = bot; y >= top + n; --y) { + screen_[y] = screen_[y - n]; + dirty_row(y); + } + Glyph blank = blank_glyph(); + for (int y = top; y < top + n; ++y) { + for (auto &cell : screen_[y]) + cell = blank; + dirty_row(y); + } + } + + void newline(bool first_col) + { + if (first_col) + cursor_.x = 0; + if (cursor_.y == scroll_bot_) + scroll_up(scroll_top_, scroll_bot_, 1); + else + cursor_.y = clamp(cursor_.y + 1, 0, term_rows_ - 1); + } + + void put_tab() + { + int next = ((cursor_.x / 8) + 1) * 8; + cursor_.x = clamp(next, 0, term_cols_ - 1); + } + + void insert_blank(int n) + { + n = std::max(n, 1); + n = std::min(n, term_cols_ - cursor_.x); + auto &line = screen_[cursor_.y]; + for (int x = term_cols_ - 1; x >= cursor_.x + n; --x) + line[x] = line[x - n]; + Glyph blank = blank_glyph(); + for (int x = cursor_.x; x < cursor_.x + n; ++x) + line[x] = blank; + dirty_row(cursor_.y); + } + + void delete_chars(int n) + { + n = std::max(n, 1); + n = std::min(n, term_cols_ - cursor_.x); + auto &line = screen_[cursor_.y]; + for (int x = cursor_.x; x < term_cols_ - n; ++x) + line[x] = line[x + n]; + Glyph blank = blank_glyph(); + for (int x = term_cols_ - n; x < term_cols_; ++x) + line[x] = blank; + dirty_row(cursor_.y); + } + + void put_rune(uint32_t rune) + { + if (mode_ & MODE_INSERT) + insert_blank(1); + + if (cursor_.x >= term_cols_) { + if (mode_ & MODE_WRAP) { + cursor_.x = 0; + newline(false); + } else { + cursor_.x = term_cols_ - 1; + } + } + + Glyph g = cursor_.attr; + g.u = rune; + screen_[cursor_.y][cursor_.x] = g; + dirty_row(cursor_.y); + cursor_.x++; + } + + void control_code(uint8_t c) + { + switch (c) { + case '\t': + put_tab(); + break; + case '\b': + cursor_.x = std::max(0, cursor_.x - 1); + break; + case '\r': + cursor_.x = 0; + break; + case '\n': + case '\v': + case '\f': + newline(true); + break; + case 0x0e: + case 0x0f: + break; + default: + break; + } + } + + int param(int index, int def) const + { + if (index >= csi_param_count_) + return def; + return csi_params_[index] == 0 ? def : csi_params_[index]; + } + + void csi_reset() + { + csi_private_ = false; + csi_secondary_ = false; + csi_param_count_ = 0; + csi_param_value_ = 0; + csi_have_value_ = false; + memset(csi_params_, 0, sizeof(csi_params_)); + } + + void csi_push_param() + { + if (csi_param_count_ >= (int)(sizeof(csi_params_) / sizeof(csi_params_[0]))) + return; + csi_params_[csi_param_count_++] = csi_have_value_ ? csi_param_value_ : 0; + csi_param_value_ = 0; + csi_have_value_ = false; + } + + void set_sgr() + { + if (csi_param_count_ == 0) { + cursor_.attr.attr = ATTR_NULL; + cursor_.attr.fg = DEFAULT_FG; + cursor_.attr.bg = DEFAULT_BG; + return; + } + + for (int i = 0; i < csi_param_count_; ++i) { + int val = csi_params_[i]; + if (val == 0) { + cursor_.attr.attr = ATTR_NULL; + cursor_.attr.fg = DEFAULT_FG; + cursor_.attr.bg = DEFAULT_BG; + } else if (val == 1) { + cursor_.attr.attr |= ATTR_BOLD; + } else if (val == 2) { + cursor_.attr.attr |= ATTR_FAINT; + } else if (val == 4) { + cursor_.attr.attr |= ATTR_UNDERLINE; + } else if (val == 5) { + cursor_.attr.attr |= ATTR_BLINK; + } else if (val == 7) { + cursor_.attr.attr |= ATTR_REVERSE; + } else if (val == 8) { + cursor_.attr.attr |= ATTR_INVISIBLE; + } else if (val == 22) { + cursor_.attr.attr &= ~(ATTR_BOLD | ATTR_FAINT); + } else if (val == 24) { + cursor_.attr.attr &= ~ATTR_UNDERLINE; + } else if (val == 25) { + cursor_.attr.attr &= ~ATTR_BLINK; + } else if (val == 27) { + cursor_.attr.attr &= ~ATTR_REVERSE; + } else if (val == 28) { + cursor_.attr.attr &= ~ATTR_INVISIBLE; + } else if (val >= 30 && val <= 37) { + cursor_.attr.fg = (uint32_t)(val - 30); + } else if (val >= 40 && val <= 47) { + cursor_.attr.bg = (uint32_t)(val - 40); + } else if (val >= 90 && val <= 97) { + cursor_.attr.fg = (uint32_t)(val - 90 + 8); + } else if (val >= 100 && val <= 107) { + cursor_.attr.bg = (uint32_t)(val - 100 + 8); + } else if ((val == 38 || val == 48) && i + 2 < csi_param_count_ && csi_params_[i + 1] == 5) { + uint32_t mapped = xterm256_to_palette(csi_params_[i + 2]); + if (val == 38) + cursor_.attr.fg = mapped; + else + cursor_.attr.bg = mapped; + i += 2; + } else if ((val == 38 || val == 48) && i + 4 < csi_param_count_ && csi_params_[i + 1] == 2) { + uint32_t mapped = rgb_to_palette(csi_params_[i + 2], csi_params_[i + 3], csi_params_[i + 4]); + if (val == 38) + cursor_.attr.fg = mapped; + else + cursor_.attr.bg = mapped; + i += 4; + } else if (val == 39) { + cursor_.attr.fg = DEFAULT_FG; + } else if (val == 49) { + cursor_.attr.bg = DEFAULT_BG; + } + } + } + + void handle_private_mode(char final) + { + bool set = final == 'h'; + for (int i = 0; i < csi_param_count_; ++i) { + switch (csi_params_[i]) { + case 1: + if (set) + mode_ |= MODE_APPCURSOR; + else + mode_ &= ~MODE_APPCURSOR; + break; + case 7: + if (set) + mode_ |= MODE_WRAP; + else + mode_ &= ~MODE_WRAP; + break; + case 25: + cursor_visible_mode_ = set; + break; + case 1049: + if (set) + clear_region(0, 0, term_cols_ - 1, term_rows_ - 1); + break; + default: + break; + } + } + } + + void handle_csi(char final) + { + if (csi_secondary_) { + if (final == 'c') + pty_write("\033[>0;115;0c", 11); + return; + } + + if (csi_private_ && (final == 'h' || final == 'l')) { + handle_private_mode(final); + return; + } + + switch (final) { + case '@': + insert_blank(param(0, 1)); + break; + case 'A': + move_to(cursor_.x, cursor_.y - param(0, 1)); + break; + case 'B': + move_to(cursor_.x, cursor_.y + param(0, 1)); + break; + case 'C': + move_to(cursor_.x + param(0, 1), cursor_.y); + break; + case 'D': + move_to(cursor_.x - param(0, 1), cursor_.y); + break; + case 'G': + move_to(param(0, 1) - 1, cursor_.y); + break; + case 'H': + case 'f': + move_to(param(1, 1) - 1, param(0, 1) - 1); + break; + case 'J': + if (param(0, 0) == 0) + clear_region(cursor_.x, cursor_.y, term_cols_ - 1, term_rows_ - 1); + else if (param(0, 0) == 1) + clear_region(0, 0, cursor_.x, cursor_.y); + else + clear_region(0, 0, term_cols_ - 1, term_rows_ - 1); + break; + case 'K': + if (param(0, 0) == 0) + clear_region(cursor_.x, cursor_.y, term_cols_ - 1, cursor_.y); + else if (param(0, 0) == 1) + clear_region(0, cursor_.y, cursor_.x, cursor_.y); + else + clear_region(0, cursor_.y, term_cols_ - 1, cursor_.y); + break; + case 'L': + scroll_down(cursor_.y, scroll_bot_, param(0, 1)); + break; + case 'M': + scroll_up(cursor_.y, scroll_bot_, param(0, 1)); + break; + case 'P': + delete_chars(param(0, 1)); + break; + case 'd': + move_to(cursor_.x, param(0, 1) - 1); + break; + case 'h': + if (param(0, 0) == 4) + mode_ |= MODE_INSERT; + break; + case 'l': + if (param(0, 0) == 4) + mode_ &= ~MODE_INSERT; + break; + case 'm': + set_sgr(); + break; + case 'n': + if (param(0, 0) == 5) { + pty_write("\033[0n", 4); + } else if (param(0, 0) == 6) { + char reply[32]; + int len = snprintf(reply, sizeof(reply), "\033[%d;%dR", cursor_.y + 1, cursor_.x + 1); + pty_write(reply, (size_t)len); + } + break; + case 'r': + scroll_top_ = clamp(param(0, 1) - 1, 0, term_rows_ - 1); + scroll_bot_ = clamp(param(1, term_rows_) - 1, 0, term_rows_ - 1); + if (scroll_top_ >= scroll_bot_) { + scroll_top_ = 0; + scroll_bot_ = term_rows_ - 1; + } + move_to(0, 0); + break; + case 's': + saved_cursor_ = cursor_; + break; + case 'u': + cursor_ = saved_cursor_; + move_to(cursor_.x, cursor_.y); + break; + case 'c': + pty_write("\033[?1;2c", 7); + break; + default: + break; + } + } + + void handle_esc(uint8_t c) + { + switch (c) { + case '[': + csi_reset(); + parse_state_ = ParseState::Csi; + return; + case ']': + parse_state_ = ParseState::Osc; + return; + case '(': + case ')': + case '*': + case '+': + parse_state_ = ParseState::Charset; + return; + case '7': + saved_cursor_ = cursor_; + break; + case '8': + cursor_ = saved_cursor_; + move_to(cursor_.x, cursor_.y); + break; + case 'D': + newline(false); + break; + case 'E': + newline(true); + break; + case 'M': + if (cursor_.y == scroll_top_) + scroll_down(scroll_top_, scroll_bot_, 1); + else + move_to(cursor_.x, cursor_.y - 1); + break; + case 'c': + reset_terminal(); + break; + default: + break; + } + parse_state_ = ParseState::Normal; + } + + void process_bytes(const char *data, int len) + { + for (int i = 0; i < len; ++i) { + uint8_t c = (uint8_t)data[i]; + + if (parse_state_ == ParseState::Osc) { + if (c == 0x07) + parse_state_ = ParseState::Normal; + else if (c == 0x1b && i + 1 < len && data[i + 1] == '\\') { + ++i; + parse_state_ = ParseState::Normal; + } + continue; + } + + if (parse_state_ == ParseState::Charset) { + parse_state_ = ParseState::Normal; + continue; + } + + if (parse_state_ == ParseState::Esc) { + handle_esc(c); + continue; + } + + if (parse_state_ == ParseState::Csi) { + if (c == '?') { + csi_private_ = true; + continue; + } + if (c == '>') { + csi_secondary_ = true; + continue; + } + if (isdigit(c)) { + csi_param_value_ = csi_param_value_ * 10 + (c - '0'); + csi_have_value_ = true; + continue; + } + if (c == ';') { + csi_push_param(); + continue; + } + if (c >= 0x20 && c <= 0x2f) + continue; + csi_push_param(); + handle_csi((char)c); + parse_state_ = ParseState::Normal; + continue; + } + + if (c == 0x1b) { + parse_state_ = ParseState::Esc; + } else if (c < 0x20 || c == 0x7f) { + control_code(c); + } else { + put_rune(c); + } + } + } + + void create_ui() + { + terminal_container_ = lv_obj_create(ui_APP_Container); + lv_obj_remove_style_all(terminal_container_); + lv_obj_set_size(terminal_container_, TERM_W, TERM_H); + lv_obj_set_pos(terminal_container_, 0, 0); + lv_obj_set_style_bg_color(terminal_container_, palette(DEFAULT_BG), 0); + lv_obj_set_style_bg_opa(terminal_container_, LV_OPA_COVER, 0); + lv_obj_clear_flag(terminal_container_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + + term_canvas_ = lv_obj_create(terminal_container_); + lv_obj_set_size(term_canvas_, TERM_W, TERM_H); + lv_obj_set_pos(term_canvas_, 0, 0); + lv_obj_set_style_bg_color(term_canvas_, palette(DEFAULT_BG), 0); + lv_obj_set_style_bg_opa(term_canvas_, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(term_canvas_, 0, 0); + lv_obj_set_style_pad_all(term_canvas_, 0, 0); + lv_obj_set_style_radius(term_canvas_, 0, 0); + lv_obj_clear_flag(term_canvas_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + + scrollbar_track_ = lv_obj_create(terminal_container_); + lv_obj_remove_style_all(scrollbar_track_); + lv_obj_set_size(scrollbar_track_, SCROLLBAR_W, TERM_H); + lv_obj_set_pos(scrollbar_track_, TERM_W - SCROLLBAR_W - 1, 0); + lv_obj_set_style_bg_color(scrollbar_track_, lv_color_hex(0x30363D), 0); + lv_obj_set_style_bg_opa(scrollbar_track_, LV_OPA_COVER, 0); + lv_obj_clear_flag(scrollbar_track_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_add_flag(scrollbar_track_, LV_OBJ_FLAG_HIDDEN); + + scrollbar_thumb_ = lv_obj_create(terminal_container_); + lv_obj_remove_style_all(scrollbar_thumb_); + lv_obj_set_size(scrollbar_thumb_, SCROLLBAR_W, 8); + lv_obj_set_pos(scrollbar_thumb_, TERM_W - SCROLLBAR_W - 1, TERM_H - 8); + lv_obj_set_style_bg_color(scrollbar_thumb_, lv_color_hex(0x8B949E), 0); + lv_obj_set_style_bg_opa(scrollbar_thumb_, LV_OPA_COVER, 0); + lv_obj_clear_flag(scrollbar_thumb_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_add_flag(scrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + + hscrollbar_track_ = lv_obj_create(terminal_container_); + lv_obj_remove_style_all(hscrollbar_track_); + lv_obj_set_size(hscrollbar_track_, TERM_W - SCROLLBAR_W - 2, 3); + lv_obj_set_pos(hscrollbar_track_, 0, BIG_VIEW_ROWS * CHAR_H); + lv_obj_set_style_bg_color(hscrollbar_track_, lv_color_hex(0x30363D), 0); + lv_obj_set_style_bg_opa(hscrollbar_track_, LV_OPA_COVER, 0); + lv_obj_clear_flag(hscrollbar_track_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_add_flag(hscrollbar_track_, LV_OBJ_FLAG_HIDDEN); + + hscrollbar_thumb_ = lv_obj_create(terminal_container_); + lv_obj_remove_style_all(hscrollbar_thumb_); + lv_obj_set_size(hscrollbar_thumb_, 18, 3); + lv_obj_set_pos(hscrollbar_thumb_, 0, BIG_VIEW_ROWS * CHAR_H); + lv_obj_set_style_bg_color(hscrollbar_thumb_, lv_color_hex(0x8B949E), 0); + lv_obj_set_style_bg_opa(hscrollbar_thumb_, LV_OPA_COVER, 0); + lv_obj_clear_flag(hscrollbar_thumb_, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE)); + lv_obj_add_flag(hscrollbar_thumb_, LV_OBJ_FLAG_HIDDEN); + + static const char *bottom_text[BOTTOM_BAR_SLOTS] = {"F4 <", "F5 up", "F6 normal", "F7 down", "F8 >"}; + constexpr int slot_w = TERM_W / BOTTOM_BAR_SLOTS; + for (int i = 0; i < BOTTOM_BAR_SLOTS; ++i) { + bottom_labels_[(size_t)i] = lv_label_create(terminal_container_); + lv_obj_set_pos(bottom_labels_[(size_t)i], i * slot_w, TERM_H - BIG_BOTTOM_H); + lv_obj_set_size(bottom_labels_[(size_t)i], slot_w, BIG_BOTTOM_H); + lv_obj_set_style_text_font(bottom_labels_[(size_t)i], &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(bottom_labels_[(size_t)i], lv_color_hex(0xF0F6FC), 0); + lv_obj_set_style_text_align(bottom_labels_[(size_t)i], LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_pad_top(bottom_labels_[(size_t)i], 1, 0); + lv_label_set_text(bottom_labels_[(size_t)i], bottom_text[i]); + lv_obj_add_flag(bottom_labels_[(size_t)i], LV_OBJ_FLAG_HIDDEN); + + bottom_indicators_[(size_t)i] = lv_label_create(terminal_container_); + lv_obj_set_pos(bottom_indicators_[(size_t)i], i * slot_w, TERM_H - 4); + lv_obj_set_size(bottom_indicators_[(size_t)i], slot_w, 4); + lv_obj_set_style_text_font(bottom_indicators_[(size_t)i], &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(bottom_indicators_[(size_t)i], lv_color_hex(0x8B949E), 0); + lv_obj_set_style_text_align(bottom_indicators_[(size_t)i], LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_text(bottom_indicators_[(size_t)i], "|"); + lv_obj_add_flag(bottom_indicators_[(size_t)i], LV_OBJ_FLAG_HIDDEN); + } + + mono_font_ = launcher_fonts().get_mono("LiberationMono-Regular.ttf", 11, LV_FREETYPE_FONT_STYLE_NORMAL); + + cursor_label_ = lv_label_create(term_canvas_); + lv_obj_set_style_text_font(cursor_label_, mono_font_, 0); + lv_obj_set_style_text_color(cursor_label_, palette(DEFAULT_BG), 0); + lv_obj_set_style_bg_color(cursor_label_, palette(DEFAULT_FG), 0); + lv_obj_set_style_bg_opa(cursor_label_, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(cursor_label_, 0, 0); + lv_obj_set_style_pad_top(cursor_label_, TEXT_Y_PAD, 0); + lv_obj_set_style_text_letter_space(cursor_label_, 0, 0); + lv_label_set_long_mode(cursor_label_, LV_LABEL_LONG_CLIP); + lv_obj_set_size(cursor_label_, CHAR_W, CHAR_H); + lv_label_set_text(cursor_label_, " "); + lv_obj_add_flag(cursor_label_, LV_OBJ_FLAG_HIDDEN); + } + + void bind_events() + { + lv_obj_add_event_cb(root_screen_, UISTPage::static_event_cb, LV_EVENT_ALL, this); + } + + static void static_event_cb(lv_event_t *e) + { + auto *self = static_cast(lv_event_get_user_data(e)); + if (self) + self->event_cb(e); + } + + void event_cb(lv_event_t *e) + { + if (lv_event_get_code(e) != LV_EVENT_KEYBOARD) + return; + struct key_item *elm = (struct key_item *)lv_event_get_param(e); + if (!elm) + return; + + if (waiting_key_to_exit_) { + if (elm->key_state == 0) { + if (terminal_sysplause) { + terminal_sysplause = false; + } else { + waiting_key_to_exit_ = false; + if (navigate_home) + navigate_home(); + } + } + return; + } + + if (elm->key_code == KEY_LEFTSHIFT || elm->key_code == KEY_RIGHTSHIFT) { + shift_down_ = elm->key_state != KBD_KEY_RELEASED; + return; + } + + if (elm->key_state && elm->key_code == KEY_F6) { + switch_big_mode(!big_mode_); + return; + } + if (elm->key_state && big_mode_) { + switch (elm->key_code) { + case KEY_F4: + pan_big_view(-8, 0); + render_all(); + return; + case KEY_F8: + pan_big_view(8, 0); + render_all(); + return; + case KEY_F5: + pan_big_view(0, -4); + render_all(); + return; + case KEY_F7: + pan_big_view(0, 4); + render_all(); + return; + default: + break; + } + } + + bool shift = shift_down_ || ((elm->mods & KBD_MOD_SHIFT) != 0); + if (elm->key_state && shift && elm->key_code == KEY_PAGEUP) { + scrollback_page(1); + render_all(); + return; + } + if (elm->key_state && shift && elm->key_code == KEY_PAGEDOWN) { + scrollback_page(-1); + render_all(); + return; + } + + if (terminal_active_ && !pty_handle_.empty() && elm->key_state) { + leave_scrollback(); + follow_cursor_in_big_mode(); + write_key(elm->key_code, elm->utf8); + } + } + + static void effective_colors(const Glyph &g, uint32_t *fg, uint32_t *bg) + { + uint32_t out_fg = g.fg; + uint32_t out_bg = g.bg; + if (g.attr & ATTR_REVERSE) + std::swap(out_fg, out_bg); + if (g.attr & ATTR_BOLD) + out_fg = out_fg < 8 ? out_fg + 8 : out_fg; + if (fg) + *fg = out_fg; + if (bg) + *bg = out_bg; + } + + bool meaningful_cell(const Glyph &g) const + { + uint32_t fg = DEFAULT_FG; + uint32_t bg = DEFAULT_BG; + effective_colors(g, &fg, &bg); + char ch = (g.attr & ATTR_INVISIBLE) ? ' ' : printable(g.u); + return ch != ' ' || fg != DEFAULT_FG || bg != DEFAULT_BG; + } + + lv_obj_t *create_segment_label() + { + lv_obj_t *lbl = lv_label_create(term_canvas_); + lv_obj_set_style_text_font(lbl, mono_font_, 0); + lv_obj_set_style_text_color(lbl, palette(DEFAULT_FG), 0); + lv_obj_set_style_bg_color(lbl, palette(DEFAULT_BG), 0); + lv_obj_set_style_bg_opa(lbl, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(lbl, 0, 0); + lv_obj_set_style_pad_top(lbl, TEXT_Y_PAD, 0); + lv_obj_set_style_text_letter_space(lbl, 0, 0); + lv_obj_set_style_text_line_space(lbl, 0, 0); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); + lv_obj_set_size(lbl, CHAR_W, CHAR_H); + lv_label_set_text(lbl, " "); + lv_obj_add_flag(lbl, LV_OBJ_FLAG_HIDDEN); + return lbl; + } + + std::vector build_row_segments(int r) + { + std::vector out; + const auto &line = display_row(r); + int last = -1; + int first_col = big_mode_ ? viewport_x_ : 0; + int cols = visible_cols(); + for (int c = cols - 1; c >= 0; --c) { + if (meaningful_cell(line[(size_t)(first_col + c)])) { + last = c; + break; + } + } + if (last < 0) + return out; + + SegmentData current; + bool has_current = false; + for (int c = 0; c <= last; ++c) { + const Glyph &g = line[(size_t)(first_col + c)]; + uint32_t fg = DEFAULT_FG; + uint32_t bg = DEFAULT_BG; + effective_colors(g, &fg, &bg); + char ch = (g.attr & ATTR_INVISIBLE) ? ' ' : printable(g.u); + + if (!has_current || current.fg != fg || current.bg != bg) { + if (has_current) + out.push_back(current); + current = SegmentData{}; + current.x = c; + current.fg = fg; + current.bg = bg; + has_current = true; + } + current.text.push_back(ch); + } + if (has_current) + out.push_back(current); + return out; + } + + void render_row(int r) + { + std::vector desired = build_row_segments(r); + std::vector &rendered = row_segments_[r]; + if (rendered.size() < desired.size()) + rendered.resize(desired.size()); + + for (size_t i = 0; i < desired.size(); ++i) { + SegmentData &want = desired[i]; + RenderSegment &have = rendered[i]; + if (!have.label) + have.label = create_segment_label(); + + int width = (int)want.text.size() * CHAR_W; + bool changed = have.hidden || have.x != want.x || have.width != width || + have.fg != want.fg || have.bg != want.bg || have.text != want.text; + if (!changed) + continue; + + have.hidden = false; + have.x = want.x; + have.width = width; + have.fg = want.fg; + have.bg = want.bg; + have.text = want.text; + + lv_obj_clear_flag(have.label, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(have.label, want.x * CHAR_W, r * CHAR_H); + lv_obj_set_size(have.label, width, CHAR_H); + lv_obj_set_style_text_color(have.label, palette(want.fg), 0); + lv_obj_set_style_bg_color(have.label, palette(want.bg), 0); + lv_label_set_text(have.label, want.text.c_str()); + } + + for (size_t i = desired.size(); i < rendered.size(); ++i) { + RenderSegment &segment = rendered[i]; + if (!segment.hidden && segment.label) { + lv_obj_add_flag(segment.label, LV_OBJ_FLAG_HIDDEN); + segment.hidden = true; + segment.text.clear(); + } + } + } + + void render_all() + { + int rows = visible_rows(); + for (int r = 0; r < ROWS; ++r) { + if (r >= rows) { + if (dirty_[r]) { + for (auto &segment : row_segments_[r]) { + if (segment.label) + lv_obj_add_flag(segment.label, LV_OBJ_FLAG_HIDDEN); + segment.hidden = true; + } + dirty_[r] = false; + } + continue; + } + if (dirty_[r]) { + render_row(r); + dirty_[r] = false; + } + } + update_cursor(); + update_hscrollbar(); + update_scrollbar(); + } + + void update_cursor() + { + if (!cursor_label_) + return; + if (scrollback_offset_ > 0) { + lv_obj_add_flag(cursor_label_, LV_OBJ_FLAG_HIDDEN); + cursor_blink_visible_ = false; + return; + } + int x = clamp(cursor_.x - (big_mode_ ? viewport_x_ : 0), 0, visible_cols() - 1); + int y = clamp(cursor_.y - (big_mode_ ? viewport_y_ : 0), 0, visible_rows() - 1); + if (big_mode_ && (cursor_.x < viewport_x_ || cursor_.x >= viewport_x_ + visible_cols() || + cursor_.y < viewport_y_ || cursor_.y >= viewport_y_ + visible_rows())) { + lv_obj_add_flag(cursor_label_, LV_OBJ_FLAG_HIDDEN); + cursor_blink_visible_ = false; + return; + } + const Glyph &g = screen_[cursor_.y][cursor_.x]; + uint32_t fg = DEFAULT_FG; + uint32_t bg = DEFAULT_BG; + effective_colors(g, &fg, &bg); + char under = (g.attr & ATTR_INVISIBLE) ? ' ' : printable(g.u); + char text[2] = {under, '\0'}; + lv_label_set_text(cursor_label_, text); + lv_obj_set_style_text_color(cursor_label_, palette(bg), 0); + lv_obj_set_style_bg_color(cursor_label_, palette(fg), 0); + lv_obj_set_pos(cursor_label_, x * CHAR_W, y * CHAR_H); + lv_obj_move_foreground(cursor_label_); + } + + void show_cursor(bool show) + { + if (!cursor_label_) + return; + if (scrollback_offset_ > 0) + show = false; + if (big_mode_ && (cursor_.x < viewport_x_ || cursor_.x >= viewport_x_ + visible_cols() || + cursor_.y < viewport_y_ || cursor_.y >= viewport_y_ + visible_rows())) + show = false; + if (show) + lv_obj_clear_flag(cursor_label_, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(cursor_label_, LV_OBJ_FLAG_HIDDEN); + cursor_blink_visible_ = show; + } + + std::string pty_open(const std::string &cmd, const std::list &args) + { + int code = -1; + std::string handle; + std::list api_args = { + "Open", + cmd, + std::to_string(term_cols_), + std::to_string(term_rows_), + cmd, + }; + for (const auto &arg : args) + api_args.push_back(arg); + + cp0_signal_pty_api(std::move(api_args), [&](int c, std::string data) { + code = c; + if (code == 0) + handle = std::move(data); + }); + return handle; + } + + void stop_timers() + { + if (poll_timer_) { + lv_timer_delete(poll_timer_); + poll_timer_ = nullptr; + } + if (cursor_timer_) { + lv_timer_delete(cursor_timer_); + cursor_timer_ = nullptr; + } + } + + void start_timers() + { + if (!poll_timer_) + poll_timer_ = lv_timer_create(UISTPage::static_poll_cb, 30, this); + if (!cursor_timer_) + cursor_timer_ = lv_timer_create(UISTPage::static_cursor_cb, 500, this); + } + + void start_command(const std::string &cmd, const std::list &args, + const char *title, const char *err_msg) + { + stop_pty(); + pty_handle_ = pty_open(cmd, args); + terminal_active_ = !pty_handle_.empty(); + if (!terminal_active_) { + process_bytes(err_msg, (int)strlen(err_msg)); + render_all(); + waiting_key_to_exit_ = true; + return; + } + if (title && title[0]) + set_page_title(title); + start_timers(); + render_all(); + } + + void start_shell() + { + start_command("bash", { + "-c", + "cd ~ && " + "if [ -r ~/.bashrc ]; then " + "exec env TERM=st-256color COLORTERM=truecolor bash --rcfile ~/.bashrc -i; " + "else " + "exec env TERM=st-256color COLORTERM=truecolor bash -i; " + "fi" + }, "ST", "st: failed to open PTY\r\n"); + } + + void stop_pty() + { + if (!pty_handle_.empty()) { + cp0_signal_pty_api({"Close", pty_handle_}, nullptr); + pty_handle_.clear(); + } + } + + int pty_read(char *buf, size_t buf_size) + { + int code = -1; + std::string data; + cp0_signal_pty_api({"Read", pty_handle_, std::to_string(buf_size)}, [&](int c, std::string d) { + code = c; + data = std::move(d); + }); + if (code < 0) + return -1; + size_t n = std::min(data.size(), buf_size); + if (n > 0) + memcpy(buf, data.data(), n); + return (int)n; + } + + int pty_write(const char *buf, size_t len) + { + if (pty_handle_.empty() || !buf || len == 0) + return -1; + int code = -1; + cp0_signal_pty_api({"Write", pty_handle_, std::string(buf, len)}, [&](int c, std::string) { + code = c; + }); + return code; + } + + int pty_check_child(int *status) + { + int code = -1; + std::string data; + cp0_signal_pty_api({"CheckChild", pty_handle_}, [&](int c, std::string d) { + code = c; + data = std::move(d); + }); + if (status) + *status = atoi(data.c_str()); + return code; + } + + static void static_poll_cb(lv_timer_t *timer) + { + auto *self = static_cast(lv_timer_get_user_data(timer)); + if (self) + self->poll_cb(); + } + + void poll_cb() + { + if (!terminal_active_ || pty_handle_.empty()) + return; + + char buf[1024]; + int n = 0; + bool changed = false; + while ((n = pty_read(buf, sizeof(buf))) > 0) { + process_bytes(buf, n); + changed = true; + } + + if (changed) { + if (scrollback_offset_ > 0) + dirty_all(); + follow_cursor_in_big_mode(); + if (big_mode_) + dirty_all(); + render_all(); + } + + bool child_exited = n < 0; + if (!child_exited && !pty_handle_.empty()) { + int status = 0; + child_exited = pty_check_child(&status) == 1; + } + if (child_exited) { + terminal_active_ = false; + stop_pty(); + const char *hint = "\r\n-- Press any key to exit --"; + process_bytes(hint, (int)strlen(hint)); + render_all(); + waiting_key_to_exit_ = true; + } + } + + static void static_cursor_cb(lv_timer_t *timer) + { + auto *self = static_cast(lv_timer_get_user_data(timer)); + if (self) + self->cursor_cb(); + } + + void cursor_cb() + { + handle_home_hold_exit(); + update_cursor(); + if (!terminal_active_ || !cursor_visible_mode_) { + show_cursor(false); + return; + } + show_cursor(!cursor_blink_visible_); + } + + void handle_home_hold_exit() + { + if (home_hold_status_ == 0) { + if (LVGL_HOME_KEY_FLAG) { + home_hold_status_ = 1; + home_hold_start_ = std::chrono::steady_clock::now(); + } + return; + } + + if (!LVGL_HOME_KEY_FLAG) { + home_hold_status_ = 0; + return; + } + + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - home_hold_start_).count() < 5) + return; + + home_hold_status_ = 0; + stop_pty(); + terminal_active_ = false; + if (navigate_home) + navigate_home(); + } + + void write_key(uint32_t evdev_key, const char *utf8) + { + char buf[16]; + int len = 0; + + switch (evdev_key) { + case 28: + buf[0] = '\r'; + len = 1; + break; + case 14: + buf[0] = 0x7f; + len = 1; + break; + case 1: + buf[0] = 0x1b; + len = 1; + break; + case 103: + len = snprintf(buf, sizeof(buf), "%s", (mode_ & MODE_APPCURSOR) ? "\033OA" : "\033[A"); + break; + case 108: + len = snprintf(buf, sizeof(buf), "%s", (mode_ & MODE_APPCURSOR) ? "\033OB" : "\033[B"); + break; + case 106: + len = snprintf(buf, sizeof(buf), "%s", (mode_ & MODE_APPCURSOR) ? "\033OC" : "\033[C"); + break; + case 105: + len = snprintf(buf, sizeof(buf), "%s", (mode_ & MODE_APPCURSOR) ? "\033OD" : "\033[D"); + break; + default: + len = utf8 ? (int)strlen(utf8) : 0; + if (len > 0 && len < (int)sizeof(buf)) + memcpy(buf, utf8, (size_t)len); + else + len = 0; + break; + } + + if (len > 0) + pty_write(buf, (size_t)len); + } +}; diff --git a/projects/APPLaunch/main/ui/ui.h b/projects/APPLaunch/main/ui/ui.h index 90985c93..605cf8a3 100644 --- a/projects/APPLaunch/main/ui/ui.h +++ b/projects/APPLaunch/main/ui/ui.h @@ -88,11 +88,15 @@ class LauncherFonts ~LauncherFonts(); lv_font_t *get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style); + lv_font_t *get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style, + lv_freetype_font_render_mode_t render_mode); + lv_font_t *get_mono(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style); private: void release(); lv_font_t *fallback(uint16_t size) const; - static std::string key(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style); + static std::string key(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style, + lv_freetype_font_render_mode_t render_mode); std::unordered_map fonts_; }; diff --git a/projects/APPLaunch/main/ui/ui_launch_page.cpp b/projects/APPLaunch/main/ui/ui_launch_page.cpp index 43a4e476..c13edfb7 100644 --- a/projects/APPLaunch/main/ui/ui_launch_page.cpp +++ b/projects/APPLaunch/main/ui/ui_launch_page.cpp @@ -234,14 +234,24 @@ LauncherFonts::~LauncherFonts() lv_font_t *LauncherFonts::get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style) { - const std::string font_key = key(ttf_name, size, style); + return get(ttf_name, size, style, LV_FREETYPE_FONT_RENDER_MODE_BITMAP); +} + +lv_font_t *LauncherFonts::get_mono(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style) +{ + return get(ttf_name, size, style, LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO); +} + +lv_font_t *LauncherFonts::get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style, + lv_freetype_font_render_mode_t render_mode) +{ + const std::string font_key = key(ttf_name, size, style, render_mode); auto it = fonts_.find(font_key); if (it != fonts_.end()) { return it->second ? it->second : fallback(size); } - lv_font_t *font = lv_freetype_font_create(cp0_file_path_c(ttf_name), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, - size, style); + lv_font_t *font = lv_freetype_font_create(cp0_file_path_c(ttf_name), render_mode, size, style); fonts_[font_key] = font; return font ? font : fallback(size); } @@ -268,10 +278,11 @@ lv_font_t *LauncherFonts::fallback(uint16_t size) const return (lv_font_t *)&lv_font_montserrat_12; } -std::string LauncherFonts::key(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style) +std::string LauncherFonts::key(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style, + lv_freetype_font_render_mode_t render_mode) { return std::string(ttf_name ? ttf_name : "") + "#" + std::to_string(size) + "#" + - std::to_string(static_cast(style)); + std::to_string(static_cast(style)) + "#" + std::to_string(static_cast(render_mode)); } lv_group_t *UILaunchPage::home_input_group() From 7d30f9f35955cb49e9fbda323c9a9e370bdf36f6 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 16 Jun 2026 16:23:10 +0800 Subject: [PATCH 68/70] Sync APPLaunch ST terminal docs --- README.md | 4 +- README_JA.md | 4 +- README_ZH.md | 4 +- docs/APPLaunch-App-packaging-guide-ja.md | 4 +- ...23\345\214\205\346\214\207\345\215\227.md" | 4 +- .../00-overview-and-reading-path.md | 4 +- ...ject-layout-and-module-responsibilities.md | 4 +- ...-application-model-and-launch-mechanism.md | 30 ++++----- .../05-built-in-page-framework.md | 36 +++++----- .../06-resources-and-configuration.md | 26 ++++---- .../07-input-system-and-key-mapping.md | 6 +- .../10-extension-development-guide.md | 66 +++++++------------ .../11-debugging-and-troubleshooting.md | 4 +- .../12-common-modification-entry-points.md | 56 ++++++++-------- .../00-overview-and-reading-path.md | 4 +- ...ject-layout-and-module-responsibilities.md | 4 +- ...-application-model-and-launch-mechanism.md | 30 ++++----- .../05-built-in-page-framework.md | 36 +++++----- .../06-resources-and-configuration.md | 26 ++++---- .../07-input-system-and-key-mapping.md | 6 +- .../10-extension-development-guide.md | 66 +++++++------------ .../11-debugging-and-troubleshooting.md | 4 +- .../12-common-modification-entry-points.md | 56 ++++++++-------- ...05\350\257\273\350\267\257\347\272\277.md" | 4 +- ...41\345\235\227\350\201\214\350\264\243.md" | 4 +- ...57\345\212\250\346\234\272\345\210\266.md" | 30 ++++----- ...65\351\235\242\346\241\206\346\236\266.md" | 36 +++++----- ...15\347\275\256\347\263\273\347\273\237.md" | 26 ++++---- ...11\351\224\256\346\230\240\345\260\204.md" | 6 +- ...00\345\217\221\346\214\207\345\215\227.md" | 66 +++++++------------ ...05\351\232\234\346\216\222\346\237\245.md" | 4 +- ...45\345\217\243\351\200\237\346\237\245.md" | 56 ++++++++-------- .../APPLaunch/main/ui/page_app/ui_app_ssh.hpp | 20 +++--- 33 files changed, 338 insertions(+), 398 deletions(-) diff --git a/README.md b/README.md index e7201b2f..c5a2d5e9 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ launcher/ ### APPLaunch Features - Application launcher UI with multi-app navigation (carousel page switching) -- Built-in app pages: Game, Setting, Compass, IP Panel, File, SSH, Mesh, Rec, Camera, LoRa, Tank Battle, Console, plus command/external entries such as Python, Store, CLI, and Math +- Built-in app pages: CLI/ST terminal, Game, Setting, Compass, IP Panel, File, SSH, Mesh, Rec, Camera, LoRa, and Tank Battle, plus command/external entries such as Python, Store, and Math - LoRa communication (based on RadioLib SX1262) - Audio playback (based on Miniaudio) - Battery status monitoring and display @@ -336,7 +336,7 @@ The UI is designed and generated by SquareLine Studio 1.5.0, with a resolution o | Main content area | Application carousel pages (left/right page navigation) | | Global hints | ESC / Shift / SYM shortcut hint overlay | -Built-in app pages include: Game, Setting, Compass, IP Panel, File, SSH, Mesh, Rec, Camera, LoRa, Tank Battle, and Console. APPLaunch can also expose command or external-process entries such as Python, Store, CLI, and Math. +Built-in app pages include: CLI/ST terminal, Game, Setting, Compass, IP Panel, File, SSH, Mesh, Rec, Camera, LoRa, and Tank Battle. APPLaunch can also expose command or external-process entries such as Python, Store, and Math. ## Related Resources diff --git a/README_JA.md b/README_JA.md index a82ffe18..3215fdc5 100644 --- a/README_JA.md +++ b/README_JA.md @@ -68,7 +68,7 @@ launcher/ ### APPLaunch の機能 - 複数アプリのナビゲーションに対応したアプリケーションランチャー UI(カルーセル形式のページ切り替え) -- 組み込みアプリページ: Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、Console、および Python、Store、CLI、Math などのコマンド/外部エントリ +- 組み込みアプリページ: CLI/ST ターミナル、Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、および Python、Store、Math などのコマンド/外部エントリ - LoRa 通信(RadioLib SX1262 ベース) - オーディオ再生(Miniaudio ベース) - バッテリー状態の監視と表示 @@ -336,7 +336,7 @@ UI は SquareLine Studio 1.5.0 によって設計・生成されており、解 | メインコンテンツ領域 | アプリケーションのカルーセルページ(左右のページナビゲーション) | | グローバルヒント | ESC / Shift / SYM ショートカットヒントのオーバーレイ | -組み込みアプリページには、Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、Console が含まれます。APPLaunch は、Python、Store、CLI、Math などのコマンドまたは外部プロセスのエントリも表示できます。 +組み込みアプリページには、CLI/ST ターミナル、Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle が含まれます。APPLaunch は、Python、Store、Math などのコマンドまたは外部プロセスのエントリも表示できます。 ## 関連リソース diff --git a/README_ZH.md b/README_ZH.md index 4be2e035..d70884d0 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -68,7 +68,7 @@ launcher/ ### APPLaunch 特性 - 应用启动器界面,支持多应用导航(轮播翻页) -- 内置应用页面:Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle、Console,以及 Python、Store、CLI、Math 等命令/外部进程入口 +- 内置应用页面:CLI/ST 终端、Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle,以及 Python、Store、Math 等命令/外部进程入口 - LoRa 通信(基于 RadioLib SX1262) - 音频播放(基于 Miniaudio) - 电池状态监控与显示 @@ -324,7 +324,7 @@ export LV_LINUX_KEYBOARD_DEVICE=/dev/input/by-path/platform-3f804000.i2c-event | 主内容区 | 应用轮播页面(左右翻页导航) | | 全局提示 | ESC / Shift / SYM 快捷键提示覆盖层 | -内置应用页面包括:Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa、Tank Battle 和 Console。APPLaunch 也可以显示 Python、Store、CLI、Math 等命令或外部进程入口。 +内置应用页面包括:CLI/ST 终端、Game、Setting、Compass、IP Panel、File、SSH、Mesh、Rec、Camera、LoRa 和 Tank Battle。APPLaunch 也可以显示 Python、Store、Math 等命令或外部进程入口。 ## 相关资源 diff --git a/docs/APPLaunch-App-packaging-guide-ja.md b/docs/APPLaunch-App-packaging-guide-ja.md index b4ec3fab..6c3d97bc 100644 --- a/docs/APPLaunch-App-packaging-guide-ja.md +++ b/docs/APPLaunch-App-packaging-guide-ja.md @@ -51,7 +51,7 @@ APPLaunch は XDG Desktop Entry に似た形式の `.desktop` ファイルで Ap | `Name` | **必須** | 文字列 | ランチャーリストに表示される App 名 | | `Exec` | **必須** | 文字列 | 実行するコマンド、または実行ファイルのパス | | `Icon` | 任意 | 文字列 | アイコンパス(`/usr/share/APPLaunch/` からの相対パス、または絶対パス) | -| `Terminal` | 任意 | `true`/`false` | 内蔵ターミナル(UIConsolePage)で実行するかどうか。デフォルトは `false` | +| `Terminal` | 任意 | `true`/`false` | 内蔵ターミナル(UISTPage)で実行するかどうか。デフォルトは `false` | | `Sysplause` | 任意 | `true`/`false` | ターミナル実行終了後に「任意のキーで戻る」プロンプトを表示するかどうか。デフォルトは `true` | | `Type` | 任意 | 文字列 | `Application` を固定で指定(ツールチェーン識別用。APPLaunch 自体は検証しません) | | `TryExec` | 任意 | 文字列 | ドキュメント説明用のみ。APPLaunch はこのフィールドを解析しません | @@ -68,7 +68,7 @@ APPLaunch は XDG Desktop Entry に似た形式の `.desktop` ファイルで Ap - `Terminal=false`(デフォルト):APPLaunch は `fork` + `execlp` で外部プログラムを直接起動します。 プログラム実行中、ランチャーの更新は一時停止します。プログラム終了後は自動的にメイン画面へ戻ります。 Home キーを 5 秒間長押しすると `SIGINT` を送信し、さらに 3 秒待っても終了しない場合は `SIGKILL` を送信します。 -- `Terminal=true`:APPLaunch は内蔵ターミナル画面(UIConsolePage)でコマンドを実行し、 +- `Terminal=true`:APPLaunch は内蔵ターミナル画面(UISTPage)でコマンドを実行し、 キーボード入力と出力表示に対応します。 - `Sysplause=true`(デフォルト。`Terminal=true` の場合のみ有効):コマンド終了後に "Press any key to return..." を表示し、ユーザーの確認を待ってからメイン画面へ戻ります。 diff --git "a/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" "b/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" index 41960ead..a445b055 100644 --- "a/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" +++ "b/docs/APPLaunch-App-\346\211\223\345\214\205\346\214\207\345\215\227.md" @@ -51,7 +51,7 @@ APPLaunch 使用类 XDG Desktop Entry 格式的 `.desktop` 文件来描述一个 | `Name` | **必填** | 字符串 | App 在启动器列表中显示的名称 | | `Exec` | **必填** | 字符串 | 要执行的命令或可执行文件路径 | | `Icon` | 可选 | 字符串 | 图标路径(相对于 `/usr/share/APPLaunch/`,或绝对路径) | -| `Terminal` | 可选 | `true`/`false` | 是否在内置终端(UIConsolePage)中运行,默认 `false` | +| `Terminal` | 可选 | `true`/`false` | 是否在内置终端(UISTPage)中运行,默认 `false` | | `Sysplause` | 可选 | `true`/`false` | 终端运行结束后是否显示"按任意键返回"提示,默认 `true` | | `Type` | 可选 | 字符串 | 固定填写 `Application`(供工具链识别,APPLaunch 本身不校验) | | `TryExec` | 可选 | 字符串 | 仅用于文档说明,APPLaunch 不解析此字段 | @@ -68,7 +68,7 @@ APPLaunch 使用类 XDG Desktop Entry 格式的 `.desktop` 文件来描述一个 - `Terminal=false`(默认):APPLaunch 通过 `fork` + `execlp` 直接启动外部程序, 程序运行期间启动器暂停刷新;程序退出后自动返回主界面。 长按 Home 键 5 秒可发送 `SIGINT`,再等待 3 秒未退出则发送 `SIGKILL`。 -- `Terminal=true`:APPLaunch 在内置终端界面(UIConsolePage)中运行命令, +- `Terminal=true`:APPLaunch 在内置终端界面(UISTPage)中运行命令, 支持键盘输入/输出显示。 - `Sysplause=true`(默认,仅在 `Terminal=true` 时生效):命令结束后显示 "Press any key to return...",等待用户确认再返回主界面。 diff --git a/docs/launcher-project-guide-ja/00-overview-and-reading-path.md b/docs/launcher-project-guide-ja/00-overview-and-reading-path.md index 396eb837..ce79ff80 100644 --- a/docs/launcher-project-guide-ja/00-overview-and-reading-path.md +++ b/docs/launcher-project-guide-ja/00-overview-and-reading-path.md @@ -34,7 +34,7 @@ LVGL 9.5 UI framework APPLaunch home, status bar, carousel, application manager | +--> Built-in page AppPage - +--> PTY terminal application UIConsolePage + +--> PTY ST terminal application UISTPage +--> External independent process cp0_process_exec_blocking() ``` @@ -80,7 +80,7 @@ APPLaunch home, status bar, carousel, application manager - **APPLaunch**: ランチャープロジェクト、またはランチャープロセス。 - **Home screen**: ステータスバーとアプリケーションカルーセルを持つ APPLaunch のメイン画面。 - **Built-in page**: `UISetupPage` など、APPLaunch プロセスにコンパイルされるページクラス。 -- **Terminal application**: `bash` のように、`UIConsolePage` + PTY を通して APPLaunch 内で実行されるコマンド。 +- **Terminal application**: `bash` のように、`UISTPage` + PTY を通して APPLaunch 内で実行されるコマンド。 - **External application**: 独立した実行可能プログラム。起動時、APPLaunch は自身の LVGL レンダリングを一時停止し、外部プログラムの終了を待ちます。 - **Resource tree**: `APPLaunch/share/images`、`APPLaunch/share/audio`、`APPLaunch/share/font` などの実行時ファイル。 - **On-device**: M5CardputerZero 上の AArch64 Linux 環境。 diff --git a/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md index ecb8eb37..659169b1 100644 --- a/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md +++ b/docs/launcher-project-guide-ja/01-project-layout-and-module-responsibilities.md @@ -196,7 +196,7 @@ main/ui/ main/ui/page_app/ ├── ui_app_camera.hpp ├── ui_app_compass.hpp -├── ui_app_console.hpp +├── ui_app_st.hpp ├── ui_app_file.hpp ├── ui_app_game.hpp ├── ui_app_lora.hpp @@ -230,7 +230,7 @@ ui_init() Launch ├── UILaunchPage::panel()/label() - ├── page_v + ├── append_page_app / page_v ├── cp0_file_path() ├── cp0_process_* ├── cp0_dir_watch_* diff --git a/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md b/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md index 7a7db6f4..9e2508c2 100644 --- a/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md +++ b/docs/launcher-project-guide-ja/04-application-model-and-launch-mechanism.md @@ -22,7 +22,7 @@ Home center card -> Launch::launch_app() -> app.launch(this) ├── Built-in page: new PageT + lv_disp_load_scr() - ├── Terminal app: UIConsolePage + PTY exec() + ├── Terminal app: UISTPage + PTY exec() └── External app: cp0_process_exec_blocking() ``` @@ -33,7 +33,7 @@ Home center card | `projects/APPLaunch/main/ui/launch.h` | 公開 `Launch` インターフェースと app モデル宣言 | | `projects/APPLaunch/main/ui/launch.cpp` | `app`、`Launch`、アプリケーションリスト、起動ロジック、`.desktop` スキャン | | `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Enter / クリックイベントを `Launch::launch_app()` へ転送する | -| `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | 端末ページ `UIConsolePage` | +| `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp` | 端末ページ `UISTPage` | | `projects/APPLaunch/main/ui/page_app/*.hpp` | settings、game、file、camera、LoRa などの組み込みページ | | `projects/APPLaunch/APPLaunch/applications/` | 実行時 `.desktop` アプリケーション記述ディレクトリ | | `ext_components/cp0_lvgl` | プロセス起動、PTY、ディレクトリ監視、パス解決などの低レベル機能 | @@ -81,8 +81,8 @@ struct app | Type | Construction | Launch function | Examples | | --- | --- | --- | --- | -| Built-in page | `page_v` | ページを構築し `lv_disp_load_scr()` を呼ぶ | `GAME`, `SETTING`, `Compass` | -| Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`, `CLI` | +| Built-in page | `page_v` / `append_page_app` | ページを構築し `lv_disp_load_scr()` を呼ぶ | `CLI`, `GAME`, `SETTING`, `Compass` | +| Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` が `UISTPage` を作成して `exec()` を呼ぶ | `Python`, `Terminal=true` の `.desktop` アプリ | | External process | `exec, terminal=false` | `launch_Exec()` | AppStore, Calculator | ## 5. Fixed Application Registration @@ -96,7 +96,7 @@ constexpr BuiltinAppRegistration kBuiltinApps[] = { {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, {{"STORE", "store_100.png", "app_Store", false, true}, "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, - {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, {{"MATH", "math_100.png", "app_Math", true, false}, @@ -166,7 +166,7 @@ Enter ## 7. Terminal Application Launch Mechanism -端末アプリケーションは `UIConsolePage` を使用し、外部コマンドは APPLaunch プロセス内の端末ページで実行されます。 +端末アプリケーションは `UISTPage` を使用し、外部コマンドは APPLaunch プロセス内の端末ページで実行されます。 ```cpp void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) @@ -174,7 +174,7 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) ui_loading_show("Loading..."); lv_refr_now(NULL); - auto p = std::make_shared(); + auto p = std::make_shared(); app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); @@ -190,10 +190,10 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) ```text Python -> exec = "python3", terminal = true -CLI -> exec = "bash", terminal = true +CLI -> 組み込み UISTPage。ページ内部で対話型 bash を起動 ``` -組み込みページと比べ、端末アプリケーションには `p->exec(exec)` という追加ステップがあります。通常、コマンドとは PTY を通してやり取りします。ユーザーが見るのは、APPLaunch の外にある別 UI ではなく `UIConsolePage` です。 +組み込みページと比べ、端末アプリケーションには `p->exec(exec)` という追加ステップがあります。通常、コマンドとは PTY を通してやり取りします。ユーザーが見るのは、APPLaunch の外にある別 UI ではなく `UISTPage` です。 ## 8. External Standalone Application Launch Mechanism @@ -256,15 +256,11 @@ Enter external app -> LVGL_RUN_FLAGE=1 ``` -`STORE` は外部アプリケーションの例です。 +`STORE` は外部アプリケーションを起動する固定エントリです。 ```cpp -app_list.emplace_back("STORE", - img_path("store_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", - false, - true, - true); +{{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, ``` ここでは `run_as_root=true` が `launch_Exec(exec, run_as_root)` に渡され、そこで `keep_root ? 1 : 0` に変換されます。 @@ -331,7 +327,7 @@ Icon=share/images/e-Mail_80.png | `Name` | Yes | ホーム画面に表示するタイトル | | `Exec` | Yes | 起動コマンド | | `Icon` | No | アイコンパス | -| `Terminal` | No | `true/True/1` は `UIConsolePage` 経由で起動することを意味する | +| `Terminal` | No | `true/True/1` は `UISTPage` 経由で起動することを意味する | | `Sysplause` | No | 端末ページへ渡す pause policy。デフォルトは true | 登録ロジック: diff --git a/docs/launcher-project-guide-ja/05-built-in-page-framework.md b/docs/launcher-project-guide-ja/05-built-in-page-framework.md index ed2428b0..4b49a4fa 100644 --- a/docs/launcher-project-guide-ja/05-built-in-page-framework.md +++ b/docs/launcher-project-guide-ja/05-built-in-page-framework.md @@ -20,7 +20,7 @@ UILaunchPage home carousel Launch::launch_app() | +-- External command: cp0_process_exec_blocking() - +-- Terminal command: UIConsolePage + PTY + +-- Terminal command: UISTPage + PTY +-- Built-in page: std::make_shared() | v @@ -177,14 +177,13 @@ if (navigate_home) | Page class | File | Launcher name | Inheritance | Description | | --- | --- | --- | --- | --- | -| `UIConsolePage` | `ui_app_console.hpp` | `CLI` or terminal external command | `AppPage` | 端末エミュレータ。PTY read/write、ANSI/VT シーケンス、キーボード escape sequence をサポート | +| `UISTPage` | `ui_app_st.hpp` | `CLI` or terminal external command | `AppPage` | 端末エミュレータ。PTY read/write、ANSI/VT シーケンス、キーボード escape sequence をサポート | | `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | Snake game。フルスクリーンのカスタム描画で LVGL timer により駆動 | | `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | システム設定、アプリ切り替え、brightness、volume、WiFi、camera resolution など | -| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPage` | 組み込みゲームエントリ | | `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | コンパスページ。sensor thread + UI timer | | `UIIpPanelPage` | `ui_app_ip_panel.hpp` | `IP_PANEL` | `AppPage` | ネットワークインターフェース/IP 情報リスト。毎秒更新 | | `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | ファイルブラウザ。ディレクトリ一覧と enter/back navigation | -| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH パラメータ入力。接続後に `UIConsolePage` を埋め込む | +| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH パラメータ入力。接続後に `UISTPage` を埋め込む | | `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh メッセージ一覧、入力 overlay、send/refresh | | `UIRecPage` | `ui_app_rec.hpp` | `REC` | Custom `rec_page` | 録音/再生/ファイル一覧と非同期リソース管理 | | `UICameraPage` | `ui_app_camera.hpp` | `CAMERA` | `AppPage` | カメラ preview、gallery、capture、status page | @@ -195,14 +194,17 @@ if (navigate_home) ## 6. Page Registration and Display Order -組み込みページは `Launch::Launch()` で `app_list` に挿入されます。最初の 5 つの固定アプリケーションが、まず 5 つのホームカルーセルスロットを初期化します。 +組み込みエントリは `launch.cpp` の `kBuiltinApps[]` で宣言されます。最初の 5 つの有効なエントリが、まず 5 つのホームカルーセルスロットを初期化します。 ```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, +}; ``` 組み込みページの表示可否は現在 `kBuiltinApps[]` と `AppDescriptor.config_key` によって駆動されます。`Launch::rebuild_builtin_apps()` は各 descriptor を追加する前に `launcher_app_registry_is_enabled()` を呼び、Settings の変更は `launcher_app_registry_set_enabled()` の後に `Launch::applications_reload()` を呼びます。 @@ -282,19 +284,19 @@ private: ## 9. Nested Pages and Special Pages -`UISSHPage` は典型的な nested page です。SSH パラメータ入力中は `UISSHPage` がキーボードを処理します。接続後は `UIConsolePage` を作成し、screen と input group を切り替えます。 +`UISSHPage` は典型的な nested page です。SSH パラメータ入力中は `UISSHPage` がキーボードを処理します。接続後は `UISTPage` を作成し、screen と input group を切り替えます。 ```cpp -console_page_ = std::make_shared(); -console_page_->navigate_home = [this]() { - console_page_.reset(); +terminal_page_ = std::make_shared(); +terminal_page_->navigate_home = [this]() { + terminal_page_.reset(); view_state_ = ViewState::INPUT; lv_disp_load_scr(this->screen()); lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); }; -lv_disp_load_scr(console_page_->screen()); -lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); +lv_disp_load_scr(terminal_page_->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), terminal_page_->input_group()); ``` この種のページには特別な注意が必要です。 @@ -320,4 +322,4 @@ lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); - 明確にホーム画面機能でない限り、ページからホームのグローバルオブジェクトへ直接アクセスしないでください。 - ページタイトルは内部 top-bar label を直接変更せず、`set_page_title()` を呼びます。 - 終了可能なすべてのページは `KEY_ESC` をサポートし、`navigate_home` または前の view へ戻る処理を呼ぶ必要があります。 -- ページ toggle key は `UISetupPage::save_app_toggle()` と `launch.cpp` の `APP_ENABLED()` と一貫している必要があります。 +- ページ toggle key は `AppDescriptor.config_key`、`UISetupPage::save_app_toggle()`、`launch.cpp` の `launcher_app_registry_is_enabled()` と一貫している必要があります。 diff --git a/docs/launcher-project-guide-ja/06-resources-and-configuration.md b/docs/launcher-project-guide-ja/06-resources-and-configuration.md index b0ccc172..246b4019 100644 --- a/docs/launcher-project-guide-ja/06-resources-and-configuration.md +++ b/docs/launcher-project-guide-ja/06-resources-and-configuration.md @@ -145,7 +145,8 @@ extern "C" const char *cp0_file_path_c(const char *file) ホーム画面や組み込みページでの一般的な使用例: ```cpp -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +{{"GAME", "game_100.png", "app_Game", false, true}, + nullptr, false, true, false, append_page_app}, lv_obj_set_style_bg_img_src(time_panel_, cp0_file_path_c("status_time_background.png"), @@ -212,7 +213,7 @@ launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD) | `Name` | Yes | カルーセルに表示する名前 | | `Exec` | Yes | 起動コマンドまたは実行ファイルパス | | `Icon` | No | アイコンパス。`share/images/...` または LVGL が読めるパスを指定可能 | -| `Terminal` | No | `true`/`True`/`1` の場合は `UIConsolePage` 内で実行 | +| `Terminal` | No | `true`/`True`/`1` の場合は `UISTPage` 内で実行 | | `Sysplause` | No | ターミナルコマンド終了後に一時停止してユーザー確認を待つかどうか。既定は `true` | 例: @@ -253,8 +254,8 @@ void cp0_config_save(void); - 読み取り時は必ずデフォルト値を渡し、設定が欠けていてもページが動作を継続できるようにします。 - 変更を永続化するには、書き込み後に `cp0_config_save()` を呼びます。 -- デバイス側実装は `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` にあります。 -- SDL 互換実装は `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` にあります。 +- デバイス側実装は `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` にあります。 +- SDL 互換実装は `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp` にあります。 典型的な使い方: @@ -302,7 +303,7 @@ cp0_config_save(); | `startup_mode` | `UISetupPage` | 起動モード。現在は `Launcher` / `CLI` | | `extport_usb` | `UISetupPage` | 拡張ポート USB トグル | | `extport_5vout` | `UISetupPage` | 拡張ポート 5V 出力トグル | -| `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | 外部プロセス / PTY コマンドで権限を下げる際のユーザー設定 | +| `run_as_user` | `cp0_lvgl_process.cpp`, `cp0_lvgl_pty.cpp` | 外部プロセス / PTY コマンドで権限を下げる際のユーザー設定 | ### 7.3 一時的な業務入力 @@ -327,19 +328,20 @@ cp0_config_save(); ```cpp void save_app_toggle(int idx) { - char cfg_key[64]; - snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); + std::size_t app_count = 0; + const AppDescriptor *apps = launcher_app_registry_entries(&app_count); + const AppDescriptor &desc = apps[idx]; bool enabled = menu_items_[0].sub_items[idx].toggle_state; - cp0_config_set_int(cfg_key, enabled ? 1 : 0); - cp0_config_save(); + launcher_app_registry_set_enabled(desc, enabled); + config_save(); } ``` 設定キーを変更するときは、次のすべてを同期して確認してください。 -- `UISetupPage::menu_init()` の `app_keys` / `app_labels`。 -- `UISetupPage::save_app_toggle()` の `app_keys` と常時有効リスト。 -- `launch.cpp` の `APP_ENABLED("...")`。 +- `kBuiltinApps[]` の `AppDescriptor` エントリ。 +- `UISetupPage` の `launcher_app_registry_entries()` / `save_app_toggle()`。 +- `launch.cpp` の `launcher_app_registry_is_enabled()`。 - ドキュメントとデフォルト設定。 ## 9. リソース命名の推奨事項 diff --git a/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md b/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md index cdb6097b..41413dc7 100644 --- a/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md +++ b/docs/launcher-project-guide-ja/07-input-system-and-key-mapping.md @@ -265,7 +265,7 @@ static uint32_t fzxc_to_arrow(uint32_t key) | Page | File | Main keys | | --- | --- | --- | -| `UIConsolePage` | `ui_app_console.hpp` | ESC/arrow/Enter/Backspace は PTY 制御シーケンスへ変換。HOME 関連状態は終了/外部ロックに使用 | +| `UISTPage` | `ui_app_st.hpp` | ESC/arrow/Enter/Backspace は PTY 制御シーケンスへ変換。HOME 関連状態は終了/外部ロックに使用 | | `UIGamePage` | `ui_app_game.hpp` | 矢印キーで移動、ENTER で開始/再開始、ESC で戻る | | `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN または F/X で選択、ENTER/RIGHT または C で入る/確定、ESC/LEFT または Z で戻る。一部ページは R/D 対応 | | `UIGamePage` | `ui_app_game.hpp` | 共通ページキー処理を使用。ESC で戻る | @@ -324,7 +324,7 @@ static char keycode_to_char(uint32_t key) ### 9.2 ターミナル入力 -`UIConsolePage` は `struct key_item` を直接読み、物理キーと UTF-8 テキストを PTY バイトストリームへ変換します。 +`UISTPage` は `struct key_item` を直接読み、物理キーと UTF-8 テキストを PTY バイトストリームへ変換します。 - `KEY_ENTER` -> `\r` - `KEY_BACKSPACE` -> `0x7f` @@ -364,7 +364,7 @@ LVGL_RUN_FLAGE = 1; - ホーム画面: `UILaunchPage::home_input_group()`。 - 組み込みページ: `AppPageRoot::input_group()`。 -- ネストしたターミナル: `UIConsolePage::input_group()`。 +- ネストしたターミナル: `UISTPage::input_group()`。 ページを切り替えるときは、同時に入力グループも切り替える必要があります。 diff --git a/docs/launcher-project-guide-ja/10-extension-development-guide.md b/docs/launcher-project-guide-ja/10-extension-development-guide.md index c191853a..f7d04e87 100644 --- a/docs/launcher-project-guide-ja/10-extension-development-guide.md +++ b/docs/launcher-project-guide-ja/10-extension-development-guide.md @@ -13,11 +13,11 @@ | `projects/APPLaunch/APPLaunch/` | 実行時アセットツリー。パッケージング後はデバイス上の `/usr/share/APPLaunch/` に対応 | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | デバイス側 `cp0_file_path()` パスルール | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 開発ホスト側 `cp0_file_path()` パスルール | -| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | デバイス側設定永続化。`/var/lib/applaunch/settings` に保存 | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | デバイス側設定永続化。`/var/lib/applaunch/settings` に保存 | APPLaunch には 2 種類のアプリソースがあります。 -- **組み込みページ**: APPLaunch プロセスにコンパイルされ、`app("NAME", icon, page_v)` で登録されます。開くと APPLaunch が `PageT` オブジェクトを作成し、その画面へ切り替えます。 +- **組み込みページ**: APPLaunch プロセスにコンパイルされ、`kBuiltinApps[]` で `append_page_app` を使って登録されます。開くと APPLaunch が `PageT` オブジェクトを作成し、その画面へ切り替えます。 - **外部アプリ**: 固定 `Exec` 値または `.desktop` 記述子を通じて独立プロセスとして起動されます。非ターミナルアプリでは、Launcher は LVGL タイマーを一時停止し、子プロセス終了を待ってからホームページへ戻ります。 ## 2. 組み込みページを追加する @@ -103,53 +103,32 @@ ui/generate_page_app_includes.py ### 2.3 ホームのアプリリストへ登録 -`projects/APPLaunch/main/ui/launch.cpp` を開き、`Launch::Launch()` を探します。組み込みページは次のように登録します。 +`projects/APPLaunch/main/ui/launch.cpp` を開き、`kBuiltinApps[]` に組み込み登録を追加します。 ```cpp -app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); +{{"MYTOOL", "mytool_100.png", "app_MyTool", true, false}, + nullptr, false, true, false, append_page_app}, ``` -Settings ページから表示有無を制御できるように、`APP_ENABLED` 制御セクション内へ置くことを推奨します。 +ページがデバイス側ハードウェアだけをサポートする場合、SDL2 ビルド失敗を避けるため、既存の `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` ブロック内に置いてください。 -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("MyTool")) - app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); - -#undef APP_ENABLED -``` - -登録ルール: +登録フィールド: -- 第 1 引数はホームカルーセルの表示名です。小さい画面で切れないよう短くしてください。 -- 第 2 引数はアイコンパスで、通常 `img_path("xxx_100.png")` です。 -- 第 3 引数 `page_v` は、アプリをクリックしたときに組み込みページが作成されることを意味します。 -- ページがデバイス側ハードウェアだけをサポートする場合、SDL2 ビルド失敗を避けるため `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 内に置いてください。 +- `AppDescriptor.label` はホームカルーセルの表示名です。小さい画面で切れないよう短くしてください。 +- `AppDescriptor.icon` は `cp0_file_path()` で解決されるアイコンファイル名です。 +- `AppDescriptor.config_key` は Settings 永続化用の完全な key です。例: `app_MyTool`。 +- `append_page_app` は、アプリをクリックしたときに組み込みページが作成されることを意味します。 ### 2.4 Settings ページのトグルを追加 -Settings の `Launcher` メニューから新しいページの表示を制御したい場合、`UISetupPage::menu_init()` の `app_keys` と `app_labels` を更新します。 - -例: +Settings の `Launcher` メニューは `launcher_app_registry_entries()` から生成されます。表示トグルを出すには、`kBuiltinApps[]` エントリの `AppDescriptor.configurable` を `true` にします。 ```cpp -static const char *app_keys[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Game", "Math", "MyTool" -}; - -static const char *app_labels[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Game", "Math", "My Tool" -}; +{{"MYTOOL", "mytool_100.png", "app_MyTool", true, false}, + nullptr, false, true, false, append_page_app}, ``` -`save_app_toggle()` はスイッチを `app_` として保存します。例: `app_MyTool=0`。`launch.cpp` では同じキーを読みます。 - -```cpp -cp0_config_get_int("app_MyTool", 1) -``` +`launcher_app_registry_set_enabled()` は `config_key` で switch を保存します。例: `app_MyTool=0`。`Launch::rebuild_builtin_apps()` は `launcher_app_registry_is_enabled()` を通して読み取ります。 デバイス側の永続化ファイル: @@ -222,7 +201,7 @@ APPLaunch が現在解析するフィールド: | `Name` | Yes | ホームページに表示する名前 | | `Exec` | Yes | 実行コマンド。絶対パスまたは shell コマンドを指定可能 | | `Icon` | No | アイコンパス。推奨形式は `share/images/xxx.png`、または LVGL が読める任意パス | -| `Terminal` | No | `true`/`True`/`1` の場合、組み込み `UIConsolePage` 内で実行 | +| `Terminal` | No | `true`/`True`/`1` の場合、組み込み `UISTPage` 内で実行 | | `Sysplause` | No | ターミナルアプリのみ。ターミナルコマンド終了後の一時停止動作を制御。既定は true | | `Type` | No | desktop-file 慣例との互換用。APPLaunch は現在依存していない | | `TryExec` | No | APPLaunch は現在解析しない。説明用フィールドとしてのみ使用可能 | @@ -265,7 +244,7 @@ Type=Application `launch.cpp` は 2 種類の外部アプリ起動モードをサポートします。 -- `Terminal=true`: `UIConsolePage` を作成し、APPLaunch プロセス内に PTY ターミナルを表示して `Exec` を実行します。 +- `Terminal=true`: `UISTPage` を作成し、APPLaunch プロセス内に PTY ターミナルを表示して `Exec` を実行します。 - `Terminal=false`: `cp0_process_exec_blocking()` を呼んで外部プロセスを開始します。APPLaunch は LVGL タイマーと入力グループを一時停止し、子プロセス終了を待ってからホームページを復元します。 非ターミナル外部アプリからの復帰は次の挙動に依存します。 @@ -362,11 +341,10 @@ Settings ページは `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` に 手順: -1. `UISetupPage::menu_init()` の `app_keys` に `MyTool` のような内部キーを追加します。 -2. 同じ場所の `app_labels` に `My Tool` のような表示ラベルを追加します。 -3. `launch.cpp` でアプリ登録時に同じキーを使います: `APP_ENABLED("MyTool")`。 -4. Settings ページを開き、`Launcher` メニューへ入り、O/X を切り替えます。 -5. ホームへ戻った後にリストが更新されない場合は APPLaunch を再起動します。現在の固定/組み込みリストは `Launch` 構築時に設定を読みます。 +1. `kBuiltinApps[]` でアプリの `AppDescriptor` を追加または更新します。 +2. `configurable=true` にし、`app_MyTool` のような完全な `config_key` を選びます。 +3. Settings ページを開き、`Launcher` メニューへ入り、O/X を切り替えます。 +4. Settings は `launcher_app_registry_set_enabled()` を呼び、Launcher に組み込みアプリリストの再構築を通知します。 ### 5.2 通常設定を追加 @@ -390,7 +368,7 @@ Settings ページは `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` に ### 5.3 設定永続化場所 -デバイス側設定実装: `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`。 +デバイス側設定実装: `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp`。 - 設定ディレクトリ: `/var/lib/applaunch` - 設定ファイル: `/var/lib/applaunch/settings` diff --git a/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md b/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md index 0faa1e56..7e299cb8 100644 --- a/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md +++ b/docs/launcher-project-guide-ja/11-debugging-and-troubleshooting.md @@ -125,8 +125,8 @@ sudo cat /var/lib/applaunch/settings | `duplicate Exec` | `launch.cpp` | `.desktop` の Exec が既存アプリと同じ | | `Launching terminal app` | `launch.cpp` | コマンド実行のため組み込みターミナルページへ入る | | `Launching external app` | `launch.cpp` | 非ターミナル外部プログラムを開始 | -| `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | 外部アプリ実行中に親プロセスが ESC を読んだ | -| `[cp0] Returned to launcher` | `cp0_app_process.cpp` | 外部アプリが終了し、ホームへ戻る準備中 | +| `[CP0-APP] ESC DOWN/UP` | `cp0_lvgl_process.cpp` | 外部アプリ実行中に親プロセスが ESC を読んだ | +| `[cp0] Returned to launcher` | `cp0_lvgl_process.cpp` | 外部アプリが終了し、ホームへ戻る準備中 | | `[HOME_STATUS] connected=` | `launch.cpp` | ホームステータスバーの WiFi/battery 状態を更新 | ## 3. 黒画面のトラブルシューティング diff --git a/docs/launcher-project-guide-ja/12-common-modification-entry-points.md b/docs/launcher-project-guide-ja/12-common-modification-entry-points.md index 29fbd948..c73aa644 100644 --- a/docs/launcher-project-guide-ja/12-common-modification-entry-points.md +++ b/docs/launcher-project-guide-ja/12-common-modification-entry-points.md @@ -12,8 +12,8 @@ git status --short | Task | Main files/directories | Key points | Verification | | --- | --- | --- | --- | | 組み込みページを追加 | `projects/APPLaunch/main/ui/page_app/` | `ui_app_xxx.hpp` を作成し、`AppPage` を継承 | SDL2 でビルドし、ページを開く | -| ホームに組み込みページを登録 | `projects/APPLaunch/main/ui/launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | ホームカルーセルにアイコンが表示される | -| 組み込みページ表示トグルを制御 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/launch.cpp` | Settings ページが `app_Key` を書き、Launcher が `APP_ENABLED("Key")` を読む | Settings で切替後、再起動またはホーム更新 | +| ホームに組み込みページを登録 | `projects/APPLaunch/main/ui/launch.cpp` | `kBuiltinApps[]` に `append_page_app` エントリを追加 | ホームカルーセルにアイコンが表示される | +| 組み込みページ表示トグルを制御 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/launch.cpp` | Settings が `AppDescriptor.config_key` を書き、Launcher が `launcher_app_registry_is_enabled()` を読む | Settings で切替後、再起動またはホーム更新 | | 外部 `.desktop` アプリを追加 | `projects/APPLaunch/APPLaunch/applications/` | ファイル名は `.desktop` で終わり、`Name` と `Exec` を含む必要がある | skip ログなしでホームに表示される | | アイコンを追加 | `projects/APPLaunch/APPLaunch/share/images/` | 組み込みページは `img_path()`、`.desktop` は `Icon=share/images/xxx.png` を使う | `missing/unreadable` ログがない | | 効果音を追加 | `projects/APPLaunch/APPLaunch/share/audio/` | ページは `audio_path()` と `cp0_signal_audio_api()` を使う | デバイスで音が鳴る | @@ -22,10 +22,10 @@ git status --short | カルーセルアニメーションを変更 | `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | カード移動、scale、opacity などのアニメーション | SDL2 で左右切替を繰り返す | | ホームステータスバーを変更 | `projects/APPLaunch/main/ui/launch.cpp`, `projects/APPLaunch/main/ui/ui.cpp` | `update_home_status_bar()` が WiFi/time/battery を更新 | `[HOME_STATUS]` ログを確認 | | Settings メニューを変更 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `menu_init()` に `MenuItem`/`SubItem` を追加 | SETTING ページに入りテスト | -| 設定保存ロジックを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 現在は `/var/lib/applaunch/settings` に保存、最大 32 エントリ | settings ファイルを確認 | +| 設定保存ロジックを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | 現在は `/var/lib/applaunch/settings` に保存、最大 32 エントリ | settings ファイルを確認 | | アセットパスルールを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | デバイスと SDL2 の整合性を考慮 | SDL2 とデバイスの両方でアセット確認 | -| 外部アプリ起動/復帰を変更 | `projects/APPLaunch/main/ui/launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | 外部アプリ起動、ESC で戻る | -| ターミナルアプリを変更 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、コマンド実行、入出力 | `Terminal=true` アプリで確認 | +| 外部アプリ起動/復帰を変更 | `projects/APPLaunch/main/ui/launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | 外部アプリ起動、ESC で戻る | +| ターミナルアプリを変更 | `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp` | PTY、コマンド実行、入出力 | `Terminal=true` アプリで確認 | | 入力マッピングを変更 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | デバイスと SDL2 の入力差分 | `evtest` + SDL2 keyboard | | 起動フローを変更 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、main loop | `[BOOT]` ログを確認 | | ビルド依存関係を変更 | `projects/APPLaunch/main/SConstruct` | `SRCS`, `INCLUDE`, `REQUIREMENTS`, `STATIC_FILES` | scons build | @@ -58,7 +58,6 @@ git status --short | Page/feature | File | Registered name or icon | Description | | --- | --- | --- | --- | -| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 組み込みゲームエントリ | | SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings ページ。app toggles、brightness、volume、WiFi、camera などを含む | | GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 組み込みゲームエントリ | | Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass ページ | @@ -70,16 +69,19 @@ git status --short | CAMERA | `projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | Camera ページ。デバイスで有効 | | LORA | `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa ページ。デバイスで有効 | | TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game。デバイスで有効 | -| CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`。bash、python、`Terminal=true` アプリで使用 | +| CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp` | `CLI` / `cli_100.png` | `UISTPage`。bash、python、`Terminal=true` アプリで使用 | -`Launch::Launch()` の固定登録エントリ: +固定エントリは `kBuiltinApps[]` で宣言されます: ```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, +}; ``` ## 4. 外部アプリ入口表 @@ -91,10 +93,10 @@ app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v Launcher | `app_` | `ui_app_setup.hpp` の `save_app_toggle()`、`launch.cpp` の `APP_ENABLED()` | -| Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | +| App visibility toggle | SETTING -> Launcher | `AppDescriptor.config_key` | `ui_app_setup.hpp` の `save_app_toggle()`、`launch.cpp` の `launcher_app_registry_is_enabled()` | +| Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp` | | Screen-off timeout | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | | Volume | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`, `cp0_volume_read/write()` | | Camera resolution | SETTING -> Camera -> Resolution | `cam_resolution` | `ui_app_setup.hpp`。camera page が読み取る | | Startup mode | Settings page の関連選択 | `startup_mode` | `ui_app_setup.hpp` | | USB extension port | SETTING -> ExtPort | `extport_usb` | `ui_app_setup.hpp` | | 5V output | SETTING -> ExtPort | `extport_5vout` | `ui_app_setup.hpp` | -| External app runtime user | 手動設定 | `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | +| External app runtime user | 手動設定 | `run_as_user` | `cp0_lvgl_process.cpp`, `cp0_lvgl_pty.cpp` | 設定実装: | File | Description | | --- | --- | | `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` | `cp0_config_get_int/set_int/get_str/set_str/save` の宣言 | -| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | デバイス側設定 read/write。`/var/lib/applaunch/settings` に保存 | -| `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` | SDL2 互換実装 | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | デバイス側設定 read/write。`/var/lib/applaunch/settings` に保存 | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp` | SDL2 互換実装 | | `ext_components/cp0_lvgl/src/commount.c` | 起動時に保存済み輝度と音量を適用 | ## 7. ビルド入口表 @@ -174,12 +176,12 @@ Type=Application | framebuffer/display | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c` | | Keyboard input | `ext_components/cp0_lvgl/include/keyboard_input.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | | File paths | `ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | -| Process | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp` | -| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp` | -| Audio | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c` | -| Settings/brightness/volume | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp` | -| Network/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp` | -| Screenshot | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp` | +| Process | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp` | +| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp` | +| Audio | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp` | +| Settings/brightness/volume | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp` | +| Network/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp` | +| Screenshot | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp` | | Camera | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp` | Device camera | `ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp` | ## 9. デバッグコマンド早見表 diff --git a/docs/launcher-project-guide/00-overview-and-reading-path.md b/docs/launcher-project-guide/00-overview-and-reading-path.md index f8cb8a18..3e117715 100644 --- a/docs/launcher-project-guide/00-overview-and-reading-path.md +++ b/docs/launcher-project-guide/00-overview-and-reading-path.md @@ -34,7 +34,7 @@ LVGL 9.5 UI framework APPLaunch home, status bar, carousel, application manager | +--> Built-in page AppPage - +--> PTY terminal application UIConsolePage + +--> PTY ST terminal application UISTPage +--> External independent process cp0_process_exec_blocking() ``` @@ -80,7 +80,7 @@ If you only want to complete a specific task: - **APPLaunch**: the launcher project or launcher process. - **Home screen**: the main screen of APPLaunch, with the status bar and application carousel. - **Built-in page**: a page class compiled into the APPLaunch process, such as `UISetupPage`. -- **Terminal application**: a command run inside APPLaunch through `UIConsolePage` + PTY, such as `bash`. +- **Terminal application**: a command run inside APPLaunch through `UISTPage` + PTY, such as `bash`. - **External application**: an independent executable program. When launched, APPLaunch pauses its own LVGL rendering and waits for the external program to exit. - **Resource tree**: runtime files such as `APPLaunch/share/images`, `APPLaunch/share/audio`, and `APPLaunch/share/font`. - **On-device**: the AArch64 Linux environment on M5CardputerZero. diff --git a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md index f9b590aa..ef7cdfae 100644 --- a/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md +++ b/docs/launcher-project-guide/01-project-layout-and-module-responsibilities.md @@ -196,7 +196,7 @@ main/ui/ main/ui/page_app/ ├── ui_app_camera.hpp ├── ui_app_compass.hpp -├── ui_app_console.hpp +├── ui_app_st.hpp ├── ui_app_file.hpp ├── ui_app_game.hpp ├── ui_app_lora.hpp @@ -230,7 +230,7 @@ ui_init() Launch ├── UILaunchPage::panel()/label() - ├── page_v + ├── append_page_app / page_v ├── cp0_file_path() ├── cp0_process_* ├── cp0_dir_watch_* diff --git a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md index 7ebefe4d..ca4f4e7d 100644 --- a/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md +++ b/docs/launcher-project-guide/04-application-model-and-launch-mechanism.md @@ -22,7 +22,7 @@ Home center card -> Launch::launch_app() -> app.launch(this) ├── Built-in page: new PageT + lv_disp_load_scr() - ├── Terminal app: UIConsolePage + PTY exec() + ├── Terminal app: UISTPage + PTY exec() └── External app: cp0_process_exec_blocking() ``` @@ -33,7 +33,7 @@ Home center card | `projects/APPLaunch/main/ui/launch.h` | Public `Launch` interface and app model declarations | | `projects/APPLaunch/main/ui/launch.cpp` | `app`, `Launch`, application list, launch logic, `.desktop` scanning | | `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Forwards Enter / click events to `Launch::launch_app()` | -| `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | Terminal page `UIConsolePage` | +| `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp` | Terminal page `UISTPage` | | `projects/APPLaunch/main/ui/page_app/*.hpp` | Built-in pages such as settings, game, file, camera, and LoRa | | `projects/APPLaunch/APPLaunch/applications/` | Runtime `.desktop` application descriptor directory | | `ext_components/cp0_lvgl` | Lower-level capabilities such as process launch, PTY, directory watching, and path resolution | @@ -81,8 +81,8 @@ Three application categories: | Type | Construction | Launch function | Examples | | --- | --- | --- | --- | -| Built-in page | `page_v` | Constructs a page and calls `lv_disp_load_scr()` | `GAME`, `SETTING`, `Compass` | -| Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`, `CLI` | +| Built-in page | `page_v` / `append_page_app` | Constructs a page and calls `lv_disp_load_scr()` | `CLI`, `GAME`, `SETTING`, `Compass` | +| Terminal command | `exec, terminal=true` | `launch_Exec_in_terminal()` creates `UISTPage` and calls `exec()` | `Python`, `Terminal=true` `.desktop` apps | | External process | `exec, terminal=false` | `launch_Exec()` | AppStore, Calculator | ## 5. Fixed Application Registration @@ -96,7 +96,7 @@ constexpr BuiltinAppRegistration kBuiltinApps[] = { {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, {{"STORE", "store_100.png", "app_Store", false, true}, "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, - {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, {{"MATH", "math_100.png", "app_Math", true, false}, @@ -166,7 +166,7 @@ Enter ## 7. Terminal Application Launch Mechanism -Terminal applications use `UIConsolePage`, and the external command runs inside a terminal page in the APPLaunch process: +Terminal applications use `UISTPage`, and the external command runs inside a terminal page in the APPLaunch process: ```cpp void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) @@ -174,7 +174,7 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) ui_loading_show("Loading..."); lv_refr_now(NULL); - auto p = std::make_shared(); + auto p = std::make_shared(); app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); @@ -190,10 +190,10 @@ Typical entries: ```text Python -> exec = "python3", terminal = true -CLI -> exec = "bash", terminal = true +CLI -> built-in UISTPage, starts interactive bash internally ``` -Compared with built-in pages, terminal applications add one extra step: `p->exec(exec)`. They usually interact with the command through a PTY. What the user sees is `UIConsolePage`, not a separate UI outside APPLaunch. +Compared with built-in pages, terminal applications add one extra step: `p->exec(exec)`. They usually interact with the command through a PTY. What the user sees is `UISTPage`, not a separate UI outside APPLaunch. ## 8. External Standalone Application Launch Mechanism @@ -256,15 +256,11 @@ Enter external app -> LVGL_RUN_FLAGE=1 ``` -`STORE` is an example external application: +`STORE` is a fixed entry that launches an external application: ```cpp -app_list.emplace_back("STORE", - img_path("store_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", - false, - true, - true); +{{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, ``` Here `run_as_root=true` is passed to `launch_Exec(exec, run_as_root)`, and then converted to `keep_root ? 1 : 0`. @@ -331,7 +327,7 @@ Icon=share/images/e-Mail_80.png | `Name` | Yes | Home-screen display title | | `Exec` | Yes | Launch command | | `Icon` | No | Icon path | -| `Terminal` | No | `true/True/1` means launch through `UIConsolePage` | +| `Terminal` | No | `true/True/1` means launch through `UISTPage` | | `Sysplause` | No | Pause policy passed to the terminal page; defaults to true | Registration logic: diff --git a/docs/launcher-project-guide/05-built-in-page-framework.md b/docs/launcher-project-guide/05-built-in-page-framework.md index cc04d345..62b7153b 100644 --- a/docs/launcher-project-guide/05-built-in-page-framework.md +++ b/docs/launcher-project-guide/05-built-in-page-framework.md @@ -20,7 +20,7 @@ UILaunchPage home carousel Launch::launch_app() | +-- External command: cp0_process_exec_blocking() - +-- Terminal command: UIConsolePage + PTY + +-- Terminal command: UISTPage + PTY +-- Built-in page: std::make_shared() | v @@ -177,14 +177,13 @@ Page implementations are concentrated in `projects/APPLaunch/main/ui/page_app/`. | Page class | File | Launcher name | Inheritance | Description | | --- | --- | --- | --- | --- | -| `UIConsolePage` | `ui_app_console.hpp` | `CLI` or terminal external command | `AppPage` | Terminal emulator, PTY read/write, supports ANSI/VT sequences and keyboard escape sequences | +| `UISTPage` | `ui_app_st.hpp` | `CLI` or terminal external command | `AppPage` | Terminal emulator, PTY read/write, supports ANSI/VT sequences and keyboard escape sequences | | `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | Snake game, full-screen custom drawing, driven by an LVGL timer | | `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | System settings, application toggles, brightness, volume, WiFi, camera resolution, and more | -| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPage` | Built-in game entry | | `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | Compass page, sensor thread + UI timer | | `UIIpPanelPage` | `ui_app_ip_panel.hpp` | `IP_PANEL` | `AppPage` | Network interface/IP information list, refreshed every second | | `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | File browser, directory list and enter/back navigation | -| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH parameter input, embeds `UIConsolePage` after connection | +| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH parameter input, embeds `UISTPage` after connection | | `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh message list, input overlay, send/refresh | | `UIRecPage` | `ui_app_rec.hpp` | `REC` | Custom `rec_page` | Recording/playback/file list with asynchronous resource management | | `UICameraPage` | `ui_app_camera.hpp` | `CAMERA` | `AppPage` | Camera preview, gallery, capture, status page | @@ -195,14 +194,17 @@ Page implementations are concentrated in `projects/APPLaunch/main/ui/page_app/`. ## 6. Page Registration and Display Order -Built-in pages are inserted into `app_list` in `Launch::Launch()`. The first 5 fixed applications initialize the 5 home carousel slots first: +Built-in entries are declared in `kBuiltinApps[]` in `launch.cpp`. The first 5 enabled entries initialize the 5 home carousel slots first: ```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, +}; ``` Built-in page visibility is now driven by `kBuiltinApps[]` and `AppDescriptor.config_key`. `Launch::rebuild_builtin_apps()` calls `launcher_app_registry_is_enabled()` before appending each descriptor, and Settings changes call `launcher_app_registry_set_enabled()` followed by `Launch::applications_reload()`. @@ -282,19 +284,19 @@ A new full-screen page may inherit `AppPageRoot`, but it must handle the `320x17 ## 9. Nested Pages and Special Pages -`UISSHPage` is a typical nested page: while entering SSH parameters, the keyboard is handled by `UISSHPage`; after connection, it creates `UIConsolePage` and switches the screen and input group. +`UISSHPage` is a typical nested page: while entering SSH parameters, the keyboard is handled by `UISSHPage`; after connection, it creates `UISTPage` and switches the screen and input group. ```cpp -console_page_ = std::make_shared(); -console_page_->navigate_home = [this]() { - console_page_.reset(); +terminal_page_ = std::make_shared(); +terminal_page_->navigate_home = [this]() { + terminal_page_.reset(); view_state_ = ViewState::INPUT; lv_disp_load_scr(this->screen()); lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); }; -lv_disp_load_scr(console_page_->screen()); -lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); +lv_disp_load_scr(terminal_page_->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), terminal_page_->input_group()); ``` Special care is required for this type of page: @@ -320,4 +322,4 @@ Built-in pages do not directly manipulate the home carousel. After returning hom - Do not directly access home global objects from a page unless it is clearly a home-screen feature. - For page titles, call `set_page_title()` instead of modifying the internal top-bar label directly. - Every page that can exit should support `KEY_ESC` and call `navigate_home` or return to the previous view. -- Page toggle keys must stay consistent with `UISetupPage::save_app_toggle()` and `APP_ENABLED()` in `launch.cpp`. +- Page toggle keys must stay consistent between `AppDescriptor.config_key`, `UISetupPage::save_app_toggle()`, and `launcher_app_registry_is_enabled()` in `launch.cpp`. diff --git a/docs/launcher-project-guide/06-resources-and-configuration.md b/docs/launcher-project-guide/06-resources-and-configuration.md index 36903428..637c8144 100644 --- a/docs/launcher-project-guide/06-resources-and-configuration.md +++ b/docs/launcher-project-guide/06-resources-and-configuration.md @@ -145,7 +145,8 @@ Therefore, the returned `const char *` is stable within the thread and can be pa Common home-screen and built-in-page usage: ```cpp -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +{{"GAME", "game_100.png", "app_Game", false, true}, + nullptr, false, true, false, append_page_app}, lv_obj_set_style_bg_img_src(time_panel_, cp0_file_path_c("status_time_background.png"), @@ -212,7 +213,7 @@ Supported keys: | `Name` | Yes | Carousel display name | | `Exec` | Yes | Launch command or executable path | | `Icon` | No | Icon path; can be `share/images/...` or a path readable by LVGL | -| `Terminal` | No | `true`/`True`/`1` means run inside `UIConsolePage` | +| `Terminal` | No | `true`/`True`/`1` means run inside `UISTPage` | | `Sysplause` | No | Whether to pause and wait for user confirmation after a terminal command exits; defaults to `true` | Example: @@ -253,8 +254,8 @@ Usage conventions: - Always provide a default value when reading, so pages keep running if a setting is missing. - Call `cp0_config_save()` after writing to persist changes. -- The device-side implementation is in `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`. -- The SDL compatibility implementation is in `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp`. +- The device-side implementation is in `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp`. +- The SDL compatibility implementation is in `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp`. Typical usage: @@ -302,7 +303,7 @@ Note: `Compass` currently has no corresponding `app_Compass` setting and is adde | `startup_mode` | `UISetupPage` | Startup mode, currently `Launcher` / `CLI` | | `extport_usb` | `UISetupPage` | Expansion-port USB toggle | | `extport_5vout` | `UISetupPage` | Expansion-port 5V output toggle | -| `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | User configuration for dropping privileges in external processes / PTY commands | +| `run_as_user` | `cp0_lvgl_process.cpp`, `cp0_lvgl_pty.cpp` | User configuration for dropping privileges in external processes / PTY commands | ### 7.3 Temporary Business Inputs @@ -327,19 +328,20 @@ Example: ```cpp void save_app_toggle(int idx) { - char cfg_key[64]; - snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); + std::size_t app_count = 0; + const AppDescriptor *apps = launcher_app_registry_entries(&app_count); + const AppDescriptor &desc = apps[idx]; bool enabled = menu_items_[0].sub_items[idx].toggle_state; - cp0_config_set_int(cfg_key, enabled ? 1 : 0); - cp0_config_save(); + launcher_app_registry_set_enabled(desc, enabled); + config_save(); } ``` When changing configuration keys, check all of the following in sync: -- `app_keys` / `app_labels` in `UISetupPage::menu_init()`. -- The `app_keys` and always-on list in `UISetupPage::save_app_toggle()`. -- `APP_ENABLED("...")` in `launch.cpp`. +- `AppDescriptor` entries in `kBuiltinApps[]`. +- `launcher_app_registry_entries()` / `save_app_toggle()` in `UISetupPage`. +- `launcher_app_registry_is_enabled()` in `launch.cpp`. - Documentation and default configuration. ## 9. Resource Naming Recommendations diff --git a/docs/launcher-project-guide/07-input-system-and-key-mapping.md b/docs/launcher-project-guide/07-input-system-and-key-mapping.md index b4340471..9b64882e 100644 --- a/docs/launcher-project-guide/07-input-system-and-key-mapping.md +++ b/docs/launcher-project-guide/07-input-system-and-key-mapping.md @@ -265,7 +265,7 @@ Each page independently binds `LV_EVENT_KEYBOARD` on its `root_screen_`. Common | Page | File | Main keys | | --- | --- | --- | -| `UIConsolePage` | `ui_app_console.hpp` | ESC/arrow/Enter/Backspace are converted to PTY control sequences; HOME-related state is used for exit/external locks | +| `UISTPage` | `ui_app_st.hpp` | ESC/arrow/Enter/Backspace are converted to PTY control sequences; HOME-related state is used for exit/external locks | | `UIGamePage` | `ui_app_game.hpp` | Arrow keys move, ENTER starts/restarts, ESC returns | | `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN or F/X selects, ENTER/RIGHT or C enters/confirms, ESC/LEFT or Z returns, some pages support R/D | | `UIGamePage` | `ui_app_game.hpp` | uses the common page key handling; ESC returns | @@ -324,7 +324,7 @@ This approach is simple, but it does not support Shift uppercase, input methods, ### 9.2 Terminal Input -`UIConsolePage` reads `struct key_item` directly and converts physical keys and UTF-8 text to a PTY byte stream: +`UISTPage` reads `struct key_item` directly and converts physical keys and UTF-8 text to a PTY byte stream: - `KEY_ENTER` -> `\r` - `KEY_BACKSPACE` -> `0x7f` @@ -364,7 +364,7 @@ The home screen and pages each have their own LVGL group: - Home screen: `UILaunchPage::home_input_group()`. - Built-in pages: `AppPageRoot::input_group()`. -- Nested terminal: `UIConsolePage::input_group()`. +- Nested terminal: `UISTPage::input_group()`. When switching pages, the input group must be switched at the same time: diff --git a/docs/launcher-project-guide/10-extension-development-guide.md b/docs/launcher-project-guide/10-extension-development-guide.md index 6514f232..e37a2b5d 100644 --- a/docs/launcher-project-guide/10-extension-development-guide.md +++ b/docs/launcher-project-guide/10-extension-development-guide.md @@ -13,11 +13,11 @@ This chapter explains how to extend APPLaunch, focusing on four common change ty | `projects/APPLaunch/APPLaunch/` | Runtime asset tree; after packaging it maps to `/usr/share/APPLaunch/` on the device | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | Device-side `cp0_file_path()` path rules | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 development-host `cp0_file_path()` path rules | -| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Device-side settings persistence, saved to `/var/lib/applaunch/settings` | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | Device-side settings persistence, saved to `/var/lib/applaunch/settings` | APPLaunch has two kinds of app sources: -- **Built-in pages**: compiled into the APPLaunch process and registered with `app("NAME", icon, page_v)`. When opened, APPLaunch creates a `PageT` object and switches to its screen. +- **Built-in pages**: compiled into the APPLaunch process and registered in `kBuiltinApps[]` with `append_page_app`. When opened, APPLaunch creates a `PageT` object and switches to its screen. - **External apps**: launched as independent processes through a fixed `Exec` value or a `.desktop` descriptor. For non-terminal apps, the Launcher pauses its LVGL timer, waits for the child process to exit, and then returns to the home page. ## 2. Adding a Built-in Page @@ -103,53 +103,32 @@ If you check manually, `generated/page_app.h` should contain: ### 2.3 Register the Page in the Home App List -Open `projects/APPLaunch/main/ui/launch.cpp` and find `Launch::Launch()`. Register a built-in page like this: +Open `projects/APPLaunch/main/ui/launch.cpp` and add a built-in registration to `kBuiltinApps[]`: ```cpp -app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); +{{"MYTOOL", "mytool_100.png", "app_MyTool", true, false}, + nullptr, false, true, false, append_page_app}, ``` -It is recommended to place it inside the `APP_ENABLED` control section so the Settings page can later control whether it is shown: +If the page only supports device-side hardware, place the entry in the existing device-only `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` block to avoid SDL2 build failures. -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("MyTool")) - app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); - -#undef APP_ENABLED -``` - -Registration rules: +Registration fields: -- The first argument is the display name in the home carousel. Keep it short to avoid truncation on the small screen. -- The second argument is the icon path, usually `img_path("xxx_100.png")`. -- The third argument, `page_v`, means a built-in page is created when the app is clicked. -- If the page only supports device-side hardware, place it inside `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` to avoid SDL2 build failures. +- `AppDescriptor.label` is the display name in the home carousel. Keep it short to avoid truncation on the small screen. +- `AppDescriptor.icon` is the icon filename resolved through `cp0_file_path()`. +- `AppDescriptor.config_key` is the full Settings persistence key, such as `app_MyTool`. +- `append_page_app` means a built-in page is created when the app is clicked. ### 2.4 Add a Settings Page Toggle -If you want the `Launcher` menu in Settings to control whether the new page is shown, update `app_keys` and `app_labels` in `UISetupPage::menu_init()`. - -Example: +The `Launcher` menu in Settings is generated from `launcher_app_registry_entries()`. To expose a toggle, set `AppDescriptor.configurable=true` in the `kBuiltinApps[]` entry: ```cpp -static const char *app_keys[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Game", "Math", "MyTool" -}; - -static const char *app_labels[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Game", "Math", "My Tool" -}; +{{"MYTOOL", "mytool_100.png", "app_MyTool", true, false}, + nullptr, false, true, false, append_page_app}, ``` -`save_app_toggle()` stores the switch as `app_`, for example `app_MyTool=0`. Read the same key in `launch.cpp`: - -```cpp -cp0_config_get_int("app_MyTool", 1) -``` +`launcher_app_registry_set_enabled()` stores the switch using `config_key`, for example `app_MyTool=0`, and `Launch::rebuild_builtin_apps()` reads it through `launcher_app_registry_is_enabled()`. The device-side persistence file is: @@ -222,7 +201,7 @@ Fields currently parsed by APPLaunch: | `Name` | Yes | Display name on the home page | | `Exec` | Yes | Command to execute; can be an absolute path or a shell command | | `Icon` | No | Icon path; recommended format is `share/images/xxx.png` or any path readable by LVGL | -| `Terminal` | No | `true`/`True`/`1` means run in the built-in `UIConsolePage` | +| `Terminal` | No | `true`/`True`/`1` means run in the built-in `UISTPage` | | `Sysplause` | No | Terminal apps only; controls pause behavior after the terminal command ends, default true | | `Type` | No | Kept for desktop-file convention compatibility; APPLaunch does not currently depend on it | | `TryExec` | No | Not currently parsed by APPLaunch; can only serve as a descriptive field | @@ -265,7 +244,7 @@ Type=Application `launch.cpp` supports two external-app launch modes: -- `Terminal=true`: creates `UIConsolePage`, displays a PTY terminal inside the APPLaunch process, and executes `Exec`. +- `Terminal=true`: creates `UISTPage`, displays a PTY terminal inside the APPLaunch process, and executes `Exec`. - `Terminal=false`: calls `cp0_process_exec_blocking()` to start an external process. APPLaunch pauses the LVGL timer and input group, waits for the child process to exit, and then restores the home page. Returning from non-terminal external apps depends on these behaviors: @@ -362,11 +341,10 @@ The Settings page is centralized in `projects/APPLaunch/main/ui/page_app/ui_app_ Steps: -1. Add an internal key such as `MyTool` to `app_keys` in `UISetupPage::menu_init()`. -2. Add a display label such as `My Tool` to `app_labels` in the same location. -3. Use the same key when registering the app in `launch.cpp`: `APP_ENABLED("MyTool")`. -4. Open the Settings page, enter the `Launcher` menu, and toggle O/X. -5. If the list does not refresh after returning to the home page, restart APPLaunch. The current fixed/built-in list reads configuration when `Launch` is constructed. +1. Add or update the app's `AppDescriptor` in `kBuiltinApps[]`. +2. Set `configurable=true` and choose a full `config_key`, such as `app_MyTool`. +3. Open the Settings page, enter the `Launcher` menu, and toggle O/X. +4. Settings calls `launcher_app_registry_set_enabled()` and notifies the Launcher to rebuild the built-in list. ### 5.2 Add a Regular Setting @@ -390,7 +368,7 @@ For second-level or third-level pages that choose values, refer to these existin ### 5.3 Configuration Persistence Location -Device-side configuration implementation: `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`. +Device-side configuration implementation: `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp`. - Configuration directory: `/var/lib/applaunch` - Configuration file: `/var/lib/applaunch/settings` diff --git a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md index b5cb01be..4ed4a309 100644 --- a/docs/launcher-project-guide/11-debugging-and-troubleshooting.md +++ b/docs/launcher-project-guide/11-debugging-and-troubleshooting.md @@ -125,8 +125,8 @@ Common configuration keys: | `duplicate Exec` | `launch.cpp` | `.desktop` has the same Exec as an existing app | | `Launching terminal app` | `launch.cpp` | Entering the built-in terminal page to run a command | | `Launching external app` | `launch.cpp` | Starting a non-terminal external program | -| `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | Parent process read ESC while an external app was running | -| `[cp0] Returned to launcher` | `cp0_app_process.cpp` | External app exited; preparing to return home | +| `[CP0-APP] ESC DOWN/UP` | `cp0_lvgl_process.cpp` | Parent process read ESC while an external app was running | +| `[cp0] Returned to launcher` | `cp0_lvgl_process.cpp` | External app exited; preparing to return home | | `[HOME_STATUS] connected=` | `launch.cpp` | Home status bar refreshed WiFi/battery state | ## 3. Black Screen Troubleshooting diff --git a/docs/launcher-project-guide/12-common-modification-entry-points.md b/docs/launcher-project-guide/12-common-modification-entry-points.md index 2d447472..7a2e649f 100644 --- a/docs/launcher-project-guide/12-common-modification-entry-points.md +++ b/docs/launcher-project-guide/12-common-modification-entry-points.md @@ -12,8 +12,8 @@ git status --short | Task | Main files/directories | Key points | Verification | | --- | --- | --- | --- | | Add a built-in page | `projects/APPLaunch/main/ui/page_app/` | Create `ui_app_xxx.hpp` and inherit from `AppPage` | Build with SDL2 and open the page | -| Register a built-in page on home | `projects/APPLaunch/main/ui/launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | Icon appears in the home carousel | -| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/launch.cpp` | Settings page writes `app_Key`, Launcher reads `APP_ENABLED("Key")` | Toggle in Settings, then restart or refresh home | +| Register a built-in page on home | `projects/APPLaunch/main/ui/launch.cpp` | Add a `kBuiltinApps[]` entry with `append_page_app` | Icon appears in the home carousel | +| Control built-in page visibility toggle | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`, `projects/APPLaunch/main/ui/launch.cpp` | Settings writes `AppDescriptor.config_key`; Launcher reads `launcher_app_registry_is_enabled()` | Toggle in Settings, then restart or refresh home | | Add external `.desktop` app | `projects/APPLaunch/APPLaunch/applications/` | Filename must end in `.desktop` and include `Name` and `Exec` | No skip logs; app appears on home | | Add icon | `projects/APPLaunch/APPLaunch/share/images/` | Built-in pages use `img_path()`, `.desktop` uses `Icon=share/images/xxx.png` | No `missing/unreadable` logs | | Add sound effect | `projects/APPLaunch/APPLaunch/share/audio/` | Pages use `audio_path()` and `cp0_signal_audio_api()` | Sound plays on device | @@ -22,10 +22,10 @@ git status --short | Change carousel animation | `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | Card movement, scale, opacity, and other animations | Switch left/right repeatedly in SDL2 | | Change home status bar | `projects/APPLaunch/main/ui/launch.cpp`, `projects/APPLaunch/main/ui/ui.cpp` | `update_home_status_bar()` refreshes WiFi/time/battery | Check `[HOME_STATUS]` logs | | Change Settings menu | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | Add `MenuItem`/`SubItem` in `menu_init()` | Enter the SETTING page and test | -| Change configuration saving logic | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Currently saves to `/var/lib/applaunch/settings`, max 32 entries | Inspect the settings file | +| Change configuration saving logic | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | Currently saves to `/var/lib/applaunch/settings`, max 32 entries | Inspect the settings file | | Change asset path rules | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | Consider device and SDL2 consistently | Check assets on both SDL2 and device | -| Change external app launch/return | `projects/APPLaunch/main/ui/launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | External app starts, ESC returns | -| Change terminal apps | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY, command execution, input/output | Verify with a `Terminal=true` app | +| Change external app launch/return | `projects/APPLaunch/main/ui/launch.cpp`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp` | `launch_Exec()`, `cp0_process_exec_blocking()` | External app starts, ESC returns | +| Change terminal apps | `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp` | PTY, command execution, input/output | Verify with a `Terminal=true` app | | Change input mapping | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`, `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | Device and SDL2 input differences | `evtest` + SDL2 keyboard | | Change startup flow | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`, `cp0_lvgl_init()`, `ui_init()`, main loop | Check `[BOOT]` logs | | Change build dependencies | `projects/APPLaunch/main/SConstruct` | `SRCS`, `INCLUDE`, `REQUIREMENTS`, `STATIC_FILES` | scons build | @@ -58,7 +58,6 @@ git status --short | Page/feature | File | Registered name or icon | Description | | --- | --- | --- | --- | -| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | | SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | Settings page, including app toggles, brightness, volume, WiFi, camera, etc. | | GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | Built-in game entry | | Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | Compass page | @@ -70,16 +69,19 @@ git status --short | CAMERA | `projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | Camera page, enabled on device | | LORA | `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa page, enabled on device | | TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | Tank game, enabled on device | -| CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`, used by bash, python, and `Terminal=true` apps | +| CLI/terminal | `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp` | `CLI` / `cli_100.png` | `UISTPage`, used by bash, python, and `Terminal=true` apps | -Fixed registration entry in `Launch::Launch()`: +Fixed entries are declared in `kBuiltinApps[]`: ```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, +}; ``` ## 4. External App Entry Table @@ -91,10 +93,10 @@ app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v Launcher | `app_` | `save_app_toggle()` in `ui_app_setup.hpp`, `APP_ENABLED()` in `launch.cpp` | -| Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | +| App visibility toggle | SETTING -> Launcher | `AppDescriptor.config_key` | `save_app_toggle()` in `ui_app_setup.hpp`, `launcher_app_registry_is_enabled()` in `launch.cpp` | +| Brightness | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`, `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp` | | Screen-off timeout | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | | Volume | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`, `cp0_volume_read/write()` | | Camera resolution | SETTING -> Camera -> Resolution | `cam_resolution` | `ui_app_setup.hpp`, read by the camera page | | Startup mode | Related selection in Settings page | `startup_mode` | `ui_app_setup.hpp` | | USB extension port | SETTING -> ExtPort | `extport_usb` | `ui_app_setup.hpp` | | 5V output | SETTING -> ExtPort | `extport_5vout` | `ui_app_setup.hpp` | -| External app runtime user | Manual configuration | `run_as_user` | `cp0_app_process.cpp`, `cp0_app_pty.cpp` | +| External app runtime user | Manual configuration | `run_as_user` | `cp0_lvgl_process.cpp`, `cp0_lvgl_pty.cpp` | Configuration implementation: | File | Description | | --- | --- | | `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` | Declarations for `cp0_config_get_int/set_int/get_str/set_str/save` | -| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | Device-side configuration read/write, saved to `/var/lib/applaunch/settings` | -| `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` | SDL2 compatibility implementation | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | Device-side configuration read/write, saved to `/var/lib/applaunch/settings` | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp` | SDL2 compatibility implementation | | `ext_components/cp0_lvgl/src/commount.c` | Applies saved brightness and volume at startup | ## 7. Build Entry Table @@ -174,12 +176,12 @@ Configuration implementation: | framebuffer/display | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c` | | Keyboard input | `ext_components/cp0_lvgl/include/keyboard_input.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | | File paths | `ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | -| Process | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp` | -| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp` | -| Audio | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c` | -| Settings/brightness/volume | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp` | -| Network/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp` | -| Screenshot | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp` | +| Process | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp` | +| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp` | +| Audio | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp` | +| Settings/brightness/volume | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp` | +| Network/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp` | +| Screenshot | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp` | | Camera | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp` | Device camera | `ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp` | ## 9. Debugging Command Quick Reference diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" index 2f44a93a..f9acac4f 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/00-\346\200\273\350\247\210\344\270\216\351\230\205\350\257\273\350\267\257\347\272\277.md" @@ -34,7 +34,7 @@ LVGL 9.5 UI 框架 APPLaunch 首页、状态栏、轮播、应用管理器 | +--> 内置页面 AppPage - +--> PTY 终端应用 UIConsolePage + +--> PTY ST 终端应用 UISTPage +--> 外部独立进程 cp0_process_exec_blocking() ``` @@ -80,7 +80,7 @@ APPLaunch 首页、状态栏、轮播、应用管理器 - **APPLaunch**:启动器工程或启动器进程。 - **首页**:APPLaunch 的主 screen,即带状态栏和应用轮播的界面。 - **内置页面**:编译在 APPLaunch 进程内的页面类,例如 `UISetupPage`。 -- **终端应用**:通过 `UIConsolePage` + PTY 在 APPLaunch 中运行的命令,例如 `bash`。 +- **终端应用**:通过 `UISTPage` + PTY 在 APPLaunch 中运行的命令,例如 `bash`。 - **外部应用**:独立可执行程序,启动时 APPLaunch 暂停自身 LVGL 渲染,等待外部程序退出。 - **资源树**:`APPLaunch/share/images`、`APPLaunch/share/audio`、`APPLaunch/share/font` 等运行时文件。 - **设备端**:M5CardputerZero 的 AArch64 Linux 环境。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" index 11c83b6c..8667005b 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/01-\345\267\245\347\250\213\347\233\256\345\275\225\344\270\216\346\250\241\345\235\227\350\201\214\350\264\243.md" @@ -196,7 +196,7 @@ main/ui/ main/ui/page_app/ ├── ui_app_camera.hpp ├── ui_app_compass.hpp -├── ui_app_console.hpp +├── ui_app_st.hpp ├── ui_app_file.hpp ├── ui_app_game.hpp ├── ui_app_lora.hpp @@ -230,7 +230,7 @@ ui_init() Launch ├── UILaunchPage::panel()/label() - ├── page_v + ├── append_page_app / page_v ├── cp0_file_path() ├── cp0_process_* ├── cp0_dir_watch_* diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" index 68967386..366b2fcb 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/04-\345\272\224\347\224\250\346\250\241\345\236\213\344\270\216\345\220\257\345\212\250\346\234\272\345\210\266.md" @@ -22,7 +22,7 @@ app -> Launch::launch_app() -> app.launch(this) ├── 内置页面:new PageT + lv_disp_load_scr() - ├── 终端应用:UIConsolePage + PTY exec() + ├── 终端应用:UISTPage + PTY exec() └── 外部应用:cp0_process_exec_blocking() ``` @@ -33,7 +33,7 @@ app | `projects/APPLaunch/main/ui/launch.h` | `Launch` 公共接口和应用模型声明 | | `projects/APPLaunch/main/ui/launch.cpp` | `app`、`Launch`、应用列表、启动逻辑、`.desktop` 扫描 | | `projects/APPLaunch/main/ui/ui_launch_page.cpp` | Enter / click 事件转发到 `Launch::launch_app()` | -| `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | 终端页面 `UIConsolePage` | +| `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp` | 终端页面 `UISTPage` | | `projects/APPLaunch/main/ui/page_app/*.hpp` | 各内置页面,例如设置、音乐、文件、相机、LoRa | | `projects/APPLaunch/APPLaunch/applications/` | 运行时 `.desktop` 应用描述目录 | | `ext_components/cp0_lvgl` | 进程启动、PTY、目录监听、路径解析等底层能力 | @@ -81,8 +81,8 @@ struct app | 类型 | 构造方式 | 启动函数 | 示例 | | --- | --- | --- | --- | -| 内置页面 | `page_v` | 构造页面并 `lv_disp_load_scr()` | `GAME`、`SETTING`、`Compass` | -| 终端命令 | `exec, terminal=true` | `launch_Exec_in_terminal()` | `Python`、`CLI` | +| 内置页面 | `page_v` / `append_page_app` | 构造页面并 `lv_disp_load_scr()` | `CLI`、`GAME`、`SETTING`、`Compass` | +| 终端命令 | `exec, terminal=true` | `launch_Exec_in_terminal()` 创建 `UISTPage` 并调用 `exec()` | `Python`、`Terminal=true` 的 `.desktop` 应用 | | 外部进程 | `exec, terminal=false` | `launch_Exec()` | AppStore、Calculator | ## 5. 固定应用注册 @@ -96,7 +96,7 @@ constexpr BuiltinAppRegistration kBuiltinApps[] = { {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, {{"STORE", "store_100.png", "app_Store", false, true}, "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, - {{"CLI", "cli_100.png", "app_CLI", false, true}, "bash", true, false, false, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, {{"MATH", "math_100.png", "app_Math", true, false}, @@ -166,7 +166,7 @@ Enter ## 7. 终端应用启动机制 -终端应用使用 `UIConsolePage`,外部命令在 APPLaunch 进程内的终端页面中执行: +终端应用使用 `UISTPage`,外部命令在 APPLaunch 进程内的终端页面中执行: ```cpp void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) @@ -174,7 +174,7 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) ui_loading_show("Loading..."); lv_refr_now(NULL); - auto p = std::make_shared(); + auto p = std::make_shared(); app_Page = p; lv_disp_load_scr(p->screen()); lv_indev_set_group(lv_indev_get_next(NULL), p->input_group()); @@ -190,10 +190,10 @@ void launch_Exec_in_terminal(const std::string &exec, bool sysplause = true) ```text Python -> exec = "python3", terminal = true -CLI -> exec = "bash", terminal = true +CLI -> 内置 UISTPage,页面内部启动交互式 bash ``` -与内置页面相比,终端应用多一步 `p->exec(exec)`。它通常通过 PTY 与命令交互,用户看到的是 `UIConsolePage`,而不是离开 APPLaunch 的独立界面。 +与内置页面相比,终端应用多一步 `p->exec(exec)`。它通常通过 PTY 与命令交互,用户看到的是 `UISTPage`,而不是离开 APPLaunch 的独立界面。 ## 8. 外部独立应用启动机制 @@ -256,15 +256,11 @@ Enter 外部应用 -> LVGL_RUN_FLAGE=1 ``` -`STORE` 是一个外部应用示例: +`STORE` 是一个固定入口,用来启动外部应用: ```cpp -app_list.emplace_back("STORE", - img_path("store_100.png"), - "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", - false, - true, - true); +{{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, ``` 其中 `run_as_root=true` 会传给 `launch_Exec(exec, run_as_root)`,再转成 `keep_root ? 1 : 0`。 @@ -331,7 +327,7 @@ Icon=share/images/e-Mail_80.png | `Name` | 是 | 首页显示标题 | | `Exec` | 是 | 启动命令 | | `Icon` | 否 | 图标路径 | -| `Terminal` | 否 | `true/True/1` 表示用 `UIConsolePage` 启动 | +| `Terminal` | 否 | `true/True/1` 表示用 `UISTPage` 启动 | | `Sysplause` | 否 | 传给终端页面的暂停策略,默认 true | 注册逻辑: diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" index 1c1e952d..1dc43a65 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/05-\345\206\205\347\275\256\351\241\265\351\235\242\346\241\206\346\236\266.md" @@ -20,7 +20,7 @@ UILaunchPage 首页轮播 Launch::launch_app() | +-- 外部命令: cp0_process_exec_blocking() - +-- 终端命令: UIConsolePage + PTY + +-- 终端命令: UISTPage + PTY +-- 内置页面: std::make_shared() | v @@ -177,14 +177,13 @@ if (navigate_home) | 页面类 | 文件 | 启动器名称 | 继承 | 说明 | | --- | --- | --- | --- | --- | -| `UIConsolePage` | `ui_app_console.hpp` | `CLI` 或终端外部命令 | `AppPage` | 终端模拟器,PTY 读写,支持 ANSI/VT 序列和键盘转义序列 | +| `UISTPage` | `ui_app_st.hpp` | `CLI` 或终端外部命令 | `AppPage` | 终端模拟器,PTY 读写,支持 ANSI/VT 序列和键盘转义序列 | | `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPageRoot` | 贪吃蛇游戏,全屏自绘,使用 LVGL timer 驱动 | | `UISetupPage` | `ui_app_setup.hpp` | `SETTING` | `AppPage` | 系统设置、应用开关、亮度、音量、WiFi、相机分辨率等 | -| `UIGamePage` | `ui_app_game.hpp` | `GAME` | `AppPage` | 内置游戏入口 | | `UICompassPage` | `ui_app_compass.hpp` | `Compass` | `AppPageRoot` | 指南针页面,传感器线程 + UI timer | | `UIIpPanelPage` | `ui_app_ip_panel.hpp` | `IP_PANEL` | `AppPage` | 网络接口/IP 信息列表,每秒刷新 | | `UIFilePage` | `ui_app_file.hpp` | `FILE` | `AppPage` | 文件浏览器,目录列表和进入/返回 | -| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH 参数输入,连接后嵌入 `UIConsolePage` | +| `UISSHPage` | `ui_app_ssh.hpp` | `SSH` | `AppPage` | SSH 参数输入,连接后嵌入 `UISTPage` | | `UIMeshPage` | `ui_app_mesh.hpp` | `MESH` | `AppPage` | Mesh 消息列表、输入弹层、发送/刷新 | | `UIRecPage` | `ui_app_rec.hpp` | `REC` | 自定义 `rec_page` | 录音/播放/文件列表,含异步资源管理 | | `UICameraPage` | `ui_app_camera.hpp` | `CAMERA` | `AppPage` | 相机预览、图库、拍照、状态页 | @@ -195,14 +194,17 @@ if (navigate_home) ## 6. 页面注册和显示顺序 -内置页面在 `Launch::Launch()` 中插入 `app_list`。固定前 5 个应用先初始化首页 5 个轮播槽: +内置入口在 `launch.cpp` 的 `kBuiltinApps[]` 中声明。前 5 个启用的入口会优先初始化首页 5 个轮播槽: ```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, +}; ``` 内置页面显示现在由 `kBuiltinApps[]` 和 `AppDescriptor.config_key` 驱动。`Launch::rebuild_builtin_apps()` 追加每个描述符前会调用 `launcher_app_registry_is_enabled()`,设置页修改则调用 `launcher_app_registry_set_enabled()` 并触发 `Launch::applications_reload()`。 @@ -282,19 +284,19 @@ private: ## 9. 嵌套页面和特殊页面 -`UISSHPage` 是一个典型的嵌套页面:输入 SSH 参数时由 `UISSHPage` 处理键盘;连接后创建 `UIConsolePage`,切换 screen 和输入组。 +`UISSHPage` 是一个典型的嵌套页面:输入 SSH 参数时由 `UISSHPage` 处理键盘;连接后创建 `UISTPage`,切换 screen 和输入组。 ```cpp -console_page_ = std::make_shared(); -console_page_->navigate_home = [this]() { - console_page_.reset(); +terminal_page_ = std::make_shared(); +terminal_page_->navigate_home = [this]() { + terminal_page_.reset(); view_state_ = ViewState::INPUT; lv_disp_load_scr(this->screen()); lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); }; -lv_disp_load_scr(console_page_->screen()); -lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); +lv_disp_load_scr(terminal_page_->screen()); +lv_indev_set_group(lv_indev_get_next(NULL), terminal_page_->input_group()); ``` 这种页面要特别注意: @@ -320,4 +322,4 @@ lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); - 不要在页面中直接访问首页全局对象,除非是明确的首页功能。 - 页面标题请调用 `set_page_title()`,不要直接改顶栏内部 label。 - 所有可退出页面都应支持 `KEY_ESC`,并调用 `navigate_home` 或返回上一级视图。 -- 页面开关 key 必须和 `UISetupPage::save_app_toggle()`、`launch.cpp` 的 `APP_ENABLED()` 保持一致。 +- 页面开关 key 必须在 `AppDescriptor.config_key`、`UISetupPage::save_app_toggle()`、`launch.cpp` 的 `launcher_app_registry_is_enabled()` 之间保持一致。 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" index bcd0cbf5..695ace92 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/06-\350\265\204\346\272\220\344\270\216\351\205\215\347\275\256\347\263\273\347\273\237.md" @@ -145,7 +145,8 @@ extern "C" const char *cp0_file_path_c(const char *file) 首页和内置页常用: ```cpp -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); +{{"GAME", "game_100.png", "app_Game", false, true}, + nullptr, false, true, false, append_page_app}, lv_obj_set_style_bg_img_src(time_panel_, cp0_file_path_c("status_time_background.png"), @@ -212,7 +213,7 @@ launcher_fonts().get("Montserrat-Bold.ttf", 16, LV_FREETYPE_FONT_STYLE_BOLD) | `Name` | 是 | 轮播显示名称 | | `Exec` | 是 | 启动命令或可执行路径 | | `Icon` | 否 | 图标路径;可以是 `share/images/...` 或可被 LVGL 读取的路径 | -| `Terminal` | 否 | `true`/`True`/`1` 表示在 `UIConsolePage` 中运行 | +| `Terminal` | 否 | `true`/`True`/`1` 表示在 `UISTPage` 中运行 | | `Sysplause` | 否 | 终端命令结束后是否暂停等待用户确认,默认 `true` | 示例: @@ -253,8 +254,8 @@ void cp0_config_save(void); - 读取时必须提供默认值,保证配置缺失时页面仍能运行。 - 写入后需要调用 `cp0_config_save()` 持久化。 -- 设备端实现位于 `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`。 -- SDL 兼容实现位于 `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp`。 +- 设备端实现位于 `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp`。 +- SDL 兼容实现位于 `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp`。 典型写法: @@ -302,7 +303,7 @@ cp0_config_save(); | `startup_mode` | `UISetupPage` | 启动模式,当前选项为 `Launcher` / `CLI` | | `extport_usb` | `UISetupPage` | 扩展口 USB 开关 | | `extport_5vout` | `UISetupPage` | 扩展口 5V 输出开关 | -| `run_as_user` | `cp0_app_process.cpp`、`cp0_app_pty.cpp` | 外部进程/PTY 降权用户配置 | +| `run_as_user` | `cp0_lvgl_process.cpp`、`cp0_lvgl_pty.cpp` | 外部进程/PTY 降权用户配置 | ### 7.3 业务临时输入 @@ -327,19 +328,20 @@ cp0_config_save(); ```cpp void save_app_toggle(int idx) { - char cfg_key[64]; - snprintf(cfg_key, sizeof(cfg_key), "app_%s", app_keys[idx]); + std::size_t app_count = 0; + const AppDescriptor *apps = launcher_app_registry_entries(&app_count); + const AppDescriptor &desc = apps[idx]; bool enabled = menu_items_[0].sub_items[idx].toggle_state; - cp0_config_set_int(cfg_key, enabled ? 1 : 0); - cp0_config_save(); + launcher_app_registry_set_enabled(desc, enabled); + config_save(); } ``` 修改配置 key 时必须同步检查: -- `UISetupPage::menu_init()` 的 `app_keys` / `app_labels`。 -- `UISetupPage::save_app_toggle()` 的 `app_keys` 和 always-on 列表。 -- `launch.cpp` 的 `APP_ENABLED("...")`。 +- `kBuiltinApps[]` 中的 `AppDescriptor` 条目。 +- `UISetupPage` 中的 `launcher_app_registry_entries()` / `save_app_toggle()`。 +- `launch.cpp` 中的 `launcher_app_registry_is_enabled()`。 - 文档和默认配置。 ## 9. 资源命名建议 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" index 54ed56d7..5f982212 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/07-\350\276\223\345\205\245\347\263\273\347\273\237\344\270\216\346\214\211\351\224\256\346\230\240\345\260\204.md" @@ -265,7 +265,7 @@ static uint32_t fzxc_to_arrow(uint32_t key) | 页面 | 文件 | 主要按键 | | --- | --- | --- | -| `UIConsolePage` | `ui_app_console.hpp` | ESC/方向/回车/退格转 PTY 控制序列;HOME 相关状态用于退出/外部锁 | +| `UISTPage` | `ui_app_st.hpp` | ESC/方向/回车/退格转 PTY 控制序列;HOME 相关状态用于退出/外部锁 | | `UIGamePage` | `ui_app_game.hpp` | 方向键移动,ENTER 开始/重开,ESC 返回 | | `UISetupPage` | `ui_app_setup.hpp` | UP/DOWN 或 F/X 选择,ENTER/RIGHT 或 C 进入/确认,ESC/LEFT 或 Z 返回,部分页面支持 R/D | | `UIGamePage` | `ui_app_game.hpp` | 使用通用页面按键处理;ESC 返回 | @@ -324,7 +324,7 @@ static char keycode_to_char(uint32_t key) ### 9.2 终端输入 -`UIConsolePage` 直接读取 `struct key_item`,把物理键和 UTF-8 文本转为 PTY 字节流: +`UISTPage` 直接读取 `struct key_item`,把物理键和 UTF-8 文本转为 PTY 字节流: - `KEY_ENTER` -> `\r` - `KEY_BACKSPACE` -> `0x7f` @@ -364,7 +364,7 @@ LVGL_RUN_FLAGE = 1; - 首页:`UILaunchPage::home_input_group()`。 - 内置页:`AppPageRoot::input_group()`。 -- 嵌套终端:`UIConsolePage::input_group()`。 +- 嵌套终端:`UISTPage::input_group()`。 切换页面时必须同步切换输入组: diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" index b66eeb78..fb394408 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/10-\346\211\251\345\261\225\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -13,11 +13,11 @@ | `projects/APPLaunch/APPLaunch/` | 运行时资源树,打包后对应设备端 `/usr/share/APPLaunch/` | | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | 设备端 `cp0_file_path()` 路径规则 | | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | SDL2 开发机 `cp0_file_path()` 路径规则 | -| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 设备端设置持久化,保存到 `/var/lib/applaunch/settings` | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | 设备端设置持久化,保存到 `/var/lib/applaunch/settings` | APPLaunch 的应用来源分两类: -- **内置页面**:编译进 APPLaunch 进程,由 `app("NAME", icon, page_v)` 注册,进入时创建 `PageT` 对象并切换到它的 screen。 +- **内置页面**:编译进 APPLaunch 进程,在 `kBuiltinApps[]` 中用 `append_page_app` 注册,进入时创建 `PageT` 对象并切换到它的 screen。 - **外部应用**:通过固定 `Exec` 或 `.desktop` 描述启动独立进程,非终端应用会暂停 Launcher 的 LVGL 定时器,等待子进程退出后回到首页。 ## 2. 新增内置页面 @@ -103,53 +103,32 @@ ui/generate_page_app_includes.py ### 2.3 在首页应用列表注册 -打开 `projects/APPLaunch/main/ui/launch.cpp`,找到 `Launch::Launch()`。内置页面注册方式如下: +打开 `projects/APPLaunch/main/ui/launch.cpp`,在 `kBuiltinApps[]` 中添加内置注册项: ```cpp -app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); +{{"MYTOOL", "mytool_100.png", "app_MyTool", true, false}, + nullptr, false, true, false, append_page_app}, ``` -推荐放在 `APP_ENABLED` 控制区,方便后续通过设置页开关控制是否显示: +如果页面只支持设备端硬件,把条目放到现有的 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 设备端条件块里,避免 SDL2 构建失败。 -```cpp -#define APP_ENABLED(key) (cp0_config_get_int("app_" key, 1) != 0) - -if (APP_ENABLED("MyTool")) - app_list.emplace_back("MYTOOL", img_path("mytool_100.png"), page_v); - -#undef APP_ENABLED -``` - -注册规则: +注册字段: -- 第一个参数是首页轮播显示名,建议短一些,避免在小屏上被截断。 -- 第二个参数是图标路径,通常使用 `img_path("xxx_100.png")`。 -- 第三个参数 `page_v` 表示点击时创建内置页面。 -- 如果页面只支持设备端硬件,可以放在 `#if defined(__linux__) && !defined(HAL_PLATFORM_SDL)` 内,避免 SDL2 构建失败。 +- `AppDescriptor.label` 是首页轮播显示名,建议短一些,避免小屏截断。 +- `AppDescriptor.icon` 是图标文件名,会通过 `cp0_file_path()` 解析。 +- `AppDescriptor.config_key` 是完整设置持久化 key,例如 `app_MyTool`。 +- `append_page_app` 表示点击时创建内置页面。 ### 2.4 添加设置页开关 -如果希望设置页的 `Launcher` 菜单可以控制新页面显示,需要修改 `UISetupPage::menu_init()` 中的 `app_keys` 和 `app_labels`。 - -示例: +设置页的 `Launcher` 菜单来自 `launcher_app_registry_entries()`。如果希望显示开关,把 `kBuiltinApps[]` 条目里的 `AppDescriptor.configurable` 设为 `true`: ```cpp -static const char *app_keys[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Game", "Math", "MyTool" -}; - -static const char *app_labels[] = { - "Python", "Store", "CLI", "Game", "Setting", - "Game", "Math", "My Tool" -}; +{{"MYTOOL", "mytool_100.png", "app_MyTool", true, false}, + nullptr, false, true, false, append_page_app}, ``` -`save_app_toggle()` 会把开关保存为 `app_`,例如 `app_MyTool=0`。`launch.cpp` 里用相同 key 读取: - -```cpp -cp0_config_get_int("app_MyTool", 1) -``` +`launcher_app_registry_set_enabled()` 会用 `config_key` 保存开关,例如 `app_MyTool=0`;`Launch::rebuild_builtin_apps()` 通过 `launcher_app_registry_is_enabled()` 读取。 设备端持久化文件是: @@ -222,7 +201,7 @@ APPLaunch 当前解析的字段: | `Name` | 是 | 首页显示名 | | `Exec` | 是 | 要执行的命令,可为绝对路径或 shell 命令 | | `Icon` | 否 | 图标路径,建议写 `share/images/xxx.png` 或可被 LVGL 读取的路径 | -| `Terminal` | 否 | `true`/`True`/`1` 表示在内置 `UIConsolePage` 中运行 | +| `Terminal` | 否 | `true`/`True`/`1` 表示在内置 `UISTPage` 中运行 | | `Sysplause` | 否 | 仅终端应用使用,控制终端页命令结束后的暂停行为,默认 true | | `Type` | 否 | 兼容桌面文件习惯,当前 APPLaunch 不依赖该字段 | | `TryExec` | 否 | 当前 APPLaunch 不解析,只能作为说明字段 | @@ -265,7 +244,7 @@ Type=Application `launch.cpp` 对外部应用有两种启动方式: -- `Terminal=true`:创建 `UIConsolePage`,在 APPLaunch 进程内显示 PTY 终端,执行 `Exec`。 +- `Terminal=true`:创建 `UISTPage`,在 APPLaunch 进程内显示 PTY 终端,执行 `Exec`。 - `Terminal=false`:调用 `cp0_process_exec_blocking()` 启动外部进程,APPLaunch 暂停 LVGL timer 和输入组,等子进程退出后恢复首页。 非终端外部应用返回首页依赖以下行为: @@ -362,11 +341,10 @@ lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); 步骤: -1. 在 `UISetupPage::menu_init()` 的 `app_keys` 增加内部 key,例如 `MyTool`。 -2. 在相同位置的 `app_labels` 增加显示名,例如 `My Tool`。 -3. 在 `launch.cpp` 注册应用时使用相同 key:`APP_ENABLED("MyTool")`。 -4. 运行设置页,进入 `Launcher` 菜单,切换 O/X。 -5. 返回首页后如列表未刷新,可重启 APPLaunch;当前固定/内置列表是在 `Launch` 构造时读取配置。 +1. 在 `kBuiltinApps[]` 中新增或更新应用的 `AppDescriptor`。 +2. 设置 `configurable=true`,并选择完整 `config_key`,例如 `app_MyTool`。 +3. 运行设置页,进入 `Launcher` 菜单,切换 O/X。 +4. 设置页会调用 `launcher_app_registry_set_enabled()` 并通知 Launcher 重建内置应用列表。 ### 5.2 新增普通设置项 @@ -390,7 +368,7 @@ lv_obj_set_style_text_font(label, font, LV_PART_MAIN | LV_STATE_DEFAULT); ### 5.3 配置持久化位置 -设备端配置实现:`ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp`。 +设备端配置实现:`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp`。 - 配置目录:`/var/lib/applaunch` - 配置文件:`/var/lib/applaunch/settings` diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" index e7ba9e4a..31372749 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/11-\350\260\203\350\257\225\344\270\216\346\225\205\351\232\234\346\216\222\346\237\245.md" @@ -125,8 +125,8 @@ sudo cat /var/lib/applaunch/settings | `duplicate Exec` | `launch.cpp` | `.desktop` 与已有应用 Exec 重复 | | `Launching terminal app` | `launch.cpp` | 进入内置终端页面运行命令 | | `Launching external app` | `launch.cpp` | 启动非终端外部程序 | -| `[CP0-APP] ESC DOWN/UP` | `cp0_app_process.cpp` | 外部应用运行期间父进程读到 ESC | -| `[cp0] Returned to launcher` | `cp0_app_process.cpp` | 外部应用退出,准备回到首页 | +| `[CP0-APP] ESC DOWN/UP` | `cp0_lvgl_process.cpp` | 外部应用运行期间父进程读到 ESC | +| `[cp0] Returned to launcher` | `cp0_lvgl_process.cpp` | 外部应用退出,准备回到首页 | | `[HOME_STATUS] connected=` | `launch.cpp` | 首页状态栏刷新 WiFi/电量 | ## 3. 黑屏排查 diff --git "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" index 8c95f42a..804ece39 100644 --- "a/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" +++ "b/docs/launcher\345\267\245\347\250\213\350\257\246\347\273\206\350\257\264\346\230\216/12-\345\270\270\347\224\250\344\277\256\346\224\271\345\205\245\345\217\243\351\200\237\346\237\245.md" @@ -12,8 +12,8 @@ git status --short | 任务 | 主要文件/目录 | 关键点 | 验证方式 | | --- | --- | --- | --- | | 新增内置页面 | `projects/APPLaunch/main/ui/page_app/` | 新建 `ui_app_xxx.hpp`,继承 `AppPage` | SDL2 编译并打开页面 | -| 注册内置页面到首页 | `projects/APPLaunch/main/ui/launch.cpp` | `app_list.emplace_back("NAME", img_path("icon.png"), page_v)` | 首页轮播出现图标 | -| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/launch.cpp` | 设置页写 `app_Key`,Launcher 读 `APP_ENABLED("Key")` | 切换设置后重启或刷新首页 | +| 注册内置页面到首页 | `projects/APPLaunch/main/ui/launch.cpp` | 在 `kBuiltinApps[]` 中添加 `append_page_app` 条目 | 首页轮播出现图标 | +| 控制内置页面显示开关 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp`、`projects/APPLaunch/main/ui/launch.cpp` | 设置页写 `AppDescriptor.config_key`,Launcher 读 `launcher_app_registry_is_enabled()` | 切换设置后重启或刷新首页 | | 新增外部 `.desktop` 应用 | `projects/APPLaunch/APPLaunch/applications/` | 文件名必须以 `.desktop` 结尾,包含 `Name` 和 `Exec` | 日志无 skip,首页出现应用 | | 新增图标 | `projects/APPLaunch/APPLaunch/share/images/` | 内置页用 `img_path()`,`.desktop` 用 `Icon=share/images/xxx.png` | 日志无 `missing/unreadable` | | 新增音效 | `projects/APPLaunch/APPLaunch/share/audio/` | 页面用 `audio_path()` 和 `cp0_signal_audio_api()` | 设备端播放声音 | @@ -22,10 +22,10 @@ git status --short | 修改轮播动画 | `projects/APPLaunch/main/ui/animation/ui_launcher_animation.cpp` | 卡片移动、缩放、透明度等动画 | SDL2 连续左右切换 | | 修改首页状态栏 | `projects/APPLaunch/main/ui/launch.cpp`、`projects/APPLaunch/main/ui/ui.cpp` | `update_home_status_bar()` 刷新 WiFi/时间/电量 | 看 `[HOME_STATUS]` 日志 | | 修改设置页菜单 | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `menu_init()` 增加 `MenuItem`/`SubItem` | 进入 SETTING 页面测试 | -| 修改配置保存逻辑 | `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 当前保存到 `/var/lib/applaunch/settings`,最多 32 项 | 查看 settings 文件 | +| 修改配置保存逻辑 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | 当前保存到 `/var/lib/applaunch/settings`,最多 32 项 | 查看 settings 文件 | | 修改资源路径规则 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | 设备端和 SDL2 要同步考虑 | SDL2 + 设备端都检查资源 | -| 修改外部应用启动/返回 | `projects/APPLaunch/main/ui/launch.cpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `launch_Exec()`、`cp0_process_exec_blocking()` | 外部应用启动、ESC 返回 | -| 修改终端应用 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | PTY、命令执行、输入输出 | `Terminal=true` 应用验证 | +| 修改外部应用启动/返回 | `projects/APPLaunch/main/ui/launch.cpp`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp` | `launch_Exec()`、`cp0_process_exec_blocking()` | 外部应用启动、ESC 返回 | +| 修改终端应用 | `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp` | PTY、命令执行、输入输出 | `Terminal=true` 应用验证 | | 修改输入映射 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c`、`ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | 设备端和 SDL2 输入差异 | `evtest` + SDL2 键盘 | | 修改启动流程 | `projects/APPLaunch/main/src/main.cpp` | `lv_init()`、`cp0_lvgl_init()`、`ui_init()`、主循环 | 看 `[BOOT]` 日志 | | 修改构建依赖 | `projects/APPLaunch/main/SConstruct` | `SRCS`、`INCLUDE`、`REQUIREMENTS`、`STATIC_FILES` | scons 编译 | @@ -58,7 +58,6 @@ git status --short | 页面/功能 | 文件 | 注册名或图标 | 说明 | | --- | --- | --- | --- | -| GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | | SETTING | `projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp` | `SETTING` / `setting_100.png` | 设置页,包含应用开关、亮度、音量、WiFi、相机等 | | GAME | `projects/APPLaunch/main/ui/page_app/ui_app_game.hpp` | `GAME` / `game_100.png` | 内置游戏入口 | | Compass | `projects/APPLaunch/main/ui/page_app/ui_app_compass.hpp` | `Compass` / `compass_needle_80.png` | 指南针页 | @@ -70,16 +69,19 @@ git status --short | CAMERA | `projects/APPLaunch/main/ui/page_app/ui_app_camera.hpp` | `CAMERA` / `camera_100.png` | 相机页,设备端启用 | | LORA | `projects/APPLaunch/main/ui/page_app/ui_app_lora.hpp` | `LORA` / `lora_100.png` | LoRa 页面,设备端启用 | | TANK | `projects/APPLaunch/main/ui/page_app/ui_app_tank_battle.hpp` | `TANK` / `tank_100.png` | 坦克游戏,设备端启用 | -| CLI/终端 | `projects/APPLaunch/main/ui/page_app/ui_app_console.hpp` | `CLI` / `cli_100.png` | `UIConsolePage`,用于 bash、python、Terminal=true 应用 | +| CLI/终端 | `projects/APPLaunch/main/ui/page_app/ui_app_st.hpp` | `CLI` / `cli_100.png` | `UISTPage`,用于 bash、python、Terminal=true 应用 | -固定注册入口在 `Launch::Launch()`: +固定入口声明在 `kBuiltinApps[]`: ```cpp -app_list.emplace_back("Python", img_path("python_100.png"), "python3", true, false); -app_list.emplace_back("STORE", img_path("store_100.png"), "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true); -app_list.emplace_back("CLI", img_path("cli_100.png"), "bash", true, false); -app_list.emplace_back("GAME", img_path("game_100.png"), page_v); -app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v); +constexpr BuiltinAppRegistration kBuiltinApps[] = { + {{"Python", "python_100.png", "app_Python", false, true}, "python3", true, false, false, nullptr}, + {{"STORE", "store_100.png", "app_Store", false, true}, + "/usr/share/APPLaunch/bin/M5CardputerZero-AppStore", false, true, true, nullptr}, + {{"CLI", "cli_100.png", "app_CLI", false, true}, nullptr, false, true, false, append_page_app}, + {{"GAME", "game_100.png", "app_Game", false, true}, nullptr, false, true, false, append_page_app}, + {{"SETTING", "setting_100.png", "app_Setting", false, true}, nullptr, false, true, false, append_page_app}, +}; ``` ## 4. 外部应用入口表 @@ -91,10 +93,10 @@ app_list.emplace_back("SETTING", img_path("setting_100.png"), page_v Launcher | `app_` | `ui_app_setup.hpp` 的 `save_app_toggle()`,`launch.cpp` 的 `APP_ENABLED()` | -| 亮度 | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | +| 应用显示开关 | SETTING -> Launcher | `AppDescriptor.config_key` | `ui_app_setup.hpp` 的 `save_app_toggle()`,`launch.cpp` 的 `launcher_app_registry_is_enabled()` | +| 亮度 | SETTING -> Screen -> Brightness | `brightness` | `ui_app_setup.hpp`、`ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp` | | 息屏时间 | SETTING -> Screen -> DarkTime | `dark_time` | `ui_app_setup.hpp` | | 音量 | SETTING -> Speaker -> Volume | `volume` | `ui_app_setup.hpp`、`cp0_volume_read/write()` | | 相机分辨率 | SETTING -> Camera -> Resolution | `cam_resolution` | `ui_app_setup.hpp`、相机页面读取 | | 启动模式 | 设置页相关选择 | `startup_mode` | `ui_app_setup.hpp` | | USB 扩展口 | SETTING -> ExtPort | `extport_usb` | `ui_app_setup.hpp` | | 5V 输出 | SETTING -> ExtPort | `extport_5vout` | `ui_app_setup.hpp` | -| 外部应用运行用户 | 手动配置 | `run_as_user` | `cp0_app_process.cpp`、`cp0_app_pty.cpp` | +| 外部应用运行用户 | 手动配置 | `run_as_user` | `cp0_lvgl_process.cpp`、`cp0_lvgl_pty.cpp` | 配置实现: | 文件 | 说明 | | --- | --- | | `ext_components/cp0_lvgl/include/cp0_lvgl_app.h` | `cp0_config_get_int/set_int/get_str/set_str/save` 声明 | -| `ext_components/cp0_lvgl/src/cp0/cp0_app_config.cpp` | 设备端配置读写,保存到 `/var/lib/applaunch/settings` | -| `ext_components/cp0_lvgl/src/sdl/cp0_app_compat_sdl.cpp` | SDL2 兼容实现 | +| `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_config.cpp` | 设备端配置读写,保存到 `/var/lib/applaunch/settings` | +| `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_config.cpp` | SDL2 兼容实现 | | `ext_components/cp0_lvgl/src/commount.c` | 启动时应用保存的亮度和音量 | ## 7. 构建入口表 @@ -174,12 +176,12 @@ Type=Application | framebuffer/display | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_display.c` | | 键盘输入 | `ext_components/cp0_lvgl/include/keyboard_input.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_keyboard.c` | | 文件路径 | `ext_components/cp0_lvgl/include/cp0_lvgl_file.hpp` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_file.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_file.cpp` | -| 进程 | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_process.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_process_sdl.cpp` | -| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_pty_sdl.cpp` | -| 音频 | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_audio_sdl.c` | -| 设置/亮度/音量 | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_settings_sdl.cpp` | -| 网络/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_network.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_network_sdl.cpp` | -| 截图 | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_app_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/cp0_hal_screenshot_sdl.cpp` | +| 进程 | `ext_components/cp0_lvgl/include/hal/hal_process.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp` | +| PTY | `ext_components/cp0_lvgl/include/hal/hal_pty.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_pty.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_pty.cpp` | +| 音频 | `ext_components/cp0_lvgl/include/hal/hal_audio.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_audio.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_audio.cpp` | +| 设置/亮度/音量 | `ext_components/cp0_lvgl/include/hal/hal_settings.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_settings.cpp` | +| 网络/WiFi | `ext_components/cp0_lvgl/include/hal/hal_network.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_network.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_network.cpp` | +| 截图 | `ext_components/cp0_lvgl/include/hal/hal_screenshot.h` | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_screenshot.cpp` | `ext_components/cp0_lvgl/src/sdl/sdl_lvgl_screenshot.cpp` | | 相机 | `ext_components/cp0_lvgl/src/cp0/cp0_lvgl_camera.cpp` | 设备端相机 | `ext_components/cp0_lvgl/src/sdl/cp0_lvgl_camera.cpp` | ## 9. 调试命令速查 diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp index 01d1f88d..0bdb5cc9 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_ssh.hpp @@ -48,7 +48,7 @@ class UISSHPage : public AppPage ~UISSHPage() { - console_page_.reset(); + terminal_page_.reset(); } private: @@ -57,7 +57,7 @@ class UISSHPage : public AppPage std::vector fields_; int active_field_ = 0; ViewState view_state_ = ViewState::INPUT; - std::shared_ptr console_page_; + std::shared_ptr terminal_page_; // ==================== keycode to char ==================== static char keycode_to_char(uint32_t key) @@ -230,25 +230,25 @@ class UISSHPage : public AppPage SLOGI("[SSH] Launching: %s", cmd.c_str()); // Create terminal page - console_page_ = std::make_shared(); + terminal_page_ = std::make_shared(); - // Restore the SSH input view when the embedded console exits. - console_page_->navigate_home = [this]() { + // Restore the SSH input view when the embedded terminal exits. + terminal_page_->navigate_home = [this]() { // Return to the SSH input view - console_page_.reset(); + terminal_page_.reset(); view_state_ = ViewState::INPUT; // Switch screen back to our root lv_disp_load_scr(this->screen()); lv_indev_set_group(lv_indev_get_next(NULL), this->input_group()); }; - // Switch to console screen + // Switch to terminal screen view_state_ = ViewState::TERMINAL; - lv_disp_load_scr(console_page_->screen()); - lv_indev_set_group(lv_indev_get_next(NULL), console_page_->input_group()); + lv_disp_load_scr(terminal_page_->screen()); + lv_indev_set_group(lv_indev_get_next(NULL), terminal_page_->input_group()); // Launch ssh command - console_page_->exec(cmd); + terminal_page_->exec(cmd); } // ==================== event binding ==================== From e42612591271d3e5ec3bfde1557b744e46fb5ef5 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 16 Jun 2026 16:25:15 +0800 Subject: [PATCH 69/70] Update APPLaunch deploy setup --- projects/APPLaunch/setup.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/APPLaunch/setup.ini b/projects/APPLaunch/setup.ini index f17392c5..ad9a5585 100644 --- a/projects/APPLaunch/setup.ini +++ b/projects/APPLaunch/setup.ini @@ -1,9 +1,9 @@ [ssh] local_file_path = dist remote_file_path = /home/pi/dist -remote_host = 192.168.28.181 +remote_host = 192.168.28.184 remote_port = 22 username = pi password = pi ; before_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service' -after_cmd = 'echo pi | sudo -S systemctl stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | sudo -S systemctl start APPLaunch.service' +after_cmd = 'systemctl --user stop APPLaunch.service; echo pi | sudo -S cp /home/pi/dist/M5CardputerZero-APPLaunch /usr/share/APPLaunch/bin ; echo pi | systemctl --user start APPLaunch.service' From bf5a8805a930308f6b9a1e9dca7afaebfae24531 Mon Sep 17 00:00:00 2001 From: dianjixz <18637716021@163.com> Date: Tue, 16 Jun 2026 16:41:05 +0800 Subject: [PATCH 70/70] Fix FreeType mono render mode compatibility --- projects/APPLaunch/main/ui/ui_launch_page.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/APPLaunch/main/ui/ui_launch_page.cpp b/projects/APPLaunch/main/ui/ui_launch_page.cpp index c13edfb7..e4417ed9 100644 --- a/projects/APPLaunch/main/ui/ui_launch_page.cpp +++ b/projects/APPLaunch/main/ui/ui_launch_page.cpp @@ -239,7 +239,12 @@ lv_font_t *LauncherFonts::get(const char *ttf_name, uint16_t size, lv_freetype_f lv_font_t *LauncherFonts::get_mono(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style) { - return get(ttf_name, size, style, LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO); + /* LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO is provided by our local + * FreeType override, but older SDK headers only name BITMAP/OUTLINE. + * Keep the ABI value here so CI builds against either header set. */ + constexpr lv_freetype_font_render_mode_t bitmap_mono_mode = + static_cast(2); + return get(ttf_name, size, style, bitmap_mono_mode); } lv_font_t *LauncherFonts::get(const char *ttf_name, uint16_t size, lv_freetype_font_style_t style,