From 29ea47a7bae438a9b89033f9ae16b21ef82ae6d5 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 7 Feb 2024 23:11:39 +0100 Subject: [PATCH] Improvements for Files app and statusbar icon (#35) - Created `tt_get_platform()` to use with more complex platform checks aside from the `ESP_PLATFORM` preprocessor directive - Expand PC/sim memory to 256k so we can load the max amount of entries in Files without memory issues. I decided to skip the LVGL buffer entirely so it's easier to catch memory leaks. - Simplified logic in `files_data.c` - Implement statusbar as a proper widget - Implement statusbar widget for wifi service - Implement statusbar widget for sdcard mounting/unmounting --- app-sim/src/lv_conf.h | 6 +- docs/ideas.md | 2 + tactility-core/src/kernel.c | 8 + tactility-core/src/kernel.h | 7 + tactility-esp/assets/sdcard_mounted.png | Bin 0 -> 220 bytes tactility-esp/assets/sdcard_unmounted.png | Bin 0 -> 224 bytes tactility-esp/src/services/wifi/wifi.c | 33 +++ tactility/src/apps/system/files/file_utils.c | 3 +- tactility/src/apps/system/files/files.c | 58 ++--- tactility/src/apps/system/files/files_data.c | 134 +++++------ tactility/src/apps/system/files/files_data.h | 6 +- tactility/src/sdcard.c | 19 +- tactility/src/services/gui/gui_draw.c | 7 +- .../src/services/gui/widgets/statusbar.c | 27 --- .../src/services/gui/widgets/statusbar.h | 16 -- tactility/src/ui/statusbar.c | 219 ++++++++++++++++++ tactility/src/ui/statusbar.h | 22 ++ 17 files changed, 410 insertions(+), 157 deletions(-) create mode 100644 tactility-esp/assets/sdcard_mounted.png create mode 100644 tactility-esp/assets/sdcard_unmounted.png delete mode 100644 tactility/src/services/gui/widgets/statusbar.c delete mode 100644 tactility/src/services/gui/widgets/statusbar.h create mode 100644 tactility/src/ui/statusbar.c create mode 100644 tactility/src/ui/statusbar.h diff --git a/app-sim/src/lv_conf.h b/app-sim/src/lv_conf.h index a39a46f0..facf78d8 100644 --- a/app-sim/src/lv_conf.h +++ b/app-sim/src/lv_conf.h @@ -46,10 +46,10 @@ *=========================*/ /*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/ -#define LV_MEM_CUSTOM 0 -#if LV_MEM_CUSTOM == 0 +#define LV_MEM_CUSTOM 1 +#if LV_MEM_CUSTOM == 1 /*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/ - #define LV_MEM_SIZE (128 * 1024U) /*[bytes]*/ + #define LV_MEM_SIZE (256 * 1024U) /*[bytes]*/ /*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/ #define LV_MEM_ADR 0 /*0: unused*/ diff --git a/docs/ideas.md b/docs/ideas.md index c8e87a08..0577a910 100644 --- a/docs/ideas.md +++ b/docs/ideas.md @@ -6,6 +6,8 @@ - Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot. - Try out Waveshare S3 120MHz mode for PSRAM (see "enabling 120M PSRAM is necessary" in [docs](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-4.3#Other_Notes)) - Fix for dark theme: the wifi icons should use the colour of the theme (they remain black when dark theme is set) +- Update Wi-Fi status bar icon based on signal strength +- Auto-detect sdcard removal (with a service?) # Core Ideas - Make a HAL? It would mainly be there to support PC development. It's a lot of effort for supporting what's effectively a dev-only feature. diff --git a/tactility-core/src/kernel.c b/tactility-core/src/kernel.c index cfc24342..a8f8fe5b 100644 --- a/tactility-core/src/kernel.c +++ b/tactility-core/src/kernel.c @@ -194,3 +194,11 @@ void tt_delay_us(uint32_t microseconds) { usleep(microseconds); #endif } + +Platform tt_get_platform() { +#ifdef ESP_PLATFORM + return PLATFORM_ESP; +#else + return PLATFORM_PC; +#endif +} diff --git a/tactility-core/src/kernel.h b/tactility-core/src/kernel.h index a81cdfad..c0bec3e2 100644 --- a/tactility-core/src/kernel.h +++ b/tactility-core/src/kernel.h @@ -6,6 +6,11 @@ extern "C" { #endif +typedef enum { + PLATFORM_ESP, + PLATFORM_PC +} Platform; + /** Check if CPU is in IRQ or kernel running and IRQ is masked * * Originally this primitive was born as a workaround for FreeRTOS kernel primitives shenanigans with PRIMASK. @@ -109,6 +114,8 @@ void tt_delay_ms(uint32_t milliseconds); */ void tt_delay_us(uint32_t microseconds); +Platform tt_get_platform(); + #ifdef __cplusplus } #endif diff --git a/tactility-esp/assets/sdcard_mounted.png b/tactility-esp/assets/sdcard_mounted.png new file mode 100644 index 0000000000000000000000000000000000000000..472f12f7cd5b9ace7c0d805c3613462032962eca GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE5DWk0lm$i!OEy*VwD9;a zL|Mr9FYR^M$(B-C!*+=0Hpi6$#_olD4u;hVZidX$8x0PNF_|{pxWRBD_3IG{kDf#Y z?hdmL_5p?`W;3!V^{6mkWd6-?r66F(!2>HACrmgnEyrPE)5L_0E&{HJ9NiyzuXKsb za6IAJsU#jWB{)N #define TAG "wifi" @@ -30,6 +31,7 @@ typedef struct { uint16_t scan_list_count; /** @brief Maximum amount of records to scan (value > 0) */ uint16_t scan_list_limit; + int8_t statusbar_icon_id; bool scan_active; esp_event_handler_instance_t event_handler_any_id; esp_event_handler_instance_t event_handler_got_ip; @@ -84,10 +86,12 @@ static Wifi* wifi_alloc() { instance->event_handler_got_ip = NULL; instance->event_group = xEventGroupCreate(); instance->radio_state = WIFI_RADIO_OFF; + instance->statusbar_icon_id = tt_statusbar_icon_add("A:/assets/ic_small_wifi_off.png"); return instance; } static void wifi_free(Wifi* instance) { + tt_statusbar_icon_remove(instance->statusbar_icon_id); tt_mutex_free(instance->mutex); tt_pubsub_free(instance->pubsub); tt_message_queue_free(instance->queue); @@ -221,6 +225,35 @@ static void wifi_scan_list_free_safely(Wifi* wifi) { static void wifi_publish_event_simple(Wifi* wifi, WifiEventType type) { WifiEvent turning_on_event = {.type = type}; + switch (type) { + case WifiEventTypeRadioStateOn: + break; + case WifiEventTypeRadioStateOnPending: + break; + case WifiEventTypeRadioStateOff: + tt_statusbar_icon_set_image(wifi->statusbar_icon_id, "A:/assets/ic_small_wifi_off.png"); + break; + case WifiEventTypeRadioStateOffPending: + break; + case WifiEventTypeScanStarted: + break; + case WifiEventTypeScanFinished: + break; + case WifiEventTypeDisconnected: + tt_statusbar_icon_set_image(wifi->statusbar_icon_id, "A:/assets/ic_small_wifi_off.png"); + break; + case WifiEventTypeConnectionPending: + tt_statusbar_icon_set_image(wifi->statusbar_icon_id, "A:/assets/network_wifi_1_bar.png"); + break; + case WifiEventTypeConnectionSuccess: + // TODO: update with actual bars + tt_statusbar_icon_set_image(wifi->statusbar_icon_id, "A:/assets/network_wifi.png"); + break; + case WifiEventTypeConnectionFailed: + tt_statusbar_icon_set_image(wifi->statusbar_icon_id, "A:/assets/ic_small_wifi_off.png"); + break; + } + tt_pubsub_publish(wifi->pubsub, &turning_on_event); } diff --git a/tactility/src/apps/system/files/file_utils.c b/tactility/src/apps/system/files/file_utils.c index a92eb161..99f488fb 100644 --- a/tactility/src/apps/system/files/file_utils.c +++ b/tactility/src/apps/system/files/file_utils.c @@ -40,14 +40,13 @@ int tt_scandir( struct dirent* current_entry; while ((current_entry = readdir(dir)) != NULL) { - TT_LOG_D(TAG, "debug: %s %d", current_entry->d_name, current_entry->d_type); if (filter(current_entry) == 0) { dirent_array[dirent_buffer_index] = malloc(sizeof(struct dirent)); memcpy(dirent_array[dirent_buffer_index], current_entry, sizeof(struct dirent)); dirent_buffer_index++; if (dirent_buffer_index >= SCANDIR_LIMIT) { - TT_LOG_E(TAG, "directory has more than %d files", SCANDIR_LIMIT); + TT_LOG_E(TAG, "Directory has more than %d files", SCANDIR_LIMIT); break; } } diff --git a/tactility/src/apps/system/files/files.c b/tactility/src/apps/system/files/files.c index f15f1a52..1f3b1337 100644 --- a/tactility/src/apps/system/files/files.c +++ b/tactility/src/apps/system/files/files.c @@ -7,18 +7,25 @@ #include "services/loader/loader.h" #include "ui/toolbar.h" #include +#include #define TAG "files_app" -bool tt_string_ends_with(const char* base, const char* postfix) { - size_t postfix_len = strlen(postfix); - size_t base_len = strlen(base); +/** + * Lower case check to see if the given file matches the provided file extension + * @param path the full path to the file + * @param extension the extension to look for, including the period symbol + * @return true on match + */ +static bool has_file_extension(const char* path, const char* extension) { + size_t postfix_len = strlen(extension); + size_t base_len = strlen(path); if (base_len < postfix_len) { return false; } for (int i = (int)postfix_len - 1; i >= 0; i--) { - if (tolower(base[base_len - postfix_len + i]) != postfix[i]) { + if (tolower(path[base_len - postfix_len + i]) != extension[i]) { return false; } } @@ -27,11 +34,11 @@ bool tt_string_ends_with(const char* base, const char* postfix) { } static bool is_image_file(const char* filename) { - return tt_string_ends_with(filename, ".jpg") || - tt_string_ends_with(filename, ".png") || - tt_string_ends_with(filename, ".jpeg") || - tt_string_ends_with(filename, ".svg") || - tt_string_ends_with(filename, ".bmp"); + return has_file_extension(filename, ".jpg") || + has_file_extension(filename, ".png") || + has_file_extension(filename, ".jpeg") || + has_file_extension(filename, ".svg") || + has_file_extension(filename, ".bmp"); } // region Views @@ -41,7 +48,11 @@ static void update_views(FilesData* data); static void on_navigate_up_pressed(TT_UNUSED lv_event_t* event) { FilesData* files_data = (FilesData*)event->user_data; if (strcmp(files_data->current_path, "/") != 0) { - files_data_set_entries_navigate_up(files_data); + TT_LOG_I(TAG, "Navigating upwards"); + char new_absolute_path[MAX_PATH_LENGTH]; + if (tt_string_get_path_parent(files_data->current_path, new_absolute_path)) { + files_data_set_entries_for_path(files_data, new_absolute_path); + } } update_views(files_data); } @@ -57,11 +68,11 @@ static void on_file_pressed(lv_event_t* e) { FilesData* files_data = lv_obj_get_user_data(button); struct dirent* dir_entry = e->user_data; - TT_LOG_I(TAG, "clicked %s %d", dir_entry->d_name, dir_entry->d_type); + TT_LOG_I(TAG, "Pressed %s %d", dir_entry->d_name, dir_entry->d_type); switch (dir_entry->d_type) { case TT_DT_DIR: - files_data_set_entries_for_path(files_data, dir_entry->d_name); + files_data_set_entries_for_child_path(files_data, dir_entry->d_name); update_views(files_data); break; case TT_DT_LNK: @@ -81,19 +92,14 @@ static void create_file_widget(FilesData* files_data, lv_obj_t* parent, struct d tt_check(parent); lv_obj_t* list = (lv_obj_t*)parent; const char* symbol; - switch (dir_entry->d_type) { - case TT_DT_DIR: - symbol = LV_SYMBOL_DIRECTORY; - break; - case TT_DT_REG: - symbol = is_image_file(dir_entry->d_name) ? LV_SYMBOL_IMAGE : LV_SYMBOL_FILE; - break; - case TT_DT_LNK: - symbol = LV_SYMBOL_LOOP; - break; - default: - symbol = LV_SYMBOL_SETTINGS; - break; + if (dir_entry->d_type == TT_DT_DIR) { + symbol = LV_SYMBOL_DIRECTORY; + } else if (is_image_file(dir_entry->d_name)) { + symbol = LV_SYMBOL_IMAGE; + } else if (dir_entry->d_type == TT_DT_LNK) { + symbol = LV_SYMBOL_LOOP; + } else { + symbol = LV_SYMBOL_SETTINGS; } lv_obj_t* button = lv_list_add_btn(list, symbol, dir_entry->d_name); lv_obj_set_user_data(button, files_data); @@ -129,7 +135,7 @@ static void on_show(App app, lv_obj_t* parent) { static void on_start(App app) { FilesData* data = files_data_alloc(); - files_data_set_entries_root(data); + files_data_set_entries_for_path(data, "/"); tt_app_set_data(app, data); } diff --git a/tactility/src/apps/system/files/files_data.c b/tactility/src/apps/system/files/files_data.c index 6a1f336f..b905b943 100644 --- a/tactility/src/apps/system/files/files_data.c +++ b/tactility/src/apps/system/files/files_data.c @@ -5,10 +5,31 @@ #define TAG "files_app" +static bool get_child_path(char* base_path, const char* child_path, char* out_path, size_t out_size) { + size_t current_path_length = strlen(base_path); + size_t added_path_length = strlen(child_path); + size_t total_path_length = current_path_length + added_path_length + 1; // two paths with `/` + + if (total_path_length >= out_size) { + TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH); + return false; + } else { + memcpy(out_path, base_path, current_path_length); + // Postfix with "/" when the current path isn't "/" + if (current_path_length != 1) { + out_path[current_path_length] = '/'; + strcpy(&out_path[current_path_length + 1], child_path); + } else { + strcpy(&out_path[current_path_length], child_path); + } + return true; + } +} + FilesData* files_data_alloc() { FilesData* data = malloc(sizeof(FilesData)); *data = (FilesData) { - .current_path = {'/', 0x00}, + .current_path = { 0x00 }, .dir_entries = NULL, .dir_entries_count = 0 }; @@ -29,7 +50,7 @@ void files_data_free_entries(FilesData* data) { data->dir_entries_count = 0; } -void files_data_set_entries(FilesData* data, struct dirent** entries, int count) { +static void files_data_set_entries(FilesData* data, struct dirent** entries, int count) { if (data->dir_entries != NULL) { files_data_free_entries(data); } @@ -38,81 +59,54 @@ void files_data_set_entries(FilesData* data, struct dirent** entries, int count) data->dir_entries_count = count; } +bool files_data_set_entries_for_path(FilesData* data, const char* path) { + TT_LOG_I(TAG, "Changing path: %s -> %s", data->current_path, path); -void files_data_set_entries_navigate_up(FilesData* data) { - TT_LOG_I(TAG, "navigating upwards"); - char new_absolute_path[MAX_PATH_LENGTH]; - if (tt_string_get_path_parent(data->current_path, new_absolute_path)) { - if (strcmp(new_absolute_path, "/") == 0) { - files_data_set_entries_root(data); + /** + * ESP32 does not have a root directory, so we have to create it manually. + * We'll add the NVS Flash partitions and the binding for the sdcard. + */ + if (tt_get_platform() == PLATFORM_ESP && strcmp(path, "/") == 0) { + int dir_entries_count = 3; + struct dirent** dir_entries = malloc(sizeof(struct dirent*) * 3); + + dir_entries[0] = malloc(sizeof(struct dirent)); + dir_entries[0]->d_type = TT_DT_DIR; + strcpy(dir_entries[0]->d_name, "assets"); + + dir_entries[1] = malloc(sizeof(struct dirent)); + dir_entries[1]->d_type = TT_DT_DIR; + strcpy(dir_entries[1]->d_name, "config"); + + dir_entries[2] = malloc(sizeof(struct dirent)); + dir_entries[2]->d_type = TT_DT_DIR; + strcpy(dir_entries[2]->d_name, "sdcard"); + + files_data_set_entries(data, dir_entries, dir_entries_count); + strcpy(data->current_path, path); + return true; + } else { + struct dirent** entries = NULL; + int count = tt_scandir(path, &entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); + if (count >= 0) { + TT_LOG_I(TAG, "%s has %u entries", path, count); + files_data_set_entries(data, entries, count); + strcpy(data->current_path, path); + return true; } else { - strcpy(data->current_path, new_absolute_path); - data->dir_entries_count = tt_scandir(new_absolute_path, &(data->dir_entries), &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); - TT_LOG_I(TAG, "%s has %u entries", new_absolute_path, data->dir_entries_count); + TT_LOG_E(TAG, "Failed to fetch entries for %s", path); + return false; } } } -void files_data_set_entries_for_path(FilesData* data, const char* path) { - size_t current_path_length = strlen(data->current_path); - size_t added_path_length = strlen(path); - size_t total_path_length = current_path_length + added_path_length + 1; // two paths with `/` - - if (total_path_length >= MAX_PATH_LENGTH) { - TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH); - return; - } - +bool files_data_set_entries_for_child_path(FilesData* data, const char* child_path) { char new_absolute_path[MAX_PATH_LENGTH]; - memcpy(new_absolute_path, data->current_path, current_path_length); - // Postfix with "/" when the current path isn't "/" - if (current_path_length != 1) { - new_absolute_path[current_path_length] = '/'; - strcpy(&new_absolute_path[current_path_length + 1], path); + if (get_child_path(data->current_path, child_path, new_absolute_path, MAX_PATH_LENGTH)) { + TT_LOG_I(TAG, "Navigating from %s to %s", data->current_path, new_absolute_path); + return files_data_set_entries_for_path(data, new_absolute_path); } else { - strcpy(&new_absolute_path[current_path_length], path); - } - TT_LOG_I(TAG, "Navigating from %s to %s", data->current_path, new_absolute_path); - - struct dirent** entries = NULL; - int count = tt_scandir(new_absolute_path, &entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); - if (count >= 0) { - TT_LOG_I(TAG, "%s has %u entries", new_absolute_path, count); - files_data_set_entries(data, entries, count); - strcpy(data->current_path, new_absolute_path); - } else { - TT_LOG_E(TAG, "Failed to fetch entries for %s", new_absolute_path); + TT_LOG_I(TAG, "Failed to get child path for %s/%s", data->current_path, child_path); + return false; } } - -void files_data_set_entries_root(FilesData* data) { - data->current_path[0] = '/'; - data->current_path[1] = 0x00; -#ifdef ESP_PLATFORM - int dir_entries_count = 3; - struct dirent** dir_entries = malloc(sizeof(struct dirent*) * 3); - - dir_entries[0] = malloc(sizeof(struct dirent)); - dir_entries[0]->d_type = 4; - strcpy(dir_entries[0]->d_name, "assets"); - - dir_entries[1] = malloc(sizeof(struct dirent)); - dir_entries[1]->d_type = 4; - strcpy(dir_entries[1]->d_name, "config"); - - dir_entries[2] = malloc(sizeof(struct dirent)); - dir_entries[2]->d_type = 4; - strcpy(dir_entries[2]->d_name, "sdcard"); - - files_data_set_entries(data, dir_entries, dir_entries_count); - TT_LOG_I(TAG, "test: %s", dir_entries[0]->d_name); -#else - struct dirent** dir_entries = NULL; - int count = tt_scandir(data->current_path, &dir_entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); - if (count >= 0) { - files_data_set_entries(data, dir_entries, count); - } else { - TT_LOG_E(TAG, "Failed to fetch root dir items"); - } -#endif -} diff --git a/tactility/src/apps/system/files/files_data.h b/tactility/src/apps/system/files/files_data.h index 47000df9..5be80cf2 100644 --- a/tactility/src/apps/system/files/files_data.h +++ b/tactility/src/apps/system/files/files_data.h @@ -19,10 +19,8 @@ extern "C" { FilesData* files_data_alloc(); void files_data_free(FilesData* data); void files_data_free_entries(FilesData* data); -void files_data_set_entries(FilesData* data, struct dirent** entries, int count); -void files_data_set_entries_for_path(FilesData* data, const char* path); -void files_data_set_entries_navigate_up(FilesData* data); -void files_data_set_entries_root(FilesData* data); +bool files_data_set_entries_for_child_path(FilesData* data, const char* child_path); +bool files_data_set_entries_for_path(FilesData* data, const char* path); #ifdef __cplusplus } diff --git a/tactility/src/sdcard.c b/tactility/src/sdcard.c index 71583406..3bf5eca4 100644 --- a/tactility/src/sdcard.c +++ b/tactility/src/sdcard.c @@ -2,29 +2,32 @@ #include "mutex.h" #include "tactility_core.h" +#include "ui/statusbar.h" #define TAG "sdcard" -Mutex* mutex = NULL; +static Mutex* mutex = NULL; +static int8_t statusbar_icon = -1; typedef struct { const SdCard* sdcard; void* context; } MountData; -MountData data = { +static MountData data = { .sdcard = NULL, .context = NULL }; -static void sdcard_ensure_mutex() { +static void sdcard_ensure_initialized() { if (mutex == NULL) { mutex = tt_mutex_alloc(MutexTypeRecursive); + statusbar_icon = tt_statusbar_icon_add("A:/assets/sdcard_unmounted.png"); } } static bool sdcard_lock(uint32_t timeout_ticks) { - sdcard_ensure_mutex(); + sdcard_ensure_initialized(); return tt_mutex_acquire(mutex, timeout_ticks) == TtStatusOk; } @@ -47,7 +50,12 @@ bool tt_sdcard_mount(const SdCard* sdcard) { .sdcard = sdcard }; sdcard_unlock(); - return data.context != NULL; + if (data.context != NULL) { + tt_statusbar_icon_set_image(statusbar_icon, "A:/assets/sdcard_mounted.png"); + return true; + } else { + return false; + } } else { TT_LOG_E(TAG, "Failed to lock"); return false; @@ -70,6 +78,7 @@ bool tt_sdcard_unmount(uint32_t timeout_ticks) { .sdcard = NULL }; result = true; + tt_statusbar_icon_set_image(statusbar_icon, "A:/assets/sdcard_unmounted.png"); } else { TT_LOG_E(TAG, "Can't unmount: nothing mounted"); } diff --git a/tactility/src/services/gui/gui_draw.c b/tactility/src/services/gui/gui_draw.c index 66ddb51d..ef49c59a 100644 --- a/tactility/src/services/gui/gui_draw.c +++ b/tactility/src/services/gui/gui_draw.c @@ -1,10 +1,9 @@ #include "check.h" -#include "ui/lvgl_sync.h" #include "gui_i.h" #include "log.h" -#include "services/gui/widgets/statusbar.h" +#include "ui/lvgl_sync.h" +#include "ui/statusbar.h" #include "ui/style.h" -#include "ui/toolbar.h" #define TAG "gui" @@ -20,7 +19,7 @@ static lv_obj_t* create_app_views(Gui* gui, lv_obj_t* parent, App app) { // TODO: Move statusbar into separate ViewPort AppFlags flags = tt_app_get_flags(app); if (flags.show_statusbar) { - tt_lv_statusbar_create(vertical_container); + tt_statusbar_create(vertical_container); } lv_obj_t* child_container = lv_obj_create(vertical_container); diff --git a/tactility/src/services/gui/widgets/statusbar.c b/tactility/src/services/gui/widgets/statusbar.c deleted file mode 100644 index b5fa9713..00000000 --- a/tactility/src/services/gui/widgets/statusbar.c +++ /dev/null @@ -1,27 +0,0 @@ -#include "statusbar.h" - -#include "ui/spacer.h" -#include "ui/style.h" - -lv_obj_t* tt_lv_statusbar_create(lv_obj_t* parent) { - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_height(wrapper, STATUSBAR_HEIGHT); - tt_lv_obj_set_style_no_padding(wrapper); - tt_lv_obj_set_style_bg_blacken(wrapper); - lv_obj_center(wrapper); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); - - lv_obj_t* left_spacer = tt_lv_spacer_create(wrapper, 1, 1); - lv_obj_set_flex_grow(left_spacer, 1); - - lv_obj_t* wifi = lv_img_create(wrapper); - lv_obj_set_size(wifi, STATUSBAR_ICON_SIZE, STATUSBAR_ICON_SIZE); - tt_lv_obj_set_style_no_padding(wifi); - tt_lv_obj_set_style_bg_blacken(wifi); - lv_obj_set_style_img_recolor(wifi, lv_color_white(), 0); - lv_obj_set_style_img_recolor_opa(wifi, 255, 0); - lv_img_set_src(wifi, "A:/assets/ic_small_wifi_off.png"); - - return wrapper; -} \ No newline at end of file diff --git a/tactility/src/services/gui/widgets/statusbar.h b/tactility/src/services/gui/widgets/statusbar.h deleted file mode 100644 index 871d7c8e..00000000 --- a/tactility/src/services/gui/widgets/statusbar.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "lvgl.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define STATUSBAR_ICON_SIZE 18 -#define STATUSBAR_HEIGHT (STATUSBAR_ICON_SIZE + 4) // 4 extra pixels for border and outline - -lv_obj_t* tt_lv_statusbar_create(lv_obj_t* parent); - -#ifdef __cplusplus -} -#endif diff --git a/tactility/src/ui/statusbar.c b/tactility/src/ui/statusbar.c new file mode 100644 index 00000000..a84d2e08 --- /dev/null +++ b/tactility/src/ui/statusbar.c @@ -0,0 +1,219 @@ +#include "statusbar.h" + +#include "mutex.h" +#include "pubsub.h" +#include "tactility_core.h" +#include "ui/spacer.h" +#include "ui/style.h" + +#include "lvgl.h" +#include "lvgl_sync.h" + +#define TAG "statusbar" + +typedef struct { + const char* image; + bool visible; + bool claimed; +} StatusbarIcon; + +typedef struct { + Mutex* mutex; + PubSub* pubsub; + StatusbarIcon icons[STATUSBAR_ICON_LIMIT]; +} StatusbarData; + +static StatusbarData statusbar_data = { + .mutex = NULL, + .icons = {0} +}; + +typedef struct { + lv_obj_t obj; + lv_obj_t* icons[STATUSBAR_ICON_LIMIT]; + PubSubSubscription* pubsub_subscription; +} Statusbar; + +static void statusbar_init() { + statusbar_data.mutex = tt_mutex_alloc(MutexTypeNormal); + statusbar_data.pubsub = tt_pubsub_alloc(); + for (int i = 0; i < STATUSBAR_ICON_LIMIT; i++) { + statusbar_data.icons[i].image = NULL; + statusbar_data.icons[i].visible = false; + statusbar_data.icons[i].claimed = false; + } +} + +static void statusbar_ensure_initialized() { + if (statusbar_data.mutex == NULL) { + statusbar_init(); + } +} + +void statusbar_lock() { + statusbar_ensure_initialized(); + tt_mutex_acquire(statusbar_data.mutex, TtWaitForever); +} + +void statusbar_unlock() { + tt_mutex_release(statusbar_data.mutex); +} + +static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj); +static void statusbar_destructor(const lv_obj_class_t* class_p, lv_obj_t* obj); +static void statusbar_event(const lv_obj_class_t* class_p, lv_event_t* event); + +static const lv_obj_class_t statusbar_class = { + .constructor_cb = &statusbar_constructor, + .destructor_cb = &statusbar_destructor, + .event_cb = &statusbar_event, + .width_def = LV_PCT(100), + .height_def = STATUSBAR_HEIGHT, + .group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE, + .instance_size = sizeof(Statusbar), + .base_class = &lv_obj_class +}; + +static void statusbar_pubsub_event(TT_UNUSED const void* message, void* obj) { + TT_LOG_I(TAG, "event"); + Statusbar* statusbar = (Statusbar*)obj; + lv_obj_invalidate(&statusbar->obj); +} + +static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) { + LV_UNUSED(class_p); + LV_TRACE_OBJ_CREATE("begin"); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + LV_TRACE_OBJ_CREATE("finished"); + Statusbar* statusbar = (Statusbar*)obj; + statusbar_ensure_initialized(); + statusbar->pubsub_subscription = tt_pubsub_subscribe(statusbar_data.pubsub, &statusbar_pubsub_event, statusbar); +} + +static void statusbar_destructor(const lv_obj_class_t* class_p, lv_obj_t* obj) { + Statusbar* statusbar = (Statusbar*)obj; + tt_pubsub_unsubscribe(statusbar_data.pubsub, statusbar->pubsub_subscription); +} + +static void update_icon(lv_obj_t* image, const StatusbarIcon* icon) { + if (icon->image != NULL && icon->visible && icon->claimed) { + lv_obj_set_style_img_recolor(image, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(image, 255, 0); + lv_img_set_src(image, icon->image); + lv_obj_clear_flag(image, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(image, LV_OBJ_FLAG_HIDDEN); + } +} + +lv_obj_t* tt_statusbar_create(lv_obj_t* parent) { + LV_LOG_INFO("begin"); + lv_obj_t* obj = lv_obj_class_create_obj(&statusbar_class, parent); + lv_obj_class_init_obj(obj); + + Statusbar* statusbar = (Statusbar*)obj; + + lv_obj_set_width(obj, LV_PCT(100)); + lv_obj_set_height(obj, STATUSBAR_HEIGHT); + tt_lv_obj_set_style_no_padding(obj); + tt_lv_obj_set_style_bg_blacken(obj); + lv_obj_center(obj); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); + + lv_obj_t* left_spacer = tt_lv_spacer_create(obj, 1, 1); + lv_obj_set_flex_grow(left_spacer, 1); + + statusbar_lock(); + for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { + lv_obj_t* image = lv_img_create(obj); + lv_obj_set_size(image, STATUSBAR_ICON_SIZE, STATUSBAR_ICON_SIZE); + tt_lv_obj_set_style_no_padding(image); + tt_lv_obj_set_style_bg_blacken(image); + statusbar->icons[i] = image; + + update_icon(image, &(statusbar_data.icons[i])); + } + statusbar_unlock(); + + return obj; +} + +static void draw_main(lv_event_t* event) { + lv_obj_t* obj = lv_event_get_target(event); + Statusbar* statusbar = (Statusbar*)obj; + + statusbar_lock(); + for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { + update_icon(statusbar->icons[i], &(statusbar_data.icons[i])); + } + statusbar_unlock(); +} + +static void statusbar_event(TT_UNUSED const lv_obj_class_t* class_p, lv_event_t* event) { + // Call the ancestor's event handler + lv_res_t res = lv_obj_event_base(&statusbar_class, event); + if (res != LV_RES_OK) { + return; + } + + lv_event_code_t code = lv_event_get_code(event); + lv_obj_t* obj = lv_event_get_target(event); + + if (code == LV_EVENT_VALUE_CHANGED) { + lv_obj_invalidate(obj); + } else if (code == LV_EVENT_DRAW_MAIN) { + draw_main(event); + } +} + +int8_t tt_statusbar_icon_add(const char* image) { + statusbar_lock(); + int8_t result = -1; + for (int8_t i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { + if (!statusbar_data.icons[i].claimed) { + statusbar_data.icons[i].claimed = true; + statusbar_data.icons[i].visible = true; + statusbar_data.icons[i].image = image; + result = i; + TT_LOG_I(TAG, "id %d: added", i); + break; + } + } + tt_pubsub_publish(statusbar_data.pubsub, NULL); + statusbar_unlock(); + return result; +} + +void tt_statusbar_icon_remove(int8_t id) { + TT_LOG_I(TAG, "id %d: remove", id); + tt_check(id >= 0 && id < STATUSBAR_ICON_LIMIT); + statusbar_lock(); + StatusbarIcon* icon = &statusbar_data.icons[id]; + icon->claimed = false; + icon->visible = false; + icon->image = NULL; + tt_pubsub_publish(statusbar_data.pubsub, NULL); + statusbar_unlock(); +} + +void tt_statusbar_icon_set_image(int8_t id, const char* image) { + TT_LOG_I(TAG, "id %d: set image %s", id, image); + tt_check(id >= 0 && id < STATUSBAR_ICON_LIMIT); + statusbar_lock(); + StatusbarIcon* icon = &statusbar_data.icons[id]; + tt_check(icon->claimed); + icon->image = image; + tt_pubsub_publish(statusbar_data.pubsub, NULL); + statusbar_unlock(); +} + +void tt_statusbar_icon_set_visibility(int8_t id, bool visible) { + TT_LOG_I(TAG, "id %d: set visibility %d", id, visible); + tt_check(id >= 0 && id < STATUSBAR_ICON_LIMIT); + statusbar_lock(); + StatusbarIcon* icon = &statusbar_data.icons[id]; + tt_check(icon->claimed); + icon->visible = visible; + tt_pubsub_publish(statusbar_data.pubsub, NULL); + statusbar_unlock(); +} diff --git a/tactility/src/ui/statusbar.h b/tactility/src/ui/statusbar.h new file mode 100644 index 00000000..73350a7e --- /dev/null +++ b/tactility/src/ui/statusbar.h @@ -0,0 +1,22 @@ +#pragma once + +#include "lvgl.h" +#include "app.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define STATUSBAR_ICON_LIMIT 8 +#define STATUSBAR_ICON_SIZE 20 +#define STATUSBAR_HEIGHT (STATUSBAR_ICON_SIZE + 4) // 4 extra pixels for border and outline + +lv_obj_t* tt_statusbar_create(lv_obj_t* parent); +int8_t tt_statusbar_icon_add(const char* image); +void tt_statusbar_icon_remove(int8_t id); +void tt_statusbar_icon_set_image(int8_t id, const char* image); +void tt_statusbar_icon_set_visibility(int8_t id, bool visible); + +#ifdef __cplusplus +} +#endif