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:
parent
5880e841a3
commit
29ea47a7ba
@ -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*/
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
tactility-esp/assets/sdcard_mounted.png
Normal file
BIN
tactility-esp/assets/sdcard_mounted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 B |
BIN
tactility-esp/assets/sdcard_unmounted.png
Normal file
BIN
tactility-esp/assets/sdcard_unmounted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 B |
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
if (dir_entry->d_type == 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:
|
||||
} 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;
|
||||
break;
|
||||
default:
|
||||
} else {
|
||||
symbol = LV_SYMBOL_SETTINGS;
|
||||
break;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
void files_data_set_entries_root(FilesData* data) {
|
||||
data->current_path[0] = '/';
|
||||
data->current_path[1] = 0x00;
|
||||
#ifdef ESP_PLATFORM
|
||||
/**
|
||||
* 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 = 4;
|
||||
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 = 4;
|
||||
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 = 4;
|
||||
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);
|
||||
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);
|
||||
strcpy(data->current_path, path);
|
||||
return true;
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to fetch root dir items");
|
||||
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 {
|
||||
TT_LOG_E(TAG, "Failed to fetch entries for %s", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool files_data_set_entries_for_child_path(FilesData* data, const char* child_path) {
|
||||
char new_absolute_path[MAX_PATH_LENGTH];
|
||||
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 {
|
||||
TT_LOG_I(TAG, "Failed to get child path for %s/%s", data->current_path, child_path);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
219
tactility/src/ui/statusbar.c
Normal file
219
tactility/src/ui/statusbar.c
Normal 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();
|
||||
}
|
||||
22
tactility/src/ui/statusbar.h
Normal file
22
tactility/src/ui/statusbar.h
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user