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
This commit is contained in:
Ken Van Hoeylandt 2024-02-07 23:11:39 +01:00 committed by GitHub
parent 5880e841a3
commit 29ea47a7ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 410 additions and 157 deletions

View File

@ -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*/

View File

@ -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.

View File

@ -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
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

View File

@ -8,6 +8,7 @@
#include "mutex.h"
#include "pubsub.h"
#include "service.h"
#include "ui/statusbar.h"
#include <sys/cdefs.h>
#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);
}

View File

@ -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;
}
}

View File

@ -7,18 +7,25 @@
#include "services/loader/loader.h"
#include "ui/toolbar.h"
#include <dirent.h>
#include <string_utils.h>
#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);
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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");
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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

View File

@ -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();
}

View File

@ -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