C++ conversion (#80)

Converted project to C++
This commit is contained in:
Ken Van Hoeylandt
2024-11-22 20:26:08 +01:00
committed by GitHub
parent 6d80144e12
commit 85e26636a3
488 changed files with 6017 additions and 39466 deletions
+102
View File
@@ -0,0 +1,102 @@
#include "App_i.h"
namespace tt {
#define TAG "app"
void AppInstance::setState(AppState newState) {
mutex.acquire(TtWaitForever);
state = newState;
mutex.release();
}
AppState AppInstance::getState() {
mutex.acquire(TtWaitForever);
auto result = state;
mutex.release();
return result;
}
/** TODO: Make this thread-safe.
* In practice, the bundle is writeable, so someone could be writing to it
* while it is being accessed from another thread.
* Consider creating MutableBundle vs Bundle.
* Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it.
*/
const AppManifest& AppInstance::getManifest() {
return manifest;
}
AppFlags AppInstance::getFlags() {
mutex.acquire(TtWaitForever);
auto result = flags;
mutex.release();
return result;
}
void AppInstance::setFlags(AppFlags newFlags) {
mutex.acquire(TtWaitForever);
flags = newFlags;
mutex.release();
}
_Nullable void* AppInstance::getData() {
mutex.acquire(TtWaitForever);
auto result = data;
mutex.release();
return result;
}
void AppInstance::setData(void* newData) {
mutex.acquire(TtWaitForever);
data = newData;
mutex.release();
}
const Bundle& AppInstance::getParameters() {
return parameters;
}
App tt_app_alloc(const AppManifest& manifest, const Bundle& parameters) {
auto* instance = new AppInstance(manifest, parameters);
return static_cast<App>(instance);
}
void tt_app_free(App app) {
auto* instance = static_cast<AppInstance*>(app);
delete instance;
}
void tt_app_set_state(App app, AppState state) {
static_cast<AppInstance*>(app)->setState(state);
}
AppState tt_app_get_state(App app) {
return static_cast<AppInstance*>(app)->getState();
}
const AppManifest& tt_app_get_manifest(App app) {
return static_cast<AppInstance*>(app)->getManifest();
}
AppFlags tt_app_get_flags(App app) {
return static_cast<AppInstance*>(app)->getFlags();
}
void tt_app_set_flags(App app, AppFlags flags) {
return static_cast<AppInstance*>(app)->setFlags(flags);
}
void* tt_app_get_data(App app) {
return static_cast<AppInstance*>(app)->getData();
}
void tt_app_set_data(App app, void* data) {
return static_cast<AppInstance*>(app)->setData(data);
}
const Bundle& tt_app_get_parameters(App app) {
return static_cast<AppInstance*>(app)->getParameters();
}
} // namespace
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include "AppManifest.h"
#include "Bundle.h"
#include "Mutex.h"
namespace tt {
typedef enum {
AppStateInitial, // App is being activated in loader
AppStateStarted, // App is in memory
AppStateShowing, // App view is created
AppStateHiding, // App view is destroyed
AppStateStopped // App is not in memory
} AppState;
typedef union {
struct {
bool show_statusbar : 1;
};
unsigned char flags;
} AppFlags;
class AppInstance {
Mutex mutex = Mutex(MutexTypeNormal);
const AppManifest& manifest;
AppState state = AppStateInitial;
AppFlags flags = { .show_statusbar = true };
/** @brief Optional parameters to start the app with
* When these are stored in the app struct, the struct takes ownership.
* Do not mutate after app creation.
*/
tt::Bundle parameters;
/** @brief @brief Contextual data related to the running app's instance
* The app can attach its data to this.
* The lifecycle is determined by the on_start and on_stop methods in the AppManifest.
* These manifest methods can optionally allocate/free data that is attached here.
*/
void* _Nullable data = nullptr;
public:
AppInstance(const AppManifest& manifest) :
manifest(manifest) {}
AppInstance(const AppManifest& manifest, const Bundle& parameters) :
manifest(manifest),
parameters(parameters) {}
void setState(AppState state);
AppState getState();
const AppManifest& getManifest();
AppFlags getFlags();
void setFlags(AppFlags flags);
_Nullable void* getData();
void setData(void* data);
const Bundle& getParameters();
};
/** @brief Create an app
* @param manifest
* @param parameters optional bundle. memory ownership is transferred to App
* @return
*/
[[deprecated("use class")]]
App tt_app_alloc(const AppManifest& manifest, const Bundle& parameters);
[[deprecated("use class")]]
void tt_app_free(App app);
[[deprecated("use class")]]
void tt_app_set_state(App app, AppState state);
[[deprecated("use class")]]
AppState tt_app_get_state(App app);
[[deprecated("use class")]]
const AppManifest& tt_app_get_manifest(App app);
[[deprecated("use class")]]
AppFlags tt_app_get_flags(App app);
[[deprecated("use class")]]
void tt_app_set_flags(App app, AppFlags flags);
[[deprecated("use class")]]
void* _Nullable tt_app_get_data(App app);
[[deprecated("use class")]]
void tt_app_set_data(App app, void* data);
[[deprecated("use class")]]
const Bundle& tt_app_get_parameters(App app);
} // namespace
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include <string>
#include "CoreDefines.h"
// Forward declarations
typedef struct _lv_obj_t lv_obj_t;
namespace tt {
typedef void* App;
typedef enum {
/** A desktop app sits at the root of the app stack managed by the Loader service */
AppTypeDesktop,
/** Apps that generally aren't started from the desktop (e.g. image viewer) */
AppTypeHidden,
/** Standard apps, provided by the system. */
AppTypeSystem,
/** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
AppTypeSettings,
/** User-provided apps. */
AppTypeUser
} AppType;
typedef void (*AppOnStart)(App app);
typedef void (*AppOnStop)(App app);
typedef void (*AppOnShow)(App app, lv_obj_t* parent);
typedef void (*AppOnHide)(App app);
typedef struct AppManifest {
/**
* The identifier by which the app is launched by the system and other apps.
*/
std::string id;
/**
* The user-readable name of the app. Used in UI.
*/
std::string name;
/**
* Optional icon.
*/
std::string icon = {};
/**
* App type affects launch behaviour.
*/
const AppType type = AppTypeUser;
/**
* Non-blocking method to call when app is started.
*/
const AppOnStart on_start = nullptr;
/**
* Non-blocking method to call when app is stopped.
*/
const AppOnStop _Nullable on_stop = nullptr;
/**
* Non-blocking method to create the GUI
*/
const AppOnShow _Nullable on_show = nullptr;
/**
* Non-blocking method, called before gui is destroyed
*/
const AppOnHide _Nullable on_hide = nullptr;
} AppManifest;
} // namespace
+63
View File
@@ -0,0 +1,63 @@
#include "AppManifestRegistry.h"
#include "Mutex.h"
#include "TactilityCore.h"
#include <unordered_map>
#define TAG "app_registry"
namespace tt {
typedef std::unordered_map<std::string, const AppManifest*> AppManifestMap;
static AppManifestMap app_manifest_map;
static Mutex* hash_mutex = nullptr;
static void app_registry_lock() {
tt_assert(hash_mutex != nullptr);
tt_mutex_acquire(hash_mutex, TtWaitForever);
}
static void app_registry_unlock() {
tt_assert(hash_mutex != nullptr);
tt_mutex_release(hash_mutex);
}
void app_manifest_registry_init() {
tt_assert(hash_mutex == nullptr);
hash_mutex = tt_mutex_alloc(MutexTypeNormal);
}
void app_manifest_registry_add(const AppManifest* manifest) {
TT_LOG_I(TAG, "adding %s", manifest->id.c_str());
app_registry_lock();
app_manifest_map[manifest->id] = manifest;
app_registry_unlock();
}
_Nullable const AppManifest * app_manifest_registry_find_by_id(const std::string& id) {
app_registry_lock();
auto iterator = app_manifest_map.find(id);
_Nullable const AppManifest* result = iterator != app_manifest_map.end() ? iterator->second : nullptr;
app_registry_unlock();
return result;
}
void app_manifest_registry_for_each_of_type(AppType type, void* _Nullable context, AppManifestCallback callback) {
app_registry_lock();
for (auto& it : app_manifest_map) {
if (it.second->type == type) {
callback(it.second, context);
}
}
app_registry_unlock();
}
void app_manifest_registry_for_each(AppManifestCallback callback, void* _Nullable context) {
app_registry_lock();
for (auto& it : app_manifest_map) {
callback(it.second, context);
}
app_registry_unlock();
}
} // namespace
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "AppManifest.h"
#include <string>
namespace tt {
typedef void (*AppManifestCallback)(const AppManifest*, void* context);
void app_manifest_registry_init();
void app_manifest_registry_add(const AppManifest* manifest);
void app_manifest_registry_remove(const AppManifest* manifest);
const AppManifest _Nullable* app_manifest_registry_find_by_id(const std::string& id);
void app_manifest_registry_for_each(AppManifestCallback callback, void* _Nullable context);
void app_manifest_registry_for_each_of_type(AppType type, void* _Nullable context, AppManifestCallback callback);
} // namespace
+43
View File
@@ -0,0 +1,43 @@
#include "AppManifestRegistry.h"
#include "Assets.h"
#include "Check.h"
#include "lvgl.h"
#include "Services/Loader/Loader.h"
namespace tt::app::desktop {
static void on_app_pressed(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::start_app(manifest->id, false, Bundle());
}
}
static void create_app_widget(const AppManifest* manifest, void* parent) {
tt_check(parent);
auto* list = static_cast<lv_obj_t*>(parent);
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
lv_obj_add_event_cb(btn, &on_app_pressed, LV_EVENT_CLICKED, (void*)manifest);
}
static void desktop_show(TT_UNUSED App app, lv_obj_t* parent) {
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_size(list, LV_PCT(100), LV_PCT(100));
lv_obj_center(list);
lv_list_add_text(list, "User");
app_manifest_registry_for_each_of_type(AppTypeUser, list, create_app_widget);
lv_list_add_text(list, "System");
app_manifest_registry_for_each_of_type(AppTypeSystem, list, create_app_widget);
}
extern const AppManifest manifest = {
.id = "desktop",
.name = "Desktop",
.type = AppTypeDesktop,
.on_show = &desktop_show,
};
} // namespace
+134
View File
@@ -0,0 +1,134 @@
#include "App.h"
#include "Assets.h"
#include "DisplayPreferences.h"
#include "Tactility.h"
#include "lvgl.h"
#include "Ui/Toolbar.h"
namespace tt::app::settings::display {
#define TAG "display"
static bool backlight_duty_set = false;
static uint8_t backlight_duty = 255;
static void slider_event_cb(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
const Configuration* config = get_config();
hal::SetBacklightDuty set_backlight_duty = config->hardware->display.set_backlight_duty;
if (set_backlight_duty != nullptr) {
int32_t slider_value = lv_slider_get_value(slider);
backlight_duty = (uint8_t)slider_value;
backlight_duty_set = true;
set_backlight_duty(backlight_duty);
}
}
#define ORIENTATION_LANDSCAPE 0
#define ORIENTATION_LANDSCAPE_FLIPPED 1
#define ORIENTATION_PORTRAIT_LEFT 2
#define ORIENTATION_PORTRAIT_RIGHT 3
static lv_display_rotation_t orientation_setting_to_display_rotation(uint32_t setting) {
if (setting == ORIENTATION_LANDSCAPE_FLIPPED) {
return LV_DISPLAY_ROTATION_180;
} else if (setting == ORIENTATION_PORTRAIT_LEFT) {
return LV_DISPLAY_ROTATION_270;
} else if (setting == ORIENTATION_PORTRAIT_RIGHT) {
return LV_DISPLAY_ROTATION_90;
} else {
return LV_DISPLAY_ROTATION_0;
}
}
static uint32_t display_rotation_to_orientation_setting(lv_display_rotation_t orientation) {
if (orientation == LV_DISPLAY_ROTATION_90) {
return ORIENTATION_PORTRAIT_RIGHT;
} else if (orientation == LV_DISPLAY_ROTATION_180) {
return ORIENTATION_LANDSCAPE_FLIPPED;
} else if (orientation == LV_DISPLAY_ROTATION_270) {
return ORIENTATION_PORTRAIT_LEFT;
} else {
return ORIENTATION_LANDSCAPE;
}
}
static void on_orientation_set(lv_event_t* event) {
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t selected = lv_dropdown_get_selected(dropdown);
TT_LOG_I(TAG, "Selected %ld", selected);
lv_display_rotation_t rotation = orientation_setting_to_display_rotation(selected);
if (lv_display_get_rotation(lv_display_get_default()) != rotation) {
lv_display_set_rotation(lv_display_get_default(), rotation);
preferences_set_rotation(rotation);
}
}
static void app_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_t* brightness_wrapper = lv_obj_create(wrapper);
lv_obj_set_size(brightness_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_t* brightness_label = lv_label_create(brightness_wrapper);
lv_label_set_text(brightness_label, "Brightness");
lv_obj_t* brightness_slider = lv_slider_create(brightness_wrapper);
lv_obj_set_width(brightness_slider, LV_PCT(100));
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_set_pos(brightness_slider, 0, 30);
const Configuration* config = get_config();
hal::SetBacklightDuty set_backlight_duty = config->hardware->display.set_backlight_duty;
if (set_backlight_duty == nullptr) {
lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF);
lv_obj_add_state(brightness_slider, LV_STATE_DISABLED);
} else {
uint8_t value = preferences_get_backlight_duty();
lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF);
}
lv_obj_t* orientation_wrapper = lv_obj_create(wrapper);
lv_obj_set_size(orientation_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_t* orientation_label = lv_label_create(orientation_wrapper);
lv_label_set_text(orientation_label, "Orientation");
lv_obj_align(orientation_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* orientation_dropdown = lv_dropdown_create(orientation_wrapper);
lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right");
lv_obj_align(orientation_dropdown, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(orientation_dropdown, on_orientation_set, LV_EVENT_VALUE_CHANGED, nullptr);
uint32_t orientation_selected = display_rotation_to_orientation_setting(
lv_display_get_rotation(lv_display_get_default())
);
lv_dropdown_set_selected(orientation_dropdown, orientation_selected);
}
static void app_hide(TT_UNUSED App app) {
if (backlight_duty_set) {
preferences_set_backlight_duty(backlight_duty);
}
}
extern const AppManifest manifest = {
.id = "display",
.name = "Display",
.icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS,
.type = AppTypeSettings,
.on_start = nullptr,
.on_stop = nullptr,
.on_show = &app_show,
.on_hide = &app_hide
};
} // namespace
@@ -0,0 +1,37 @@
#include "DisplayPreferences.h"
#include "Preferences.h"
namespace tt::app::settings::display {
tt::Preferences preferences("display");
#define BACKLIGHT_DUTY_KEY "backlight_duty"
#define ROTATION_KEY "rotation"
void preferences_set_backlight_duty(uint8_t value) {
preferences.putInt32(BACKLIGHT_DUTY_KEY, (int32_t)value);
}
uint8_t preferences_get_backlight_duty() {
int32_t result;
if (preferences.optInt32(BACKLIGHT_DUTY_KEY, result)) {
return (uint8_t)(result % 255);
} else {
return 200;
}
}
void preferences_set_rotation(lv_display_rotation_t rotation) {
preferences.putInt32(ROTATION_KEY, (int32_t)rotation);
}
lv_display_rotation_t preferences_get_rotation() {
int32_t rotation;
if (preferences.optInt32(ROTATION_KEY, rotation)) {
return (lv_display_rotation_t)rotation;
} else {
return LV_DISPLAY_ROTATION_0;
}
}
} // namespace
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <src/display/lv_display.h>
namespace tt::app::settings::display {
void preferences_set_backlight_duty(uint8_t value);
uint8_t preferences_get_backlight_duty();
void preferences_set_rotation(lv_display_rotation_t rotation);
lv_display_rotation_t preferences_get_rotation();
} // namespace
+72
View File
@@ -0,0 +1,72 @@
#include "FileUtils.h"
#include "TactilityCore.h"
#include <cstdlib>
#include <cstring>
namespace tt::app::files {
#define TAG "file_utils"
#define SCANDIR_LIMIT 128
int dirent_filter_dot_entries(const struct dirent* entry) {
return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0;
}
int dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right) {
bool left_is_dir = (*left)->d_type == TT_DT_DIR;
bool right_is_dir = (*right)->d_type == TT_DT_DIR;
if (left_is_dir == right_is_dir) {
return strcmp((*left)->d_name, (*right)->d_name);
} else {
return (left_is_dir < right_is_dir) ? 1 : -1;
}
}
int dirent_sort_alpha(const struct dirent** left, const struct dirent** right) {
return strcmp((*left)->d_name, (*right)->d_name);
}
int scandir(
const char* path,
struct dirent*** output,
ScandirFilter _Nullable filter,
ScandirSort _Nullable sort
) {
DIR* dir = opendir(path);
if (dir == nullptr) {
return -1;
}
*output = static_cast<dirent**>(malloc(sizeof(void*) * SCANDIR_LIMIT));
struct dirent** dirent_array = *output;
int dirent_buffer_index = 0;
struct dirent* current_entry;
while ((current_entry = readdir(dir)) != nullptr) {
if (filter(current_entry) == 0) {
dirent_array[dirent_buffer_index] = static_cast<dirent*>(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);
break;
}
}
}
if (dirent_buffer_index == 0) {
free(*output);
*output = nullptr;
} else {
if (sort) {
qsort(dirent_array, dirent_buffer_index, sizeof(struct dirent*), (__compar_fn_t)sort);
}
}
closedir(dir);
return dirent_buffer_index;
};
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <dirent.h>
namespace tt::app::files {
/** File types for `dirent`'s `d_type`. */
enum {
TT_DT_UNKNOWN = 0,
#define TT_DT_UNKNOWN TT_DT_UNKNOWN
TT_DT_FIFO = 1,
#define TT_DT_FIFO TT_DT_FIFO
TT_DT_CHR = 2,
#define TT_DT_CHR TT_DT_CHR
TT_DT_DIR = 4,
#define TT_DT_DIR TT_DT_DIR
TT_DT_BLK = 6,
#define TT_DT_BLK TT_DT_BLK
TT_DT_REG = 8,
#define TT_DT_REG TT_DT_REG
TT_DT_LNK = 10,
#define TT_DT_LNK TT_DT_LNK
TT_DT_SOCK = 12,
#define TT_DT_SOCK TT_DT_SOCK
TT_DT_WHT = 14
#define TT_DT_WHT TT_DT_WHT
};
typedef int (*ScandirFilter)(const struct dirent*);
typedef int (*ScandirSort)(const struct dirent**, const struct dirent**);
/**
* Alphabetic sorting function for tt_scandir()
* @param left left-hand side part for comparison
* @param right right-hand side part for comparison
* @return 0, -1 or 1
*/
int dirent_sort_alpha(const struct dirent** left, const struct dirent** right);
int dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right);
int dirent_filter_dot_entries(const struct dirent* entry);
/**
* A scandir()-like implementation that works on ESP32.
* It does not return "." and ".." items but otherwise functions the same.
* It returns an allocated output array with allocated dirent instances.
* The caller is responsible for free-ing the memory of these.
*
* @param[in] path path the scan for files and directories
* @param[out] output a pointer to an array of dirent*
* @param[in] filter an optional filter to filter out specific items
* @param[in] sort an optional sorting function
* @return the amount of items that were stored in "output" or -1 when an error occurred
*/
int scandir(
const char* path,
struct dirent*** output,
ScandirFilter _Nullable filter,
ScandirSort _Nullable sort
);
} // namespace
+234
View File
@@ -0,0 +1,234 @@
#include "FilesData.h"
#include "App.h"
#include "Assets.h"
#include "Check.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include "Apps/ImageViewer/ImageViewer.h"
#include "Apps/TextViewer/TextViewer.h"
#include "lvgl.h"
#include "Services/Loader/Loader.h"
#include "Ui/Toolbar.h"
#include <dirent.h>
#include <unistd.h>
namespace tt::app::files {
#define TAG "files_app"
/**
* Case-insensitive 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, in lower case
* @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(path[base_len - postfix_len + i]) != tolower(extension[i])) {
return false;
}
}
return true;
}
static bool is_supported_image_file(const char* filename) {
// Currently only the PNG library is built into Tactility
return has_file_extension(filename, ".png");
}
static bool is_supported_text_file(const char* filename) {
return has_file_extension(filename, ".txt") ||
has_file_extension(filename, ".ini") ||
has_file_extension(filename, ".json") ||
has_file_extension(filename, ".yaml") ||
has_file_extension(filename, ".yml") ||
has_file_extension(filename, ".lua") ||
has_file_extension(filename, ".js") ||
has_file_extension(filename, ".properties");
}
// region Views
static void update_views(Data* data);
static void on_navigate_up_pressed(lv_event_t* event) {
auto* files_data = (Data*)lv_event_get_user_data(event);
if (strcmp(files_data->current_path, "/") != 0) {
TT_LOG_I(TAG, "Navigating upwards");
char new_absolute_path[MAX_PATH_LENGTH];
if (string_get_path_parent(files_data->current_path, new_absolute_path)) {
data_set_entries_for_path(files_data, new_absolute_path);
}
}
update_views(files_data);
}
static void on_exit_app_pressed(TT_UNUSED lv_event_t* event) {
service::loader::stop_app();
}
static void view_file(const char* path, const char* filename) {
size_t path_len = strlen(path);
size_t filename_len = strlen(filename);
char* filepath = static_cast<char*>(malloc(path_len + filename_len + 2));
sprintf(filepath, "%s/%s", path, filename);
// For PC we need to make the path relative to the current work directory,
// because that's how LVGL maps its 'drive letter' to the file system.
char* processed_filepath;
if (get_platform() == PlatformPc) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
TT_LOG_E(TAG, "Failed to get current working directory");
return;
}
if (!strstr(filepath, cwd)) {
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
return;
}
char* substr = filepath + strlen(cwd);
processed_filepath = substr;
} else {
processed_filepath = filepath;
}
TT_LOG_I(TAG, "Clicked %s", filepath);
if (is_supported_image_file(filename)) {
Bundle bundle;
bundle.putString(IMAGE_VIEWER_FILE_ARGUMENT, processed_filepath);
service::loader::start_app("ImageViewer", false, bundle);
} else if (is_supported_text_file(filename)) {
Bundle bundle;
if (get_platform() == PlatformEsp) {
bundle.putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath);
} else {
// Remove forward slash, because we need a relative path
bundle.putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath + 1);
}
service::loader::start_app("TextViewer", false, bundle);
} else {
TT_LOG_W(TAG, "opening files of this type is not supported");
}
free(filepath);
}
static void on_file_pressed(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event);
if (code == LV_EVENT_CLICKED) {
lv_obj_t* button = lv_event_get_current_target_obj(event);
auto* files_data = static_cast<Data*>(lv_obj_get_user_data(button));
auto* dir_entry = static_cast<dirent*>(lv_event_get_user_data(event));
TT_LOG_I(TAG, "Pressed %s %d", dir_entry->d_name, dir_entry->d_type);
switch (dir_entry->d_type) {
case TT_DT_DIR:
data_set_entries_for_child_path(files_data, dir_entry->d_name);
update_views(files_data);
break;
case TT_DT_LNK:
TT_LOG_W(TAG, "opening links is not supported");
break;
case TT_DT_REG:
view_file(files_data->current_path, dir_entry->d_name);
break;
default:
// Assume it's a file
// TODO: Find a better way to identify a file
view_file(files_data->current_path, dir_entry->d_name);
break;
}
}
}
static void create_file_widget(Data* files_data, lv_obj_t* parent, struct dirent* dir_entry) {
tt_check(parent);
auto* list = (lv_obj_t*)parent;
const char* symbol;
if (dir_entry->d_type == TT_DT_DIR) {
symbol = LV_SYMBOL_DIRECTORY;
} else if (is_supported_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_FILE;
}
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry->d_name);
lv_obj_set_user_data(button, files_data);
lv_obj_add_event_cb(button, &on_file_pressed, LV_EVENT_CLICKED, (void*)dir_entry);
}
static void update_views(Data* data) {
lv_obj_clean(data->list);
for (int i = 0; i < data->dir_entries_count; ++i) {
create_file_widget(data, data->list, data->dir_entries[i]);
}
}
// endregion Views
// region Lifecycle
static void on_show(App app, lv_obj_t* parent) {
auto* data = static_cast<Data*>(tt_app_get_data(app));
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create(parent, "Files");
lvgl::toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &on_exit_app_pressed, nullptr);
lvgl::toolbar_add_action(toolbar, LV_SYMBOL_UP, "Navigate up", &on_navigate_up_pressed, data);
data->list = lv_list_create(parent);
lv_obj_set_width(data->list, LV_PCT(100));
lv_obj_set_flex_grow(data->list, 1);
update_views(data);
}
static void on_start(App app) {
auto* data = data_alloc();
// PC platform is bound to current work directory because of the LVGL file system mapping
if (get_platform() == PlatformPc) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
data_set_entries_for_path(data, cwd);
} else {
TT_LOG_E(TAG, "Failed to get current work directory files");
data_set_entries_for_path(data, "/");
}
} else {
data_set_entries_for_path(data, "/");
}
tt_app_set_data(app, data);
}
static void on_stop(App app) {
auto* data = static_cast<Data*>(tt_app_get_data(app));
data_free(data);
}
// endregion Lifecycle
extern const AppManifest manifest = {
.id = "files",
.name = "Files",
.icon = TT_ASSETS_APP_ICON_FILES,
.type = AppTypeSystem,
.on_start = &on_start,
.on_stop = &on_stop,
.on_show = &on_show,
};
} // namespace
+114
View File
@@ -0,0 +1,114 @@
#include "FilesData.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include "TactilityCore.h"
namespace tt::app::files {
#define TAG "files_app"
static bool get_child_path(char* base_path, const char* child_path, char* out_path, size_t max_chars) {
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 >= max_chars) {
TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH);
return false;
} else {
// Postfix with "/" when the current path isn't "/"
if (current_path_length != 1) {
sprintf(out_path, "%s/%s", base_path, child_path);
} else {
sprintf(out_path, "/%s", child_path);
}
return true;
}
}
Data* data_alloc() {
auto* data = static_cast<Data*>(malloc(sizeof(Data)));
*data = (Data) {
.current_path = { 0x00 },
.dir_entries = nullptr,
.dir_entries_count = 0
};
return data;
}
void data_free(Data* data) {
data_free_entries(data);
free(data);
}
void data_free_entries(Data* data) {
for (int i = 0; i < data->dir_entries_count; ++i) {
free(data->dir_entries[i]);
}
free(data->dir_entries);
data->dir_entries = nullptr;
data->dir_entries_count = 0;
}
static void data_set_entries(Data* data, struct dirent** entries, int count) {
if (data->dir_entries != nullptr) {
data_free_entries(data);
}
data->dir_entries = entries;
data->dir_entries_count = count;
}
bool data_set_entries_for_path(Data* data, const char* path) {
TT_LOG_I(TAG, "Changing path: %s -> %s", data->current_path, path);
/**
* 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 (get_platform() == PlatformEsp && strcmp(path, "/") == 0) {
int dir_entries_count = 3;
auto** dir_entries = static_cast<dirent**>(malloc(sizeof(struct dirent*) * 3));
dir_entries[0] = static_cast<dirent*>(malloc(sizeof(struct dirent)));
dir_entries[0]->d_type = TT_DT_DIR;
strcpy(dir_entries[0]->d_name, "assets");
dir_entries[1] = static_cast<dirent*>(malloc(sizeof(struct dirent)));
dir_entries[1]->d_type = TT_DT_DIR;
strcpy(dir_entries[1]->d_name, "config");
dir_entries[2] = static_cast<dirent*>(malloc(sizeof(struct dirent)));
dir_entries[2]->d_type = TT_DT_DIR;
strcpy(dir_entries[2]->d_name, "sdcard");
data_set_entries(data, dir_entries, dir_entries_count);
strcpy(data->current_path, path);
return true;
} else {
struct dirent** entries = nullptr;
int count = tt::app::files::scandir(path, &entries, &dirent_filter_dot_entries, &dirent_sort_alpha_and_type);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path, count);
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 data_set_entries_for_child_path(Data* data, const char* child_path) {
char new_absolute_path[MAX_PATH_LENGTH + 1];
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 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;
}
}
} // namespace
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <dirent.h>
#include "lvgl.h"
namespace tt::app::files {
#define MAX_PATH_LENGTH 256
typedef struct {
char current_path[MAX_PATH_LENGTH];
struct dirent** dir_entries;
int dir_entries_count;
lv_obj_t* list;
} Data;
Data* data_alloc();
void data_free(Data* data);
void data_free_entries(Data* data);
bool data_set_entries_for_child_path(Data* data, const char* child_path);
bool data_set_entries_for_path(Data* data, const char* path);
} // namespace
+213
View File
@@ -0,0 +1,213 @@
#include "Mutex.h"
#include "Thread.h"
#include "Services/Loader/Loader.h"
#include "Ui/Toolbar.h"
#include "GpioHal.h"
#include "Ui/LvglSync.h"
namespace tt::app::gpio {
typedef struct {
lv_obj_t* lv_pins[GPIO_NUM_MAX];
uint8_t pin_states[GPIO_NUM_MAX];
Thread* thread;
Mutex* mutex;
bool thread_interrupted;
} Gpio;
static void lock(Gpio* gpio) {
tt_check(tt_mutex_acquire(gpio->mutex, 1000) == TtStatusOk);
}
static void unlock(Gpio* gpio) {
tt_check(tt_mutex_release(gpio->mutex) == TtStatusOk);
}
static void update_pin_states(Gpio* gpio) {
lock(gpio);
// Update pin states
for (int i = 0; i < GPIO_NUM_MAX; ++i) {
#ifdef ESP_PLATFORM
gpio->pin_states[i] = gpio_get_level((gpio_num_t)i);
#else
gpio->pin_states[i] = gpio_get_level(i);
#endif
}
unlock(gpio);
}
static void update_pin_widgets(Gpio* gpio) {
if (lvgl::lock(100)) {
lock(gpio);
for (int j = 0; j < GPIO_NUM_MAX; ++j) {
int level = gpio->pin_states[j];
lv_obj_t* label = gpio->lv_pins[j];
void* label_user_data = lv_obj_get_user_data(label);
// The user data stores the state, so we can avoid unnecessary updates
if ((void*)level != label_user_data) {
lv_obj_set_user_data(label, (void*)level);
if (level == 0) {
lv_obj_set_style_text_color(label, lv_color_black(), 0);
} else {
lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), 0);
}
}
}
lvgl::unlock();
unlock(gpio);
}
}
static lv_obj_t* create_gpio_row_wrapper(lv_obj_t* parent) {
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
return wrapper;
}
// region Task
static int32_t gpio_task(void* context) {
Gpio* gpio = (Gpio*)context;
bool interrupted = false;
while (!interrupted) {
delay_ms(100);
update_pin_states(gpio);
update_pin_widgets(gpio);
lock(gpio);
interrupted = gpio->thread_interrupted;
unlock(gpio);
}
return 0;
}
static void task_start(Gpio* gpio) {
tt_assert(gpio->thread == nullptr);
lock(gpio);
gpio->thread = thread_alloc_ex(
"gpio",
4096,
&gpio_task,
gpio
);
gpio->thread_interrupted = false;
thread_start(gpio->thread);
unlock(gpio);
}
static void task_stop(Gpio* gpio) {
tt_assert(gpio->thread);
lock(gpio);
gpio->thread_interrupted = true;
unlock(gpio);
thread_join(gpio->thread);
lock(gpio);
thread_free(gpio->thread);
gpio->thread = nullptr;
unlock(gpio);
}
// endregion Task
// region App lifecycle
static void app_show(App app, lv_obj_t* parent) {
auto* gpio = static_cast<Gpio*>(tt_app_get_data(app));
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Main content wrapper, enables scrolling content without scrolling the toolbar
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_style_border_width(wrapper, 0, 0);
uint8_t column = 0;
uint8_t column_limit = 10;
int32_t x_spacing = 20;
lv_obj_t* row_wrapper = create_gpio_row_wrapper(wrapper);
lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0);
lock(gpio);
for (int i = GPIO_NUM_MIN; i < GPIO_NUM_MAX; ++i) {
// Add the GPIO number before the first item on a row
if (column == 0) {
lv_obj_t* prefix = lv_label_create(row_wrapper);
lv_label_set_text_fmt(prefix, "%02d", i);
}
// Add a new GPIO status indicator
lv_obj_t* status_label = lv_label_create(row_wrapper);
lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0);
lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP);
gpio->lv_pins[i] = status_label;
column++;
if (column >= column_limit) {
// Add the GPIO number after the last item on a row
lv_obj_t* postfix = lv_label_create(row_wrapper);
lv_label_set_text_fmt(postfix, "%02d", i);
lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing), 0);
// Add a new row wrapper underneath the last one
lv_obj_t* new_row_wrapper = create_gpio_row_wrapper(wrapper);
lv_obj_align_to(new_row_wrapper, row_wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 4);
row_wrapper = new_row_wrapper;
column = 0;
}
}
unlock(gpio);
task_start(gpio);
}
static void on_hide(App app) {
auto* gpio = static_cast<Gpio*>(tt_app_get_data(app));
task_stop(gpio);
}
static void on_start(App app) {
auto* gpio = static_cast<Gpio*>(malloc(sizeof(Gpio)));
*gpio = (Gpio) {
.lv_pins = { nullptr },
.pin_states = { 0 },
.thread = nullptr,
.mutex = tt_mutex_alloc(MutexTypeNormal),
.thread_interrupted = true,
};
tt_app_set_data(app, gpio);
}
static void on_stop(App app) {
auto* gpio = static_cast<Gpio*>(tt_app_get_data(app));
tt_mutex_free(gpio->mutex);
free(gpio);
}
// endregion App lifecycle
extern const AppManifest manifest = {
.id = "gpio",
.name = "GPIO",
.type = AppTypeSystem,
.on_start = &on_start,
.on_stop = &on_stop,
.on_show = &app_show,
.on_hide = &on_hide
};
} // namespace
+7
View File
@@ -0,0 +1,7 @@
#ifndef ESP_PLATFORM
int gpio_get_level(int gpio_num) {
return gpio_num % 3;
}
#endif
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#ifdef ESP_PLATFORM
#include "driver/gpio.h"
#define GPIO_NUM_MIN GPIO_NUM_0
#else
#define GPIO_NUM_MIN 0
#define GPIO_NUM_MAX 50
#endif
#ifndef ESP_PLATFORM
int gpio_get_level(int gpio_num);
#endif
@@ -0,0 +1,41 @@
#include "ImageViewer.h"
#include "Log.h"
#include "lvgl.h"
#include "Ui/Style.h"
#include "Ui/Toolbar.h"
namespace tt::app::image_viewer {
#define TAG "image_viewer"
static void on_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
lv_obj_t* image = lv_img_create(wrapper);
lv_obj_align(image, LV_ALIGN_CENTER, 0, 0);
const Bundle& bundle = tt_app_get_parameters(app);
std::string file_argument;
if (bundle.optString(IMAGE_VIEWER_FILE_ARGUMENT, file_argument)) {
std::string prefixed_path = "A:" + file_argument;
TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str());
lv_img_set_src(image, prefixed_path.c_str());
}
}
extern const AppManifest manifest = {
.id = "ImageViewer",
.name = "Image Viewer",
.type = AppTypeHidden,
.on_show = &on_show
};
} // namespace
@@ -0,0 +1,3 @@
#pragma once
#define IMAGE_VIEWER_FILE_ARGUMENT "file"
+125
View File
@@ -0,0 +1,125 @@
#include "App.h"
#include "Assets.h"
#include "lvgl.h"
#include "Tactility.h"
#include "Timer.h"
#include "Ui/LvglSync.h"
#include "Ui/Style.h"
#include "Ui/Toolbar.h"
namespace tt::app::settings::power {
#define TAG "power"
typedef struct {
Timer* update_timer;
const hal::Power* power;
lv_obj_t* enable_switch;
lv_obj_t* charge_state;
lv_obj_t* charge_level;
lv_obj_t* current;
} AppData;
static void app_update_ui(App app) {
auto* data = static_cast<AppData*>(tt_app_get_data(app));
bool charging_enabled = data->power->is_charging_enabled();
const char* charge_state = data->power->is_charging() ? "yes" : "no";
uint8_t charge_level = data->power->get_charge_level();
uint16_t charge_level_scaled = (int16_t)charge_level * 100 / 255;
int32_t current = data->power->get_current();
lvgl::lock(ms_to_ticks(1000));
lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled);
lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state);
lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level_scaled);
#ifdef ESP_PLATFORM
lv_label_set_text_fmt(data->current, "Current: %ld mAh", current);
#else
lv_label_set_text_fmt(data->current, "Current: %d mAh", current);
#endif
lvgl::unlock();
}
static void on_power_enabled_change(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event);
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
if (code == LV_EVENT_VALUE_CHANGED) {
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
App app = lv_event_get_user_data(event);
auto* data = static_cast<AppData*>(tt_app_get_data(app));
if (data->power->is_charging_enabled() != is_on) {
data->power->set_charging_enabled(is_on);
app_update_ui(app);
}
}
}
static void app_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
auto* data = static_cast<AppData*>(tt_app_get_data(app));
// Top row: enable/disable
lv_obj_t* switch_container = lv_obj_create(wrapper);
lv_obj_set_width(switch_container, LV_PCT(100));
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(switch_container);
lvgl::obj_set_style_bg_invisible(switch_container);
lv_obj_t* enable_label = lv_label_create(switch_container);
lv_label_set_text(enable_label, "Charging enabled");
lv_obj_set_align(enable_label, LV_ALIGN_LEFT_MID);
lv_obj_t* enable_switch = lv_switch_create(switch_container);
lv_obj_add_event_cb(enable_switch, on_power_enabled_change, LV_EVENT_ALL, app);
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
data->enable_switch = enable_switch;
data->charge_state = lv_label_create(wrapper);
data->charge_level = lv_label_create(wrapper);
data->current = lv_label_create(wrapper);
app_update_ui(app);
timer_start(data->update_timer, ms_to_ticks(1000));
}
static void app_hide(TT_UNUSED App app) {
auto* data = static_cast<AppData*>(tt_app_get_data(app));
timer_stop(data->update_timer);
}
static void app_start(App app) {
auto* data = static_cast<AppData*>(malloc(sizeof(AppData)));
tt_app_set_data(app, data);
data->update_timer = timer_alloc(&app_update_ui, TimerTypePeriodic, app);
data->power = get_config()->hardware->power;
assert(data->power != nullptr); // The Power app only shows up on supported devices
}
static void app_stop(App app) {
auto* data = static_cast<AppData*>(tt_app_get_data(app));
timer_free(data->update_timer);
free(data);
}
extern const AppManifest manifest = {
.id = "power",
.name = "Power",
.icon = TT_ASSETS_APP_ICON_POWER_SETTINGS,
.type = AppTypeSettings,
.on_start = &app_start,
.on_stop = &app_stop,
.on_show = &app_show,
.on_hide = &app_hide
};
} // namespace
@@ -0,0 +1,30 @@
#include "ScreenshotUi.h"
namespace tt::app::screenshot {
static void on_show(App app, lv_obj_t* parent) {
auto* ui = static_cast<ScreenshotUi*>(tt_app_get_data(app));
create_ui(app, ui, parent);
}
static void on_start(App app) {
auto* ui = static_cast<ScreenshotUi*>(malloc(sizeof(ScreenshotUi)));
tt_app_set_data(app, ui);
}
static void on_stop(App app) {
auto* ui = static_cast<ScreenshotUi*>(tt_app_get_data(app));
free(ui);
}
extern const AppManifest manifest = {
.id = "screenshot",
.name = "Screenshot",
.icon = LV_SYMBOL_IMAGE,
.type = AppTypeSystem,
.on_start = &on_start,
.on_stop = &on_stop,
.on_show = &on_show,
};
} // namespace
@@ -0,0 +1,177 @@
#include "ScreenshotUi.h"
#include "TactilityCore.h"
#include "Hal/Sdcard.h"
#include "Services/Gui/Gui.h"
#include "Services/Screenshot/Screenshot.h"
#include "Ui/Toolbar.h"
namespace tt::app::screenshot {
#define TAG "screenshot_ui"
static void update_mode(ScreenshotUi* ui) {
lv_obj_t* label = ui->start_stop_button_label;
if (service::screenshot::is_started()) {
lv_label_set_text(label, "Stop");
} else {
lv_label_set_text(label, "Start");
}
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown);
if (selected == 0) { // Timer
lv_obj_remove_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN);
}
}
static void on_mode_set(lv_event_t* event) {
auto* ui = (ScreenshotUi*)lv_event_get_user_data(event);
update_mode(ui);
}
static void on_start_pressed(lv_event_t* event) {
auto* ui = static_cast<ScreenshotUi*>(lv_event_get_user_data(event));
if (service::screenshot::is_started()) {
TT_LOG_I(TAG, "Stop screenshot");
service::screenshot::stop();
} else {
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown);
const char* path = lv_textarea_get_text(ui->path_textarea);
if (selected == 0) {
TT_LOG_I(TAG, "Start timed screenshots");
const char* delay_text = lv_textarea_get_text(ui->delay_textarea);
int delay = atoi(delay_text);
if (delay > 0) {
service::screenshot::start_timed(path, delay, 1);
} else {
TT_LOG_W(TAG, "Ignored screenshot start because delay was 0");
}
} else {
TT_LOG_I(TAG, "Start app screenshots");
service::screenshot::start_apps(path);
}
}
update_mode(ui);
}
static void create_mode_setting_ui(ScreenshotUi* ui, lv_obj_t* parent) {
lv_obj_t* mode_wrapper = lv_obj_create(parent);
lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(mode_wrapper, 0, 0);
lv_obj_set_style_border_width(mode_wrapper, 0, 0);
lv_obj_t* mode_label = lv_label_create(mode_wrapper);
lv_label_set_text(mode_label, "Mode:");
lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* mode_dropdown = lv_dropdown_create(mode_wrapper);
lv_dropdown_set_options(mode_dropdown, "Timer\nApp start");
lv_obj_align_to(mode_dropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
lv_obj_add_event_cb(mode_dropdown, on_mode_set, LV_EVENT_VALUE_CHANGED, ui);
ui->mode_dropdown = mode_dropdown;
service::screenshot::ScreenshotMode mode = service::screenshot::get_mode();
if (mode == service::screenshot::ScreenshotModeApps) {
lv_dropdown_set_selected(mode_dropdown, 1);
}
lv_obj_t* button = lv_button_create(mode_wrapper);
lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
ui->start_stop_button_label = button_label;
lv_obj_add_event_cb(button, &on_start_pressed, LV_EVENT_CLICKED, ui);
}
static void create_path_ui(ScreenshotUi* ui, lv_obj_t* parent) {
lv_obj_t* path_wrapper = lv_obj_create(parent);
lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(path_wrapper, 0, 0);
lv_obj_set_style_border_width(path_wrapper, 0, 0);
lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_t* label_wrapper = lv_obj_create(path_wrapper);
lv_obj_set_style_border_width(label_wrapper, 0, 0);
lv_obj_set_style_pad_all(label_wrapper, 0, 0);
lv_obj_set_size(label_wrapper, 44, 36);
lv_obj_t* path_label = lv_label_create(label_wrapper);
lv_label_set_text(path_label, "Path:");
lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* path_textarea = lv_textarea_create(path_wrapper);
lv_textarea_set_one_line(path_textarea, true);
lv_obj_set_flex_grow(path_textarea, 1);
ui->path_textarea = path_textarea;
if (get_platform() == PlatformEsp) {
if (hal::sdcard::get_state() == hal::sdcard::StateMounted) {
lv_textarea_set_text(path_textarea, "A:/sdcard");
} else {
lv_textarea_set_text(path_textarea, "Error: no SD card");
}
} else { // PC
lv_textarea_set_text(path_textarea, "A:");
}
}
static void create_timer_settings_ui(ScreenshotUi* ui, lv_obj_t* parent) {
lv_obj_t* timer_wrapper = lv_obj_create(parent);
lv_obj_set_size(timer_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(timer_wrapper, 0, 0);
lv_obj_set_style_border_width(timer_wrapper, 0, 0);
ui->timer_wrapper = timer_wrapper;
lv_obj_t* delay_wrapper = lv_obj_create(timer_wrapper);
lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(delay_wrapper, 0, 0);
lv_obj_set_style_border_width(delay_wrapper, 0, 0);
lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_t* delay_label_wrapper = lv_obj_create(delay_wrapper);
lv_obj_set_style_border_width(delay_label_wrapper, 0, 0);
lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0);
lv_obj_set_size(delay_label_wrapper, 44, 36);
lv_obj_t* delay_label = lv_label_create(delay_label_wrapper);
lv_label_set_text(delay_label, "Delay:");
lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* delay_textarea = lv_textarea_create(delay_wrapper);
lv_textarea_set_one_line(delay_textarea, true);
lv_textarea_set_accepted_chars(delay_textarea, "0123456789");
lv_textarea_set_text(delay_textarea, "10");
lv_obj_set_flex_grow(delay_textarea, 1);
ui->delay_textarea = delay_textarea;
lv_obj_t* delay_unit_label_wrapper = lv_obj_create(delay_wrapper);
lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0);
lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0);
lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36);
lv_obj_t* delay_unit_label = lv_label_create(delay_unit_label_wrapper);
lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_label_set_text(delay_unit_label, "seconds");
}
void create_ui(App app, ScreenshotUi* ui, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
create_mode_setting_ui(ui, wrapper);
create_path_ui(ui, wrapper);
create_timer_settings_ui(ui, wrapper);
service::gui::keyboard_add_textarea(ui->delay_textarea);
service::gui::keyboard_add_textarea(ui->path_textarea);
update_mode(ui);
}
} // namespace
@@ -0,0 +1,18 @@
#pragma once
#include "App.h"
#include "lvgl.h"
namespace tt::app::screenshot {
typedef struct {
lv_obj_t* mode_dropdown;
lv_obj_t* path_textarea;
lv_obj_t* start_stop_button_label;
lv_obj_t* timer_wrapper;
lv_obj_t* delay_textarea;
} ScreenshotUi;
void create_ui(App app, ScreenshotUi* ui, lv_obj_t* parent);
} // namespace
@@ -0,0 +1,49 @@
#include "AppManifestRegistry.h"
#include "Assets.h"
#include "Check.h"
#include "lvgl.h"
#include "Services/Loader/Loader.h"
#include "Ui/Toolbar.h"
namespace tt::app::settings {
static void on_app_pressed(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::start_app(manifest->id, false, Bundle());
}
}
static void create_app_widget(const AppManifest* manifest, void* parent) {
tt_check(parent);
auto* list = (lv_obj_t*)parent;
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
lv_obj_add_event_cb(btn, &on_app_pressed, LV_EVENT_CLICKED, (void*)manifest);
}
static void on_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
app_manifest_registry_for_each_of_type(AppTypeSettings, list, create_app_widget);
}
extern const AppManifest manifest = {
.id = "settings",
.name = "Settings",
.icon = TT_ASSETS_APP_ICON_SETTINGS,
.type = AppTypeSystem,
.on_start = nullptr,
.on_stop = nullptr,
.on_show = &on_show,
.on_hide = nullptr
};
} // namespace
@@ -0,0 +1,115 @@
#include "Assets.h"
#include "lvgl.h"
#include "Tactility.h"
#include "Ui/Toolbar.h"
namespace tt::app::system_info {
static size_t get_heap_free() {
#ifdef ESP_PLATFORM
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
#else
return 4096 * 1024;
#endif
}
static size_t get_heap_total() {
#ifdef ESP_PLATFORM
return heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
#else
return 8192 * 1024;
#endif
}
static size_t get_spi_free() {
#ifdef ESP_PLATFORM
return heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
#else
return 4096 * 1024;
#endif
}
static size_t get_spi_total() {
#ifdef ESP_PLATFORM
return heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
#else
return 8192 * 1024;
#endif
}
static void add_memory_bar(lv_obj_t* parent, const char* label, size_t used, size_t total) {
lv_obj_t* container = lv_obj_create(parent);
lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(container, 0, 0);
lv_obj_set_style_border_width(container, 0, 0);
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
lv_obj_t* left_label = lv_label_create(container);
lv_label_set_text(left_label, label);
lv_obj_set_width(left_label, 60);
lv_obj_t* bar = lv_bar_create(container);
lv_obj_set_flex_grow(bar, 1);
if (total > 0) {
lv_bar_set_range(bar, 0, (int32_t)total);
} else {
lv_bar_set_range(bar, 0, 1);
}
lv_bar_set_value(bar, (int32_t)used, LV_ANIM_OFF);
lv_obj_t* bottom_label = lv_label_create(parent);
lv_label_set_text_fmt(bottom_label, "%u / %u kB", (used / 1024), (total / 1024));
lv_obj_set_width(bottom_label, LV_PCT(100));
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0);
}
static void on_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
// This wrapper automatically has its children added vertically underneath eachother
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
// Wrapper for the memory usage bars
lv_obj_t* memory_label = lv_label_create(wrapper);
lv_label_set_text(memory_label, "Memory usage");
lv_obj_t* memory_wrapper = lv_obj_create(wrapper);
lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
add_memory_bar(memory_wrapper, "Heap", get_heap_total() - get_heap_free(), get_heap_total());
add_memory_bar(memory_wrapper, "SPI", get_spi_total() - get_spi_free(), get_spi_total());
#ifdef ESP_PLATFORM
// Build info
lv_obj_t* build_info_label = lv_label_create(wrapper);
lv_label_set_text(build_info_label, "Build info");
lv_obj_t* build_info_wrapper = lv_obj_create(wrapper);
lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_t* esp_idf_version = lv_label_create(build_info_wrapper);
lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
#endif
}
extern const AppManifest manifest = {
.id = "systeminfo",
.name = "System Info",
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
.type = AppTypeSystem,
.on_start = nullptr,
.on_stop = nullptr,
.on_show = &on_show
};
} // namespace
@@ -0,0 +1,41 @@
#include "Log.h"
#include "TextViewer.h"
#include "lvgl.h"
#include "Ui/LabelUtils.h"
#include "Ui/Style.h"
#include "Ui/Toolbar.h"
#define TAG "text_viewer"
namespace tt::app::text_viewer {
static void on_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
lv_obj_t* label = lv_label_create(wrapper);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
const Bundle& bundle = tt_app_get_parameters(app);
std::string file_argument;
if (bundle.optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) {
std::string prefixed_path = "A:" + file_argument;
TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str());
lvgl::label_set_text_file(label, prefixed_path.c_str());
}
}
extern const AppManifest manifest = {
.id = "TextViewer",
.name = "Text Viewer",
.type = AppTypeHidden,
.on_show = &on_show
};
} // namespace
@@ -0,0 +1,3 @@
#pragma once
#define TEXT_VIEWER_FILE_ARGUMENT "file"
@@ -0,0 +1,146 @@
#include "WifiConnect.h"
#include "App.h"
#include "TactilityCore.h"
#include "WifiConnectStateUpdating.h"
#include "Services/Loader/Loader.h"
#include "Services/Wifi/Wifi.h"
#include "Ui/LvglSync.h"
namespace tt::app::wifi_connect {
#define TAG "wifi_connect"
// Forward declarations
static void event_callback(const void* message, void* context);
static void on_connect(const service::wifi::settings::WifiApSettings* ap_settings, bool remember, TT_UNUSED void* parameter) {
auto* wifi = static_cast<WifiConnect*>(parameter);
state_set_ap_settings(wifi, ap_settings);
state_set_connecting(wifi, true);
service::wifi::connect(ap_settings, remember);
}
static WifiConnect* wifi_connect_alloc() {
auto* wifi = static_cast<WifiConnect*>(malloc(sizeof(WifiConnect)));
PubSub* wifi_pubsub = service::wifi::get_pubsub();
wifi->wifi_subscription = tt_pubsub_subscribe(wifi_pubsub, &event_callback, wifi);
wifi->mutex = tt_mutex_alloc(MutexTypeNormal);
wifi->state = (WifiConnectState) {
.settings = {
.ssid = { 0 },
.password = { 0 },
.auto_connect = false,
},
.connection_error = false,
.is_connecting = false
};
wifi->bindings = (WifiConnectBindings) {
.on_connect_ssid = &on_connect,
.on_connect_ssid_context = wifi,
};
wifi->view_enabled = false;
return wifi;
}
static void wifi_connect_free(WifiConnect* wifi) {
PubSub* wifi_pubsub = service::wifi::get_pubsub();
tt_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription);
tt_mutex_free(wifi->mutex);
free(wifi);
}
void lock(WifiConnect* wifi) {
tt_assert(wifi);
tt_assert(wifi->mutex);
tt_mutex_acquire(wifi->mutex, TtWaitForever);
}
void unlock(WifiConnect* wifi) {
tt_assert(wifi);
tt_assert(wifi->mutex);
tt_mutex_release(wifi->mutex);
}
void request_view_update(WifiConnect* wifi) {
lock(wifi);
if (wifi->view_enabled) {
if (lvgl::lock(1000)) {
view_update(&wifi->view, &wifi->bindings, &wifi->state);
lvgl::unlock();
} else {
TT_LOG_E(TAG, "Failed to lock lvgl");
}
}
unlock(wifi);
}
static void event_callback(const void* message, void* context) {
auto* event = static_cast<const service::wifi::WifiEvent*>(message);
auto* wifi = static_cast<WifiConnect*>(context);
switch (event->type) {
case service::wifi::WifiEventTypeConnectionFailed:
if (wifi->state.is_connecting) {
state_set_connecting(wifi, false);
state_set_radio_error(wifi, true);
request_view_update(wifi);
}
break;
case service::wifi::WifiEventTypeConnectionSuccess:
if (wifi->state.is_connecting) {
state_set_connecting(wifi, false);
service::loader::stop_app();
}
break;
default:
break;
}
request_view_update(wifi);
}
static void app_show(App app, lv_obj_t* parent) {
auto* wifi = static_cast<WifiConnect*>(tt_app_get_data(app));
lock(wifi);
wifi->view_enabled = true;
view_create(app, wifi, parent);
view_update(&wifi->view, &wifi->bindings, &wifi->state);
unlock(wifi);
}
static void app_hide(App app) {
auto* wifi = static_cast<WifiConnect*>(tt_app_get_data(app));
// No need to lock view, as this is called from within Gui's LVGL context
view_destroy(&wifi->view);
lock(wifi);
wifi->view_enabled = false;
unlock(wifi);
}
static void app_start(App app) {
auto* wifi_connect = wifi_connect_alloc();
tt_app_set_data(app, wifi_connect);
}
static void app_stop(App app) {
auto* wifi = static_cast<WifiConnect*>(tt_app_get_data(app));
tt_assert(wifi != nullptr);
wifi_connect_free(wifi);
tt_app_set_data(app, nullptr);
}
extern const AppManifest manifest = {
.id = "wifi_connect",
.name = "Wi-Fi Connect",
.icon = LV_SYMBOL_WIFI,
.type = AppTypeSettings,
.on_start = &app_start,
.on_stop = &app_stop,
.on_show = &app_show,
.on_hide = &app_hide
};
} // namespace
@@ -0,0 +1,26 @@
#pragma once
#include "Mutex.h"
#include "WifiConnectBindings.h"
#include "WifiConnectState.h"
#include "WifiConnectView.h"
#include "Services/Wifi/Wifi.h"
namespace tt::app::wifi_connect {
typedef struct {
PubSubSubscription* wifi_subscription;
Mutex* mutex;
WifiConnectState state;
WifiConnectView view;
bool view_enabled;
WifiConnectBindings bindings;
} WifiConnect;
void lock(WifiConnect* wifi);
void unlock(WifiConnect* wifi);
void view_update(WifiConnect* wifi);
} // namespace
@@ -0,0 +1,14 @@
#pragma once
#include "Services/Wifi/WifiSettings.h"
namespace tt::app::wifi_connect {
typedef void (*OnConnectSsid)(const service::wifi::settings::WifiApSettings* settings, bool store, void* context);
typedef struct {
OnConnectSsid on_connect_ssid;
void* on_connect_ssid_context;
} WifiConnectBindings;
} // namespace
@@ -0,0 +1,4 @@
#pragma once
#define WIFI_CONNECT_PARAM_SSID "ssid" // String
#define WIFI_CONNECT_PARAM_PASSWORD "password" // String
@@ -0,0 +1,18 @@
#pragma once
#include "App.h"
#include "Services/Wifi/Wifi.h"
#include "Services/Wifi/WifiSettings.h"
namespace tt::app::wifi_connect {
/**
* View's state
*/
typedef struct {
service::wifi::settings::WifiApSettings settings;
bool connection_error;
bool is_connecting;
} WifiConnectState;
} // namespace
@@ -0,0 +1,23 @@
#include "WifiConnectStateUpdating.h"
namespace tt::app::wifi_connect {
void state_set_radio_error(WifiConnect* wifi, bool error) {
lock(wifi);
wifi->state.connection_error = error;
unlock(wifi);
}
void state_set_ap_settings(WifiConnect* wifi, const service::wifi::settings::WifiApSettings* settings) {
lock(wifi);
memcpy(&(wifi->state.settings), settings, sizeof(service::wifi::settings::WifiApSettings));
unlock(wifi);
}
void state_set_connecting(WifiConnect* wifi, bool is_connecting) {
lock(wifi);
wifi->state.is_connecting = is_connecting;
unlock(wifi);
}
} // namespace
@@ -0,0 +1,11 @@
#pragma once
#include "WifiConnect.h"
namespace tt::app::wifi_connect {
void state_set_radio_error(WifiConnect* wifi, bool error);
void state_set_ap_settings(WifiConnect* wifi, const service::wifi::settings::WifiApSettings* settings);
void state_set_connecting(WifiConnect* wifi, bool is_connecting);
} // namespace
@@ -0,0 +1,227 @@
#include "WifiConnectView.h"
#include "Log.h"
#include "WifiConnect.h"
#include "WifiConnectBundle.h"
#include "WifiConnectState.h"
#include "WifiConnectStateUpdating.h"
#include "lvgl.h"
#include "Services/Gui/Gui.h"
#include "Services/Wifi/WifiSettings.h"
#include "Ui/Style.h"
#include "Ui/Toolbar.h"
namespace tt::app::wifi_connect {
#define TAG "wifi_connect"
static void view_set_loading(WifiConnectView* view, bool loading);
static void reset_errors(WifiConnectView* view) {
lv_obj_add_flag(view->password_error, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(view->ssid_error, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(view->connection_error, LV_OBJ_FLAG_HIDDEN);
}
static void on_connect(lv_event_t* event) {
WifiConnect* wifi = (WifiConnect*)lv_event_get_user_data(event);
WifiConnectView* view = &wifi->view;
state_set_radio_error(wifi, false);
reset_errors(view);
const char* ssid = lv_textarea_get_text(view->ssid_textarea);
size_t ssid_len = strlen(ssid);
if (ssid_len > TT_WIFI_SSID_LIMIT) {
TT_LOG_E(TAG, "SSID too long");
lv_label_set_text(view->ssid_error, "SSID too long");
lv_obj_remove_flag(view->ssid_error, LV_OBJ_FLAG_HIDDEN);
return;
}
const char* password = lv_textarea_get_text(view->password_textarea);
size_t password_len = strlen(password);
if (password_len > TT_WIFI_CREDENTIALS_PASSWORD_LIMIT) {
TT_LOG_E(TAG, "Password too long");
lv_label_set_text(view->password_error, "Password too long");
lv_obj_remove_flag(view->password_error, LV_OBJ_FLAG_HIDDEN);
return;
}
bool store = lv_obj_get_state(view->remember_switch) & LV_STATE_CHECKED;
view_set_loading(view, true);
service::wifi::settings::WifiApSettings settings;
strcpy((char*)settings.password, password);
strcpy((char*)settings.ssid, ssid);
settings.auto_connect = TT_WIFI_AUTO_CONNECT; // No UI yet, so use global setting:w
WifiConnectBindings* bindings = &wifi->bindings;
bindings->on_connect_ssid(
&settings,
store,
bindings->on_connect_ssid_context
);
}
static void view_set_loading(WifiConnectView* view, bool loading) {
if (loading) {
lv_obj_add_flag(view->connect_button, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(view->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_state(view->password_textarea, LV_STATE_DISABLED);
lv_obj_add_state(view->ssid_textarea, LV_STATE_DISABLED);
lv_obj_add_state(view->remember_switch, LV_STATE_DISABLED);
} else {
lv_obj_remove_flag(view->connect_button, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(view->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_state(view->password_textarea, LV_STATE_DISABLED);
lv_obj_remove_state(view->ssid_textarea, LV_STATE_DISABLED);
lv_obj_remove_state(view->remember_switch, LV_STATE_DISABLED);
}
}
void view_create_bottom_buttons(WifiConnect* wifi, lv_obj_t* parent) {
WifiConnectView* view = &wifi->view;
lv_obj_t* button_container = lv_obj_create(parent);
lv_obj_set_width(button_container, LV_PCT(100));
lv_obj_set_height(button_container, LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(button_container);
lv_obj_set_style_border_width(button_container, 0, 0);
view->remember_switch = lv_switch_create(button_container);
lv_obj_add_state(view->remember_switch, LV_STATE_CHECKED);
lv_obj_align(view->remember_switch, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* remember_label = lv_label_create(button_container);
lv_label_set_text(remember_label, "Remember");
lv_obj_align(remember_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_align_to(remember_label, view->remember_switch, LV_ALIGN_OUT_RIGHT_MID, 4, 0);
view->connecting_spinner = lv_spinner_create(button_container);
lv_obj_set_size(view->connecting_spinner, 32, 32);
lv_obj_align(view->connecting_spinner, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_flag(view->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
view->connect_button = lv_btn_create(button_container);
lv_obj_t* connect_label = lv_label_create(view->connect_button);
lv_label_set_text(connect_label, "Connect");
lv_obj_align(view->connect_button, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(view->connect_button, &on_connect, LV_EVENT_CLICKED, wifi);
}
// TODO: Standardize dialogs
void view_create(App app, void* wifi, lv_obj_t* parent) {
WifiConnect* wifi_connect = (WifiConnect*)wifi;
WifiConnectView* view = &wifi_connect->view;
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
// SSID
lv_obj_t* ssid_wrapper = lv_obj_create(wrapper);
lv_obj_set_width(ssid_wrapper, LV_PCT(100));
lv_obj_set_height(ssid_wrapper, LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(ssid_wrapper);
lv_obj_set_style_border_width(ssid_wrapper, 0, 0);
lv_obj_t* ssid_label_wrapper = lv_obj_create(ssid_wrapper);
lv_obj_set_width(ssid_label_wrapper, LV_PCT(50));
lv_obj_set_height(ssid_label_wrapper, LV_SIZE_CONTENT);
lv_obj_align(ssid_label_wrapper, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_border_width(ssid_label_wrapper, 0, 0);
lv_obj_set_style_pad_left(ssid_label_wrapper, 0, 0);
lv_obj_set_style_pad_right(ssid_label_wrapper, 0, 0);
lv_obj_t* ssid_label = lv_label_create(ssid_label_wrapper);
lv_label_set_text(ssid_label, "Network:");
view->ssid_textarea = lv_textarea_create(ssid_wrapper);
lv_textarea_set_one_line(view->ssid_textarea, true);
lv_obj_align(view->ssid_textarea, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_width(view->ssid_textarea, LV_PCT(50));
view->ssid_error = lv_label_create(wrapper);
lv_obj_set_style_text_color(view->ssid_error, lv_color_make(255, 50, 50), 0);
lv_obj_add_flag(view->ssid_error, LV_OBJ_FLAG_HIDDEN);
// Password
lv_obj_t* password_wrapper = lv_obj_create(wrapper);
lv_obj_set_width(password_wrapper, LV_PCT(100));
lv_obj_set_height(password_wrapper, LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(password_wrapper);
lv_obj_set_style_border_width(password_wrapper, 0, 0);
lv_obj_t* password_label_wrapper = lv_obj_create(password_wrapper);
lv_obj_set_width(password_label_wrapper, LV_PCT(50));
lv_obj_set_height(password_label_wrapper, LV_SIZE_CONTENT);
lv_obj_align_to(password_label_wrapper, password_wrapper, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_border_width(password_label_wrapper, 0, 0);
lv_obj_set_style_pad_left(password_label_wrapper, 0, 0);
lv_obj_set_style_pad_right(password_label_wrapper, 0, 0);
lv_obj_t* password_label = lv_label_create(password_label_wrapper);
lv_label_set_text(password_label, "Password:");
view->password_textarea = lv_textarea_create(password_wrapper);
lv_textarea_set_one_line(view->password_textarea, true);
lv_textarea_set_password_mode(view->password_textarea, true);
lv_obj_align(view->password_textarea, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_width(view->password_textarea, LV_PCT(50));
view->password_error = lv_label_create(wrapper);
lv_obj_set_style_text_color(view->password_error, lv_color_make(255, 50, 50), 0);
lv_obj_add_flag(view->password_error, LV_OBJ_FLAG_HIDDEN);
// Connection error
view->connection_error = lv_label_create(wrapper);
lv_obj_set_style_text_color(view->connection_error, lv_color_make(255, 50, 50), 0);
lv_obj_add_flag(view->connection_error, LV_OBJ_FLAG_HIDDEN);
// Bottom buttons
view_create_bottom_buttons(wifi_connect, wrapper);
// Keyboard bindings
service::gui::keyboard_add_textarea(view->ssid_textarea);
service::gui::keyboard_add_textarea(view->password_textarea);
// Init from app parameters
const Bundle& bundle = tt_app_get_parameters(app);
std::string ssid;
if (bundle.optString(WIFI_CONNECT_PARAM_SSID, ssid)) {
lv_textarea_set_text(view->ssid_textarea, ssid.c_str());
}
std::string password;
if (bundle.optString(WIFI_CONNECT_PARAM_PASSWORD, password)) {
lv_textarea_set_text(view->password_textarea, password.c_str());
}
}
void view_destroy(TT_UNUSED WifiConnectView* view) {
// NO-OP
}
void view_update(
WifiConnectView* view,
TT_UNUSED WifiConnectBindings* bindings,
WifiConnectState* state
) {
if (state->connection_error) {
view_set_loading(view, false);
reset_errors(view);
lv_label_set_text(view->connection_error, "Connection failed");
lv_obj_remove_flag(view->connection_error, LV_OBJ_FLAG_HIDDEN);
}
}
} // namespace
@@ -0,0 +1,26 @@
#pragma once
#include "WifiConnectBindings.h"
#include "WifiConnectState.h"
#include "lvgl.h"
namespace tt::app::wifi_connect {
typedef struct {
lv_obj_t* ssid_textarea;
lv_obj_t* ssid_error;
lv_obj_t* password_textarea;
lv_obj_t* password_error;
lv_obj_t* connect_button;
lv_obj_t* cancel_button;
lv_obj_t* remember_switch;
lv_obj_t* connecting_spinner;
lv_obj_t* connection_error;
lv_group_t* group;
} WifiConnectView;
void view_create(App app, void* wifi, lv_obj_t* parent);
void view_update(WifiConnectView* view, WifiConnectBindings* bindings, WifiConnectState* state);
void view_destroy(WifiConnectView* view);
} // namespace
@@ -0,0 +1,180 @@
#include "WifiManage.h"
#include "App.h"
#include "Apps/WifiConnect/WifiConnectBundle.h"
#include "TactilityCore.h"
#include "Services/Loader/Loader.h"
#include "Services/Wifi/WifiSettings.h"
#include "Ui/LvglSync.h"
#include "WifiManageStateUpdating.h"
#include "WifiManageView.h"
namespace tt::app::wifi_manage {
#define TAG "wifi_manage"
// Forward declarations
static void event_callback(const void* message, void* context);
static void on_connect(const char* ssid) {
service::wifi::settings::WifiApSettings settings;
if (service::wifi::settings::load(ssid, &settings)) {
TT_LOG_I(TAG, "Connecting with known credentials");
service::wifi::connect(&settings, false);
} else {
TT_LOG_I(TAG, "Starting connection dialog");
Bundle bundle;
bundle.putString(WIFI_CONNECT_PARAM_SSID, ssid);
bundle.putString(WIFI_CONNECT_PARAM_PASSWORD, "");
service::loader::start_app("wifi_connect", false, bundle);
}
}
static void on_disconnect() {
service::wifi::disconnect();
}
static void on_wifi_toggled(bool enabled) {
service::wifi::set_enabled(enabled);
}
static WifiManage* wifi_manage_alloc() {
auto* wifi = static_cast<WifiManage*>(malloc(sizeof(WifiManage)));
wifi->wifi_subscription = nullptr;
wifi->mutex = tt_mutex_alloc(MutexTypeNormal);
wifi->state = (WifiManageState) {
.scanning = service::wifi::is_scanning(),
.radio_state = service::wifi::get_radio_state(),
.connect_ssid = { 0 },
.ap_records = { },
.ap_records_count = 0
};
wifi->view_enabled = false;
wifi->bindings = (WifiManageBindings) {
.on_wifi_toggled = &on_wifi_toggled,
.on_connect_ssid = &on_connect,
.on_disconnect = &on_disconnect
};
return wifi;
}
static void wifi_manage_free(WifiManage* wifi) {
tt_mutex_free(wifi->mutex);
free(wifi);
}
void lock(WifiManage* wifi) {
tt_assert(wifi);
tt_assert(wifi->mutex);
tt_mutex_acquire(wifi->mutex, TtWaitForever);
}
void unlock(WifiManage* wifi) {
tt_assert(wifi);
tt_assert(wifi->mutex);
tt_mutex_release(wifi->mutex);
}
void request_view_update(WifiManage* wifi) {
lock(wifi);
if (wifi->view_enabled) {
if (lvgl::lock(1000)) {
view_update(&wifi->view, &wifi->bindings, &wifi->state);
lvgl::unlock();
} else {
TT_LOG_E(TAG, "failed to lock lvgl");
}
}
unlock(wifi);
}
static void wifi_manage_event_callback(const void* message, void* context) {
auto* event = (service::wifi::WifiEvent*)message;
auto* wifi = (WifiManage*)context;
TT_LOG_I(TAG, "Update with state %d", service::wifi::get_radio_state());
state_set_radio_state(wifi, service::wifi::get_radio_state());
switch (event->type) {
case tt::service::wifi::WifiEventTypeScanStarted:
state_set_scanning(wifi, true);
break;
case tt::service::wifi::WifiEventTypeScanFinished:
state_set_scanning(wifi, false);
state_update_scanned_records(wifi);
break;
case tt::service::wifi::WifiEventTypeRadioStateOn:
if (!service::wifi::is_scanning()) {
service::wifi::scan();
}
break;
default:
break;
}
request_view_update(wifi);
}
static void app_show(App app, lv_obj_t* parent) {
auto* wifi = (WifiManage*)tt_app_get_data(app);
PubSub* wifi_pubsub = service::wifi::get_pubsub();
wifi->wifi_subscription = tt_pubsub_subscribe(wifi_pubsub, &wifi_manage_event_callback, wifi);
// State update (it has its own locking)
state_set_radio_state(wifi, service::wifi::get_radio_state());
state_set_scanning(wifi, service::wifi::is_scanning());
state_update_scanned_records(wifi);
// View update
lock(wifi);
wifi->view_enabled = true;
strcpy((char*)wifi->state.connect_ssid, "Connected"); // TODO update with proper SSID
view_create(app, &wifi->view, &wifi->bindings, parent);
view_update(&wifi->view, &wifi->bindings, &wifi->state);
unlock(wifi);
service::wifi::WifiRadioState radio_state = service::wifi::get_radio_state();
bool can_scan = radio_state == service::wifi::WIFI_RADIO_ON ||
radio_state == service::wifi::WIFI_RADIO_CONNECTION_PENDING ||
radio_state == service::wifi::WIFI_RADIO_CONNECTION_ACTIVE;
if (can_scan && !service::wifi::is_scanning()) {
service::wifi::scan();
}
}
static void app_hide(App app) {
auto* wifi = (WifiManage*)tt_app_get_data(app);
lock(wifi);
PubSub* wifi_pubsub = service::wifi::get_pubsub();
tt_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription);
wifi->wifi_subscription = nullptr;
wifi->view_enabled = false;
unlock(wifi);
}
static void app_start(App app) {
WifiManage* wifi = wifi_manage_alloc();
tt_app_set_data(app, wifi);
}
static void app_stop(App app) {
auto* wifi = (WifiManage*)tt_app_get_data(app);
tt_assert(wifi != nullptr);
wifi_manage_free(wifi);
tt_app_set_data(app, nullptr);
}
extern const AppManifest manifest = {
.id = "WifiManage",
.name = "Wi-Fi",
.icon = LV_SYMBOL_WIFI,
.type = AppTypeSettings,
.on_start = &app_start,
.on_stop = &app_stop,
.on_show = &app_show,
.on_hide = &app_hide
};
} // namespace
@@ -0,0 +1,24 @@
#pragma once
#include "Mutex.h"
#include "WifiManageView.h"
#include "Services/Wifi/Wifi.h"
namespace tt::app::wifi_manage {
typedef struct {
PubSubSubscription* wifi_subscription;
Mutex* mutex;
WifiManageState state;
WifiManageView view;
bool view_enabled;
WifiManageBindings bindings;
} WifiManage;
void lock(WifiManage* wifi);
void unlock(WifiManage* wifi);
void request_view_update(WifiManage* wifi);
} // namespace
@@ -0,0 +1,15 @@
#pragma once
namespace tt::app::wifi_manage {
typedef void (*OnWifiToggled)(bool enable);
typedef void (*OnConnectSsid)(const char* ssid);
typedef void (*OnDisconnect)();
typedef struct {
OnWifiToggled on_wifi_toggled;
OnConnectSsid on_connect_ssid;
OnDisconnect on_disconnect;
} WifiManageBindings;
} // namespace
@@ -0,0 +1,20 @@
#pragma once
#include "Services/Wifi/Wifi.h"
namespace tt::app::wifi_manage {
#define WIFI_SCAN_AP_RECORD_COUNT 16
/**
* View's state
*/
typedef struct {
bool scanning;
service::wifi::WifiRadioState radio_state;
uint8_t connect_ssid[33];
service::wifi::WifiApRecord ap_records[WIFI_SCAN_AP_RECORD_COUNT];
uint16_t ap_records_count;
} WifiManageState;
} // namespace
@@ -0,0 +1,27 @@
#include "WifiManage.h"
namespace tt::app::wifi_manage {
void state_set_scanning(WifiManage* wifi, bool is_scanning) {
lock(wifi);
wifi->state.scanning = is_scanning;
unlock(wifi);
}
void state_set_radio_state(WifiManage* wifi, service::wifi::WifiRadioState state) {
lock(wifi);
wifi->state.radio_state = state;
unlock(wifi);
}
void state_update_scanned_records(WifiManage* wifi) {
lock(wifi);
service::wifi::get_scan_results(
wifi->state.ap_records,
WIFI_SCAN_AP_RECORD_COUNT,
&wifi->state.ap_records_count
);
unlock(wifi);
}
} // namespace
@@ -0,0 +1,11 @@
#pragma once
#include "WifiManage.h"
namespace tt::app::wifi_manage {
void state_set_scanning(WifiManage* wifi, bool is_scanning);
void state_set_radio_state(WifiManage* wifi, service::wifi::WifiRadioState state);
void state_update_scanned_records(WifiManage* wifi);
} // namespace
@@ -0,0 +1,207 @@
#include "WifiManageView.h"
#include "Log.h"
#include "WifiManageState.h"
#include "Services/Statusbar/Statusbar.h"
#include "Services/Wifi/Wifi.h"
#include "Ui/Style.h"
#include "Ui/Toolbar.h"
namespace tt::app::wifi_manage {
#define TAG "wifi_main_view"
#define SPINNER_HEIGHT 40
static void on_enable_switch_changed(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event);
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
if (code == LV_EVENT_VALUE_CHANGED) {
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
auto* bindings = static_cast<WifiManageBindings*>(lv_event_get_user_data(event));
bindings->on_wifi_toggled(is_on);
}
}
static void on_disconnect_pressed(lv_event_t* event) {
auto* bindings = static_cast<WifiManageBindings*>(lv_event_get_user_data(event));
bindings->on_disconnect();
}
// region Secondary updates
static void connect(lv_event_t* event) {
lv_obj_t* button = lv_event_get_current_target_obj(event);
// Assumes that the second child of the button is a label ... risky
lv_obj_t* label = lv_obj_get_child(button, 1);
// We get the SSID from the button label because it's safer than alloc'ing
// our own and passing it as the event data
const char* ssid = lv_label_get_text(label);
TT_LOG_I(TAG, "Clicked AP: %s", ssid);
auto* bindings = static_cast<WifiManageBindings*>(lv_event_get_user_data(event));
bindings->on_connect_ssid(ssid);
}
static void create_network_button(WifiManageView* view, WifiManageBindings* bindings, service::wifi::WifiApRecord* record) {
const char* ssid = (const char*)record->ssid;
const char* icon = service::statusbar::get_status_icon_for_rssi(record->rssi, record->auth_mode != WIFI_AUTH_OPEN);
lv_obj_t* ap_button = lv_list_add_btn(
view->networks_list,
icon,
ssid
);
lv_obj_add_event_cb(ap_button, &connect, LV_EVENT_CLICKED, bindings);
}
static void update_network_list(WifiManageView* view, WifiManageState* state, WifiManageBindings* bindings) {
lv_obj_clean(view->networks_list);
switch (state->radio_state) {
case service::wifi::WIFI_RADIO_ON_PENDING:
case service::wifi::WIFI_RADIO_ON:
case service::wifi::WIFI_RADIO_CONNECTION_PENDING:
case service::wifi::WIFI_RADIO_CONNECTION_ACTIVE: {
lv_obj_clear_flag(view->networks_label, LV_OBJ_FLAG_HIDDEN);
if (state->ap_records_count > 0) {
for (int i = 0; i < state->ap_records_count; ++i) {
create_network_button(view, bindings, &state->ap_records[i]);
}
lv_obj_clear_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
} else if (state->scanning) {
lv_obj_add_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* label = lv_label_create(view->networks_list);
lv_label_set_text(label, "No networks found.");
}
break;
}
case service::wifi::WIFI_RADIO_OFF_PENDING:
case service::wifi::WIFI_RADIO_OFF: {
lv_obj_add_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(view->networks_label, LV_OBJ_FLAG_HIDDEN);
break;
}
}
}
void update_scanning(WifiManageView* view, WifiManageState* state) {
if (state->radio_state == service::wifi::WIFI_RADIO_ON && state->scanning) {
lv_obj_clear_flag(view->scanning_spinner, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(view->scanning_spinner, LV_OBJ_FLAG_HIDDEN);
}
}
static void update_wifi_toggle(WifiManageView* view, WifiManageState* state) {
lv_obj_clear_state(view->enable_switch, LV_STATE_ANY);
switch (state->radio_state) {
case service::wifi::WIFI_RADIO_ON:
case service::wifi::WIFI_RADIO_CONNECTION_PENDING:
case service::wifi::WIFI_RADIO_CONNECTION_ACTIVE:
lv_obj_add_state(view->enable_switch, LV_STATE_CHECKED);
break;
case service::wifi::WIFI_RADIO_ON_PENDING:
lv_obj_add_state(view->enable_switch, LV_STATE_CHECKED | LV_STATE_DISABLED);
break;
case service::wifi::WIFI_RADIO_OFF:
lv_obj_remove_state(view->enable_switch, LV_STATE_CHECKED | LV_STATE_DISABLED);
break;
case service::wifi::WIFI_RADIO_OFF_PENDING:
lv_obj_remove_state(view->enable_switch, LV_STATE_CHECKED);
lv_obj_add_state(view->enable_switch, LV_STATE_DISABLED);
break;
}
}
static void update_connected_ap(WifiManageView* view, WifiManageState* state, TT_UNUSED WifiManageBindings* bindings) {
switch (state->radio_state) {
case service::wifi::WIFI_RADIO_CONNECTION_PENDING:
case service::wifi::WIFI_RADIO_CONNECTION_ACTIVE:
lv_obj_clear_flag(view->connected_ap_container, LV_OBJ_FLAG_HIDDEN);
lv_label_set_text(view->connected_ap_label, (const char*)state->connect_ssid);
break;
default:
lv_obj_add_flag(view->connected_ap_container, LV_OBJ_FLAG_HIDDEN);
break;
}
}
// endregion Secondary updates
// region Main
void view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) {
view->root = parent;
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
// Top row: enable/disable
lv_obj_t* switch_container = lv_obj_create(wrapper);
lv_obj_set_width(switch_container, LV_PCT(100));
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(switch_container);
lvgl::obj_set_style_bg_invisible(switch_container);
lv_obj_t* enable_label = lv_label_create(switch_container);
lv_label_set_text(enable_label, "Wi-Fi");
lv_obj_set_align(enable_label, LV_ALIGN_LEFT_MID);
view->enable_switch = lv_switch_create(switch_container);
lv_obj_add_event_cb(view->enable_switch, on_enable_switch_changed, LV_EVENT_ALL, bindings);
lv_obj_set_align(view->enable_switch, LV_ALIGN_RIGHT_MID);
view->connected_ap_container = lv_obj_create(wrapper);
lv_obj_set_size(view->connected_ap_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_min_height(view->connected_ap_container, SPINNER_HEIGHT, 0);
lvgl::obj_set_style_no_padding(view->connected_ap_container);
lv_obj_set_style_border_width(view->connected_ap_container, 0, 0);
view->connected_ap_label = lv_label_create(view->connected_ap_container);
lv_obj_align(view->connected_ap_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* disconnect_button = lv_btn_create(view->connected_ap_container);
lv_obj_add_event_cb(disconnect_button, &on_disconnect_pressed, LV_EVENT_CLICKED, bindings);
lv_obj_t* disconnect_label = lv_label_create(disconnect_button);
lv_label_set_text(disconnect_label, "Disconnect");
lv_obj_align(disconnect_button, LV_ALIGN_RIGHT_MID, 0, 0);
// Networks
lv_obj_t* networks_header = lv_obj_create(wrapper);
lv_obj_set_size(networks_header, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_min_height(networks_header, SPINNER_HEIGHT, 0);
lvgl::obj_set_style_no_padding(networks_header);
lv_obj_set_style_border_width(networks_header, 0, 0);
view->networks_label = lv_label_create(networks_header);
lv_label_set_text(view->networks_label, "Networks");
lv_obj_align(view->networks_label, LV_ALIGN_LEFT_MID, 0, 0);
view->scanning_spinner = lv_spinner_create(networks_header);
lv_spinner_set_anim_params(view->scanning_spinner, 1000, 60);
lv_obj_set_size(view->scanning_spinner, SPINNER_HEIGHT, SPINNER_HEIGHT);
lv_obj_set_style_pad_top(view->scanning_spinner, 4, 0);
lv_obj_set_style_pad_bottom(view->scanning_spinner, 4, 0);
lv_obj_align_to(view->scanning_spinner, view->networks_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
view->networks_list = lv_obj_create(wrapper);
lv_obj_set_flex_flow(view->networks_list, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(view->networks_list, LV_PCT(100));
lv_obj_set_height(view->networks_list, LV_SIZE_CONTENT);
lv_obj_set_style_pad_top(view->networks_list, 8, 0);
lv_obj_set_style_pad_bottom(view->networks_list, 8, 0);
}
void view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state) {
update_wifi_toggle(view, state);
update_scanning(view, state);
update_network_list(view, state, bindings);
update_connected_ap(view, state, bindings);
}
} // namespace
@@ -0,0 +1,23 @@
#pragma once
#include "App.h"
#include "WifiManageBindings.h"
#include "WifiManageState.h"
#include "lvgl.h"
namespace tt::app::wifi_manage {
typedef struct {
lv_obj_t* root;
lv_obj_t* enable_switch;
lv_obj_t* scanning_spinner;
lv_obj_t* networks_label;
lv_obj_t* networks_list;
lv_obj_t* connected_ap_container;
lv_obj_t* connected_ap_label;
} WifiManageView;
void view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent);
void view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state);
} // namespace
+20
View File
@@ -0,0 +1,20 @@
#include "Apps/Display/DisplayPreferences.h"
#include "lvgl.h"
#include "LvglInit_i.h"
namespace tt {
void lvgl_init(const hal::Configuration* config) {
hal::SetBacklightDuty set_backlight_duty = config->display.set_backlight_duty;
if (set_backlight_duty != nullptr) {
int32_t backlight_duty = app::settings::display::preferences_get_backlight_duty();
set_backlight_duty(backlight_duty);
}
lv_display_rotation_t rotation = app::settings::display::preferences_get_rotation();
if (rotation != lv_disp_get_rotation(lv_disp_get_default())) {
lv_disp_set_rotation(lv_disp_get_default(), static_cast<lv_display_rotation_t>(rotation));
}
}
} // namespace
+166
View File
@@ -0,0 +1,166 @@
#include "Services/Gui/Gui_i.h"
#include "Tactility.h"
#include "Services/Loader/Loader.h"
#include "Ui/LvglKeypad.h"
#include "Ui/LvglSync.h"
#ifdef ESP_PLATFORM
#include "freertos/FreeRTOS.h"
#else
#include "FreeRTOS.h"
#endif
namespace tt::service::gui {
#define TAG "gui"
// Forward declarations
void redraw(Gui*);
static int32_t gui_main(void*);
Gui* gui = nullptr;
typedef void (*PubSubCallback)(const void* message, void* context);
void loader_callback(const void* message, TT_UNUSED void* context) {
auto* event = static_cast<const loader::LoaderEvent*>(message);
if (event->type == loader::LoaderEventTypeApplicationShowing) {
App* app = event->app_showing.app;
const AppManifest& app_manifest = tt_app_get_manifest(app);
show_app(app, app_manifest.on_show, app_manifest.on_hide);
} else if (event->type == loader::LoaderEventTypeApplicationHiding) {
hide_app();
}
}
Gui* gui_alloc() {
auto* instance = static_cast<Gui*>(malloc(sizeof(Gui)));
memset(instance, 0, sizeof(Gui));
tt_check(instance != NULL);
instance->thread = thread_alloc_ex(
"gui",
4096, // Last known minimum was 2800 for launching desktop
&gui_main,
nullptr
);
instance->mutex = tt_mutex_alloc(MutexTypeRecursive);
instance->keyboard = nullptr;
instance->loader_pubsub_subscription = tt_pubsub_subscribe(loader::get_pubsub(), &loader_callback, instance);
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
instance->keyboard_group = lv_group_create();
instance->lvgl_parent = lv_scr_act();
lvgl::unlock();
return instance;
}
void gui_free(Gui* instance) {
tt_assert(instance != nullptr);
thread_free(instance->thread);
tt_mutex_free(instance->mutex);
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
lv_group_del(instance->keyboard_group);
lvgl::unlock();
free(instance);
}
void lock() {
tt_assert(gui);
tt_assert(gui->mutex);
tt_check(tt_mutex_acquire(gui->mutex, configTICK_RATE_HZ) == TtStatusOk);
}
void unlock() {
tt_assert(gui);
tt_assert(gui->mutex);
tt_check(tt_mutex_release(gui->mutex) == TtStatusOk);
}
void request_draw() {
tt_assert(gui);
ThreadId thread_id = thread_get_id(gui->thread);
thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW);
}
void show_app(App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) {
lock();
tt_check(gui->app_view_port == NULL);
gui->app_view_port = view_port_alloc(app, on_show, on_hide);
unlock();
request_draw();
}
void hide_app() {
lock();
ViewPort* view_port = gui->app_view_port;
tt_check(view_port != NULL);
// We must lock the LVGL port, because the viewport hide callbacks
// might call LVGL APIs (e.g. to remove the keyboard from the screen root)
tt_check(lvgl::lock(configTICK_RATE_HZ));
view_port_hide(view_port);
lvgl::unlock();
view_port_free(view_port);
gui->app_view_port = nullptr;
unlock();
}
static int32_t gui_main(TT_UNUSED void* p) {
tt_check(gui);
Gui* local_gui = gui;
while (true) {
uint32_t flags = thread_flags_wait(
GUI_THREAD_FLAG_ALL,
TtFlagWaitAny,
TtWaitForever
);
// Process and dispatch draw call
if (flags & GUI_THREAD_FLAG_DRAW) {
thread_flags_clear(GUI_THREAD_FLAG_DRAW);
redraw(local_gui);
}
if (flags & GUI_THREAD_FLAG_EXIT) {
thread_flags_clear(GUI_THREAD_FLAG_EXIT);
break;
}
}
return 0;
}
// region AppManifest
static void start(TT_UNUSED Service& service) {
gui = gui_alloc();
thread_set_priority(gui->thread, THREAD_PRIORITY_SERVICE);
thread_start(gui->thread);
}
static void stop(TT_UNUSED Service& service) {
lock();
ThreadId thread_id = thread_get_id(gui->thread);
thread_flags_set(thread_id, GUI_THREAD_FLAG_EXIT);
thread_join(gui->thread);
thread_free(gui->thread);
unlock();
gui_free(gui);
}
extern const ServiceManifest manifest = {
.id = "Gui",
.on_start = &start,
.on_stop = &stop
};
// endregion
} // namespace
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include "App.h"
#include "ServiceManifest.h"
#include "ViewPort.h"
namespace tt::service::gui {
typedef struct Gui Gui;
/**
* Set the app viewport in the gui state and request the gui to draw it.
*
* @param app
* @param on_show
* @param on_hide
*/
void show_app(App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide);
/**
* Hide the current app's viewport.
* Does not request a re-draw because after hiding the current app,
* we always show the previous app, and there is always at least 1 app running.
*/
void hide_app();
/**
* Show the on-screen keyboard.
* @param textarea the textarea to focus the input for
*/
void keyboard_show(lv_obj_t* textarea);
/**
* Hide the on-screen keyboard.
* Has no effect when the keyboard is not visible.
*/
void keyboard_hide();
/**
* The on-screen keyboard is only shown when both of these conditions are true:
* - there is no hardware keyboard
* - TT_CONFIG_FORCE_ONSCREEN_KEYBOARD is set to true in tactility_config.h
* @return if we should show a on-screen keyboard for text input inside our apps
*/
bool keyboard_is_enabled();
/**
* Glue code for the on-screen keyboard and the hardware keyboard:
* - Attach automatic hide/show parameters for the on-screen keyboard.
* - Registers the textarea to the default lv_group_t for hardware keyboards.
* @param textarea
*/
void keyboard_add_textarea(lv_obj_t* textarea);
} // namespace
+68
View File
@@ -0,0 +1,68 @@
#include "Check.h"
#include "Log.h"
#include "Services/Gui/Gui_i.h"
#include "Ui/LvglSync.h"
#include "Ui/Statusbar.h"
#include "Ui/Style.h"
namespace tt::service::gui {
#define TAG "gui"
static lv_obj_t* create_app_views(Gui* gui, lv_obj_t* parent, App app) {
lvgl::obj_set_style_bg_blacken(parent);
lv_obj_t* vertical_container = lv_obj_create(parent);
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(vertical_container);
lvgl::obj_set_style_bg_blacken(vertical_container);
// TODO: Move statusbar into separate ViewPort
AppFlags flags = tt_app_get_flags(app);
if (flags.show_statusbar) {
lvgl::statusbar_create(vertical_container);
}
lv_obj_t* child_container = lv_obj_create(vertical_container);
lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_flex_grow(child_container, 1);
if (keyboard_is_enabled()) {
gui->keyboard = lv_keyboard_create(vertical_container);
lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
} else {
gui->keyboard = nullptr;
}
return child_container;
}
void redraw(Gui* gui) {
tt_assert(gui);
// Lock GUI and LVGL
lock();
if (lvgl::lock(1000)) {
lv_obj_clean(gui->lvgl_parent);
ViewPort* view_port = gui->app_view_port;
if (view_port != nullptr) {
App app = view_port->app;
lv_obj_t* container = create_app_views(gui, gui->lvgl_parent, app);
view_port_show(view_port, container);
} else {
TT_LOG_W(TAG, "nothing to draw");
}
// Unlock GUI and LVGL
lvgl::unlock();
} else {
TT_LOG_E(TAG, "failed to lock lvgl");
}
unlock();
}
} // namespace
@@ -0,0 +1,65 @@
#include "Services/Gui/Gui_i.h"
#include "TactilityConfig.h"
#include "Ui/LvglKeypad.h"
#include "Ui/LvglSync.h"
namespace tt::service::gui {
extern Gui* gui;
static void show_keyboard(lv_event_t* event) {
lv_obj_t* target = lv_event_get_current_target_obj(event);
keyboard_show(target);
lv_obj_scroll_to_view(target, LV_ANIM_ON);
}
static void hide_keyboard(TT_UNUSED lv_event_t* event) {
keyboard_hide();
}
bool keyboard_is_enabled() {
return !lvgl::keypad_is_available() || TT_CONFIG_FORCE_ONSCREEN_KEYBOARD;
}
void keyboard_show(lv_obj_t* textarea) {
lock();
if (gui->keyboard) {
lv_obj_clear_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
lv_keyboard_set_textarea(gui->keyboard, textarea);
}
unlock();
}
void keyboard_hide() {
lock();
if (gui->keyboard) {
lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
}
unlock();
}
void keyboard_add_textarea(lv_obj_t* textarea) {
lock();
tt_check(lvgl::lock(0), "lvgl should already be locked before calling this method");
if (keyboard_is_enabled()) {
lv_obj_add_event_cb(textarea, show_keyboard, LV_EVENT_FOCUSED, nullptr);
lv_obj_add_event_cb(textarea, hide_keyboard, LV_EVENT_DEFOCUSED, nullptr);
lv_obj_add_event_cb(textarea, hide_keyboard, LV_EVENT_READY, nullptr);
}
// lv_obj_t auto-remove themselves from the group when they are destroyed (last checked in LVGL 8.3)
lv_group_add_obj(gui->keyboard_group, textarea);
lvgl::keypad_activate(gui->keyboard_group);
lvgl::unlock();
unlock();
}
} // namespace
@@ -0,0 +1,44 @@
#include "ViewPort.h"
#include "Check.h"
#include "Services/Gui/ViewPort_i.h"
#include "Ui/Style.h"
namespace tt::service::gui {
#define TAG "viewport"
ViewPort* view_port_alloc(
App app,
ViewPortShowCallback on_show,
ViewPortHideCallback on_hide
) {
auto* view_port = static_cast<ViewPort*>(malloc(sizeof(ViewPort)));
view_port->app = app;
view_port->on_show = on_show;
view_port->on_hide = on_hide;
return view_port;
}
void view_port_free(ViewPort* view_port) {
tt_assert(view_port);
free(view_port);
}
void view_port_show(ViewPort* view_port, lv_obj_t* parent) {
tt_assert(view_port);
tt_assert(parent);
if (view_port->on_show) {
lvgl::obj_set_style_no_padding(parent);
view_port->on_show(view_port->app, parent);
}
}
void view_port_hide(ViewPort* view_port) {
tt_assert(view_port);
if (view_port->on_hide) {
view_port->on_hide(view_port->app);
}
}
} // namespace
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "App.h"
#include "lvgl.h"
namespace tt::service::gui {
/** ViewPort Draw callback
* @warning called from GUI thread
*/
typedef void (*ViewPortShowCallback)(App app, lv_obj_t* parent);
typedef void (*ViewPortHideCallback)(App app);
// TODO: Move internally, use handle publicly
typedef struct {
App app;
ViewPortShowCallback on_show;
ViewPortHideCallback _Nullable on_hide;
bool app_toolbar;
} ViewPort;
/** ViewPort allocator
*
* always returns view_port or stops system if not enough memory.
* @param app
* @param on_show Called to create LVGL widgets
* @param on_hide Called before clearing the LVGL widget parent
*
* @return ViewPort instance
*/
ViewPort* view_port_alloc(
App app,
ViewPortShowCallback on_show,
ViewPortHideCallback on_hide
);
/** ViewPort deallocator
*
* Ensure that view_port was unregistered in GUI system before use.
*
* @param view_port ViewPort instance
*/
void view_port_free(ViewPort* view_port);
} // namespace
+362
View File
@@ -0,0 +1,362 @@
#include "ApiLock.h"
#include "AppManifest.h"
#include "AppManifestRegistry.h"
#include "App_i.h"
#include "ServiceManifest.h"
#include "Services/Gui/Gui.h"
#include "Services/Loader/Loader_i.h"
#ifdef ESP_PLATFORM
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#else
#include "FreeRTOS.h"
#endif
namespace tt::service::loader {
#define TAG "loader"
typedef struct {
LoaderEventType type;
} LoaderEventInternal;
// Forward declarations
static int32_t loader_main(void* p);
static Loader* loader_singleton = nullptr;
static Loader* loader_alloc() {
assert(loader_singleton == nullptr);
loader_singleton = new Loader();
loader_singleton->pubsub_internal = tt_pubsub_alloc();
loader_singleton->pubsub_external = tt_pubsub_alloc();
loader_singleton->thread = thread_alloc_ex(
"loader",
4096, // Last known minimum was 2400 for starting Hello World app
&loader_main,
nullptr
);
loader_singleton->mutex = tt_mutex_alloc(MutexTypeRecursive);
loader_singleton->app_stack_index = -1;
memset(loader_singleton->app_stack, 0, sizeof(App) * APP_STACK_SIZE);
return loader_singleton;
}
static void loader_free() {
tt_assert(loader_singleton != nullptr);
thread_free(loader_singleton->thread);
tt_pubsub_free(loader_singleton->pubsub_internal);
tt_pubsub_free(loader_singleton->pubsub_external);
tt_mutex_free(loader_singleton->mutex);
delete loader_singleton;
loader_singleton = nullptr;
}
static void loader_lock() {
tt_assert(loader_singleton);
tt_assert(loader_singleton->mutex);
tt_check(tt_mutex_acquire(loader_singleton->mutex, TtWaitForever) == TtStatusOk);
}
static void loader_unlock() {
tt_assert(loader_singleton);
tt_assert(loader_singleton->mutex);
tt_check(tt_mutex_release(loader_singleton->mutex) == TtStatusOk);
}
LoaderStatus start_app(const std::string& id, bool blocking, const Bundle& bundle) {
tt_assert(loader_singleton);
LoaderMessageLoaderStatusResult result = {
.value = LoaderStatusOk
};
auto* start_message = new LoaderMessageAppStart(id, bundle);
LoaderMessage message(start_message, result);
ApiLock lock = blocking ? tt_api_lock_alloc_locked() : nullptr;
if (lock != nullptr) {
message.setLock(lock);
}
loader_singleton->queue.put(&message, TtWaitForever);
if (lock != nullptr) {
tt_api_lock_wait_unlock_and_free(message.api_lock);
}
return result.value;
}
void stop_app() {
tt_check(loader_singleton);
LoaderMessage message(LoaderMessageTypeAppStop);
loader_singleton->queue.put(&message, TtWaitForever);
}
App _Nullable get_current_app() {
tt_assert(loader_singleton);
loader_lock();
App app = (loader_singleton->app_stack_index >= 0)
? loader_singleton->app_stack[loader_singleton->app_stack_index]
: nullptr;
loader_unlock();
return app;
}
PubSub* get_pubsub() {
tt_assert(loader_singleton);
// it's safe to return pubsub without locking
// because it's never freed and loader is never exited
// also the loader instance cannot be obtained until the pubsub is created
return loader_singleton->pubsub_external;
}
static const char* app_state_to_string(AppState state) {
switch (state) {
case AppStateInitial:
return "initial";
case AppStateStarted:
return "started";
case AppStateShowing:
return "showing";
case AppStateHiding:
return "hiding";
case AppStateStopped:
return "stopped";
default:
return "?";
}
}
static void app_transition_to_state(App app, AppState state) {
const AppManifest& manifest = tt_app_get_manifest(app);
const AppState old_state = tt_app_get_state(app);
TT_LOG_I(
TAG,
"app \"%s\" state: %s -> %s",
manifest.id.c_str(),
app_state_to_string(old_state),
app_state_to_string(state)
);
switch (state) {
case AppStateInitial:
tt_app_set_state(app, AppStateInitial);
break;
case AppStateStarted:
if (manifest.on_start != nullptr) {
manifest.on_start(app);
}
tt_app_set_state(app, AppStateStarted);
break;
case AppStateShowing: {
LoaderEvent event_showing = {
.type = LoaderEventTypeApplicationShowing,
.app_showing = {
.app = static_cast<App*>(app)
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_showing);
tt_app_set_state(app, AppStateShowing);
break;
}
case AppStateHiding: {
LoaderEvent event_hiding = {
.type = LoaderEventTypeApplicationHiding,
.app_hiding = {
.app = static_cast<App*>(app)
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_hiding);
tt_app_set_state(app, AppStateHiding);
break;
}
case AppStateStopped:
if (manifest.on_stop) {
manifest.on_stop(app);
}
tt_app_set_data(app, nullptr);
tt_app_set_state(app, AppStateStopped);
break;
}
}
static LoaderStatus loader_do_start_app_with_manifest(
const AppManifest* manifest,
const Bundle& bundle
) {
TT_LOG_I(TAG, "start with manifest %s", manifest->id.c_str());
loader_lock();
if (loader_singleton->app_stack_index >= (APP_STACK_SIZE - 1)) {
TT_LOG_E(TAG, "failed to start app: stack limit of %d reached", APP_STACK_SIZE);
return LoaderStatusErrorInternal;
}
int8_t previous_index = loader_singleton->app_stack_index;
loader_singleton->app_stack_index++;
App app = tt_app_alloc(*manifest, bundle);
tt_check(loader_singleton->app_stack[loader_singleton->app_stack_index] == nullptr);
loader_singleton->app_stack[loader_singleton->app_stack_index] = app;
app_transition_to_state(app, AppStateInitial);
app_transition_to_state(app, AppStateStarted);
// We might have to hide the previous app first
if (previous_index != -1) {
App previous_app = loader_singleton->app_stack[previous_index];
app_transition_to_state(previous_app, AppStateHiding);
}
app_transition_to_state(app, AppStateShowing);
loader_unlock();
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted};
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
LoaderEvent event_external = {
.type = LoaderEventTypeApplicationStarted,
.app_started = {
.app = static_cast<App*>(app)
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_external);
return LoaderStatusOk;
}
static LoaderStatus do_start_by_id(
const std::string& id,
const Bundle& bundle
) {
TT_LOG_I(TAG, "Start by id %s", id.c_str());
const AppManifest* manifest = app_manifest_registry_find_by_id(id);
if (manifest == nullptr) {
return LoaderStatusErrorUnknownApp;
} else {
return loader_do_start_app_with_manifest(manifest, bundle);
}
}
static void do_stop_app() {
loader_lock();
int8_t current_app_index = loader_singleton->app_stack_index;
if (current_app_index == -1) {
loader_unlock();
TT_LOG_E(TAG, "Stop app: no app running");
return;
}
if (current_app_index == 0) {
loader_unlock();
TT_LOG_E(TAG, "Stop app: can't stop root app");
return;
}
// Stop current app
App app_to_stop = loader_singleton->app_stack[current_app_index];
const AppManifest& manifest = tt_app_get_manifest(app_to_stop);
app_transition_to_state(app_to_stop, AppStateHiding);
app_transition_to_state(app_to_stop, AppStateStopped);
tt_app_free(app_to_stop);
loader_singleton->app_stack[current_app_index] = nullptr;
loader_singleton->app_stack_index--;
#ifdef ESP_PLATFORM
TT_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
#endif
// Resume previous app
tt_assert(loader_singleton->app_stack[loader_singleton->app_stack_index] != nullptr);
App app_to_resume = loader_singleton->app_stack[loader_singleton->app_stack_index];
app_transition_to_state(app_to_resume, AppStateShowing);
loader_unlock();
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStopped};
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
LoaderEvent event_external = {
.type = LoaderEventTypeApplicationStopped,
.app_stopped = {
.manifest = &manifest
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_external);
}
static int32_t loader_main(TT_UNUSED void* parameter) {
LoaderMessage message;
bool exit_requested = false;
while (!exit_requested) {
tt_assert(loader_singleton != nullptr);
if (loader_singleton->queue.get(&message, TtWaitForever) == TtStatusOk) {
TT_LOG_I(TAG, "Processing message of type %d", message.type);
switch (message.type) {
case LoaderMessageTypeAppStart:
message.result.status_value.value = do_start_by_id(
message.payload.start->id,
message.payload.start->bundle
);
if (message.api_lock != nullptr) {
tt_api_lock_unlock(message.api_lock);
}
message.cleanup();
break;
case LoaderMessageTypeAppStop:
do_stop_app();
break;
case LoaderMessageTypeServiceStop:
exit_requested = true;
break;
case LoaderMessageTypeNone:
break;
}
}
}
return 0;
}
// region AppManifest
static void loader_start(TT_UNUSED Service& service) {
tt_check(loader_singleton == nullptr);
loader_singleton = loader_alloc();
thread_set_priority(loader_singleton->thread, THREAD_PRIORITY_SERVICE);
thread_start(loader_singleton->thread);
}
static void loader_stop(TT_UNUSED Service& service) {
tt_check(loader_singleton != nullptr);
// Send stop signal to thread and wait for thread to finish
LoaderMessage message(LoaderMessageTypeServiceStop);
loader_singleton->queue.put(&message, TtWaitForever);
thread_join(loader_singleton->thread);
thread_free(loader_singleton->thread);
loader_free();
loader_singleton = nullptr;
}
extern const ServiceManifest manifest = {
.id = "Loader",
.on_start = &loader_start,
.on_stop = &loader_stop
};
// endregion
} // namespace
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include "AppManifest.h"
#include "Bundle.h"
#include "Pubsub.h"
#include "ServiceManifest.h"
#include "TactilityCore.h"
namespace tt::service::loader {
typedef struct Loader Loader;
typedef enum {
LoaderStatusOk,
LoaderStatusErrorAppStarted,
LoaderStatusErrorUnknownApp,
LoaderStatusErrorInternal,
} LoaderStatus;
typedef enum {
LoaderEventTypeApplicationStarted,
LoaderEventTypeApplicationShowing,
LoaderEventTypeApplicationHiding,
LoaderEventTypeApplicationStopped
} LoaderEventType;
typedef struct {
App* app;
} LoaderEventAppStarted;
typedef struct {
App* app;
} LoaderEventAppShowing;
typedef struct {
App* app;
} LoaderEventAppHiding;
typedef struct {
const AppManifest* manifest;
} LoaderEventAppStopped;
typedef struct {
LoaderEventType type;
union {
LoaderEventAppStarted app_started;
LoaderEventAppShowing app_showing;
LoaderEventAppHiding app_hiding;
LoaderEventAppStopped app_stopped;
};
} LoaderEvent;
/**
* @brief Start an app
* @param[in] id application name or id
* @param[in] blocking application arguments
* @param[in] bundle optional bundle. Ownership is transferred to Loader.
* @return LoaderStatus
*/
LoaderStatus start_app(const std::string& id, bool blocking, const Bundle& bundle);
/**
* @brief Stop the currently showing app. Show the previous app if any app was still running.
*/
void stop_app();
App _Nullable get_current_app();
/**
* @brief PubSub for LoaderEvent
*/
PubSub* get_pubsub();
} // namespace
@@ -0,0 +1,141 @@
#include "Screenshot.h"
#include <cstdlib>
#include "Mutex.h"
#include "ScreenshotTask.h"
#include "Service.h"
#include "ServiceRegistry.h"
#include "TactilityCore.h"
namespace tt::service::screenshot {
#define TAG "sdcard_service"
extern const ServiceManifest manifest;
typedef struct {
Mutex* mutex;
ScreenshotTask* task;
ScreenshotMode mode;
} ServiceData;
static ServiceData* service_data_alloc() {
auto* data = static_cast<ServiceData*>(malloc(sizeof(ServiceData)));
*data = (ServiceData) {
.mutex = tt_mutex_alloc(MutexTypeNormal),
.task = nullptr,
.mode = ScreenshotModeNone
};
return data;
}
static void service_data_free(ServiceData* data) {
tt_mutex_free(data->mutex);
}
static void service_data_lock(ServiceData* data) {
tt_check(tt_mutex_acquire(data->mutex, TtWaitForever) == TtStatusOk);
}
static void service_data_unlock(ServiceData* data) {
tt_check(tt_mutex_release(data->mutex) == TtStatusOk);
}
static void on_start(Service& service) {
ServiceData* data = service_data_alloc();
service.setData(data);
}
static void on_stop(Service& service) {
auto* data = static_cast<ServiceData*>(service.getData());
if (data->task) {
task_free(data->task);
data->task = nullptr;
}
tt_mutex_free(data->mutex);
service_data_free(data);
}
void start_apps(const char* path) {
_Nullable auto* service = service_find(manifest.id);
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
return;
}
auto* data = static_cast<ServiceData*>(service->getData());
service_data_lock(data);
if (data->task == nullptr) {
data->task = task_alloc();
data->mode = ScreenshotModeApps;
task_start_apps(data->task, path);
} else {
TT_LOG_E(TAG, "Screenshot task already running");
}
service_data_unlock(data);
}
void start_timed(const char* path, uint8_t delay_in_seconds, uint8_t amount) {
_Nullable auto* service = service_find(manifest.id);
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
return;
}
auto* data = static_cast<ServiceData*>(service->getData());
service_data_lock(data);
if (data->task == nullptr) {
data->task = task_alloc();
data->mode = ScreenshotModeTimed;
task_start_timed(data->task, path, delay_in_seconds, amount);
} else {
TT_LOG_E(TAG, "Screenshot task already running");
}
service_data_unlock(data);
}
void stop() {
_Nullable Service* service = service_find(manifest.id);
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
return;
}
auto data = static_cast<ServiceData*>(service->getData());
service_data_lock(data);
if (data->task != nullptr) {
task_stop(data->task);
task_free(data->task);
data->task = nullptr;
data->mode = ScreenshotModeNone;
} else {
TT_LOG_E(TAG, "Screenshot task not running");
}
service_data_unlock(data);
}
ScreenshotMode get_mode() {
_Nullable auto* service = service_find(manifest.id);
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
return ScreenshotModeNone;
} else {
auto* data = static_cast<ServiceData*>(service->getData());
service_data_lock(data);
ScreenshotMode mode = data->mode;
service_data_unlock(data);
return mode;
}
}
bool is_started() {
return get_mode() != ScreenshotModeNone;
}
extern const ServiceManifest manifest = {
.id = "Screenshot",
.on_start = &on_start,
.on_stop = &on_stop
};
} // namespace
@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
namespace tt::service::screenshot {
typedef enum {
ScreenshotModeNone,
ScreenshotModeTimed,
ScreenshotModeApps
} ScreenshotMode;
/** @brief Starts taking screenshot with a timer
* @param path the path to store the screenshots in
* @param delay_in_seconds the delay before starting (and between successive screenshots)
* @param amount 0 = indefinite, >0 for a specific
*/
void start_timed(const char* path, uint8_t delay_in_seconds, uint8_t amount);
/** @brief Starts taking screenshot when an app is started
* @param path the path to store the screenshots in
*/
void start_apps(const char* path);
void stop();
ScreenshotMode get_mode();
bool is_started();
} // namespace
@@ -0,0 +1,187 @@
#include "ScreenshotTask.h"
#include "lv_screenshot.h"
#include "App.h"
#include "Mutex.h"
#include "TactilityCore.h"
#include "Thread.h"
#include "Services/Loader/Loader.h"
#include "Ui/LvglSync.h"
namespace tt::service::screenshot {
#define TAG "screenshot_task"
#define TASK_WORK_TYPE_DELAY 1
#define TASK_WORK_TYPE_APPS 2
#define SCREENSHOT_PATH_LIMIT 128
typedef struct {
int type;
uint8_t delay_in_seconds;
uint8_t amount;
char path[SCREENSHOT_PATH_LIMIT];
} ScreenshotTaskWork;
typedef struct {
Thread* thread;
Mutex* mutex;
bool interrupted;
ScreenshotTaskWork work;
} ScreenshotTaskData;
static void task_lock(ScreenshotTaskData* data) {
tt_check(tt_mutex_acquire(data->mutex, TtWaitForever) == TtStatusOk);
}
static void task_unlock(ScreenshotTaskData* data) {
tt_check(tt_mutex_release(data->mutex) == TtStatusOk);
}
ScreenshotTask* task_alloc() {
auto* data = static_cast<ScreenshotTaskData*>(malloc(sizeof(ScreenshotTaskData)));
*data = (ScreenshotTaskData) {
.thread = nullptr,
.mutex = tt_mutex_alloc(MutexTypeRecursive),
.interrupted = false
};
return data;
}
void task_free(ScreenshotTask* task) {
auto* data = static_cast<ScreenshotTaskData*>(task);
if (data->thread) {
task_stop(data);
}
}
static bool is_interrupted(ScreenshotTaskData* data) {
task_lock(data);
bool interrupted = data->interrupted;
task_unlock(data);
return interrupted;
}
static int32_t screenshot_task(void* context) {
auto* data = static_cast<ScreenshotTaskData*>(context);
bool interrupted = false;
uint8_t screenshots_taken = 0;
std::string last_app_id;
while (!interrupted) {
interrupted = is_interrupted(data);
if (data->work.type == TASK_WORK_TYPE_DELAY) {
// Splitting up the delays makes it easier to stop the service
for (int i = 0; i < (data->work.delay_in_seconds * 10) && !is_interrupted(data); ++i){
delay_ms(100);
}
if (is_interrupted(data)) {
break;
}
screenshots_taken++;
char filename[SCREENSHOT_PATH_LIMIT + 32];
sprintf(filename, "%s/screenshot-%d.png", data->work.path, screenshots_taken);
lvgl::lock(TtWaitForever);
if (lv_screenshot_create(lv_scr_act(), LV_COLOR_FORMAT_NATIVE, LV_100ASK_SCREENSHOT_SV_PNG, filename)){
TT_LOG_I(TAG, "Screenshot saved to %s", filename);
} else {
TT_LOG_E(TAG, "Screenshot not saved to %s", filename);
}
lvgl::unlock();
if (data->work.amount > 0 && screenshots_taken >= data->work.amount) {
break; // Interrupted loop
}
} else if (data->work.type == TASK_WORK_TYPE_APPS) {
App _Nullable app = loader::get_current_app();
if (app) {
const AppManifest& manifest = tt_app_get_manifest(app);
if (manifest.id != last_app_id) {
delay_ms(100);
last_app_id = manifest.id;
char filename[SCREENSHOT_PATH_LIMIT + 32];
sprintf(filename, "%s/screenshot-%s.png", data->work.path, manifest.id.c_str());
lvgl::lock(TtWaitForever);
if (lv_screenshot_create(lv_scr_act(), LV_COLOR_FORMAT_NATIVE, LV_100ASK_SCREENSHOT_SV_PNG, filename)){
TT_LOG_I(TAG, "Screenshot saved to %s", filename);
} else {
TT_LOG_E(TAG, "Screenshot not saved to %s", filename);
}
lvgl::unlock();
}
}
delay_ms(250);
}
}
return 0;
}
static void task_start(ScreenshotTaskData* data) {
task_lock(data);
tt_check(data->thread == NULL);
data->thread = thread_alloc_ex(
"screenshot",
8192,
&screenshot_task,
data
);
thread_start(data->thread);
task_unlock(data);
}
void task_start_apps(ScreenshotTask* task, const char* path) {
tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1));
auto* data = static_cast<ScreenshotTaskData*>(task);
task_lock(data);
if (data->thread == nullptr) {
data->interrupted = false;
data->work.type = TASK_WORK_TYPE_APPS;
strcpy(data->work.path, path);
task_start(data);
} else {
TT_LOG_E(TAG, "Task was already running");
}
task_unlock(data);
}
void task_start_timed(ScreenshotTask* task, const char* path, uint8_t delay_in_seconds, uint8_t amount) {
tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1));
auto* data = static_cast<ScreenshotTaskData*>(task);
task_lock(data);
if (data->thread == nullptr) {
data->interrupted = false;
data->work.type = TASK_WORK_TYPE_DELAY;
data->work.delay_in_seconds = delay_in_seconds;
data->work.amount = amount;
strcpy(data->work.path, path);
task_start(data);
} else {
TT_LOG_E(TAG, "Task was already running");
}
task_unlock(data);
}
void task_stop(ScreenshotTask* task) {
auto* data = static_cast<ScreenshotTaskData*>(task);
if (data->thread != nullptr) {
task_lock(data);
data->interrupted = true;
task_unlock(data);
thread_join(data->thread);
task_lock(data);
thread_free(data->thread);
data->thread = nullptr;
task_unlock(data);
}
}
} // namespace
@@ -0,0 +1,32 @@
#pragma once
#include <cstdint>
namespace tt::service::screenshot {
typedef void ScreenshotTask;
ScreenshotTask* task_alloc();
void task_free(ScreenshotTask* task);
/** @brief Start taking screenshots after a certain delay
* @param task the screenshot task
* @param path the path to store the screenshots at
* @param delay_in_seconds the delay before starting (and between successive screenshots)
* @param amount 0 = indefinite, >0 for a specific
*/
void task_start_timed(ScreenshotTask* task, const char* path, uint8_t delay_in_seconds, uint8_t amount);
/** @brief Start taking screenshot whenever an app is started
* @param task the screenshot task
* @param path the path to store the screenshots at
*/
void task_start_apps(ScreenshotTask* task, const char* path);
/** @brief Stop taking screenshots
* @param task the screenshot task
*/
void task_stop(ScreenshotTask* task);
}
@@ -0,0 +1,219 @@
#include "Assets.h"
#include "Hal/Power.h"
#include "Hal/Sdcard.h"
#include "Mutex.h"
#include "Service.h"
#include "Services/Wifi/Wifi.h"
#include "Tactility.h"
#include "Ui/Statusbar.h"
namespace tt::service::statusbar {
#define TAG "statusbar_service"
typedef struct {
Mutex* mutex;
Thread* thread;
bool service_interrupted;
int8_t wifi_icon_id;
const char* wifi_last_icon;
int8_t sdcard_icon_id;
const char* sdcard_last_icon;
int8_t power_icon_id;
const char* power_last_icon;
} ServiceData;
// region wifi
const char* get_status_icon_for_rssi(int rssi, bool secured) {
if (rssi > 0) {
return TT_ASSETS_ICON_WIFI_CONNECTION_ISSUE;
} else if (rssi >= -30) {
return secured ? TT_ASSETS_ICON_WIFI_SIGNAL_4_LOCKED : TT_ASSETS_ICON_WIFI_SIGNAL_4;
} else if (rssi >= -67) {
return secured ? TT_ASSETS_ICON_WIFI_SIGNAL_3_LOCKED : TT_ASSETS_ICON_WIFI_SIGNAL_3;
} else if (rssi >= -70) {
return secured ? TT_ASSETS_ICON_WIFI_SIGNAL_2_LOCKED : TT_ASSETS_ICON_WIFI_SIGNAL_2;
} else if (rssi >= -80) {
return secured ? TT_ASSETS_ICON_WIFI_SIGNAL_1_LOCKED : TT_ASSETS_ICON_WIFI_SIGNAL_1;
} else {
return secured ? TT_ASSETS_ICON_WIFI_SIGNAL_0_LOCKED : TT_ASSETS_ICON_WIFI_SIGNAL_0;
}
}
static const char* wifi_get_status_icon(wifi::WifiRadioState state, bool secure) {
int rssi;
switch (state) {
case wifi::WIFI_RADIO_ON_PENDING:
case wifi::WIFI_RADIO_ON:
case wifi::WIFI_RADIO_OFF_PENDING:
case wifi::WIFI_RADIO_OFF:
return TT_ASSETS_ICON_WIFI_OFF;
case wifi::WIFI_RADIO_CONNECTION_PENDING:
return TT_ASSETS_ICON_WIFI_FIND;
case wifi::WIFI_RADIO_CONNECTION_ACTIVE:
rssi = wifi::get_rssi();
return get_status_icon_for_rssi(rssi, secure);
default:
tt_crash("not implemented");
}
}
static void update_wifi_icon(ServiceData* data) {
wifi::WifiRadioState radio_state = wifi::get_radio_state();
bool is_secure = wifi::is_connection_secure();
const char* desired_icon = wifi_get_status_icon(radio_state, is_secure);
if (data->wifi_last_icon != desired_icon) {
lvgl::statusbar_icon_set_image(data->wifi_icon_id, desired_icon);
data->wifi_last_icon = desired_icon;
}
}
// endregion wifi
// region sdcard
static _Nullable const char* sdcard_get_status_icon(hal::sdcard::State state) {
switch (state) {
case hal::sdcard::StateMounted:
return TT_ASSETS_ICON_SDCARD;
case hal::sdcard::StateError:
case hal::sdcard::StateUnmounted:
return TT_ASSETS_ICON_SDCARD_ALERT;
default:
return nullptr;
}
}
static void update_sdcard_icon(ServiceData* data) {
hal::sdcard::State state = hal::sdcard::get_state();
const char* desired_icon = sdcard_get_status_icon(state);
if (data->sdcard_last_icon != desired_icon) {
lvgl::statusbar_icon_set_image(data->sdcard_icon_id, desired_icon);
lvgl::statusbar_icon_set_visibility(data->sdcard_icon_id, desired_icon != nullptr);
data->sdcard_last_icon = desired_icon;
}
}
// endregion sdcard
// region power
static _Nullable const char* power_get_status_icon() {
_Nullable const hal::Power* power = get_config()->hardware->power;
if (power != nullptr) {
uint8_t charge = power->get_charge_level();
if (charge >= 230) {
return TT_ASSETS_ICON_POWER_100;
} else if (charge >= 161) {
return TT_ASSETS_ICON_POWER_080;
} else if (charge >= 127) {
return TT_ASSETS_ICON_POWER_060;
} else if (charge >= 76) {
return TT_ASSETS_ICON_POWER_040;
} else {
return TT_ASSETS_ICON_POWER_020;
}
} else {
return nullptr;
}
}
static void update_power_icon(ServiceData* data) {
const char* desired_icon = power_get_status_icon();
if (data->power_last_icon != desired_icon) {
lvgl::statusbar_icon_set_image(data->power_icon_id, desired_icon);
lvgl::statusbar_icon_set_visibility(data->power_icon_id, desired_icon != nullptr);
data->power_last_icon = desired_icon;
}
}
// endregion power
// region service
static ServiceData* service_data_alloc() {
auto* data = static_cast<ServiceData*>(malloc(sizeof(ServiceData)));
*data = (ServiceData) {
.mutex = tt_mutex_alloc(MutexTypeNormal),
.thread = thread_alloc(),
.service_interrupted = false,
.wifi_icon_id = lvgl::statusbar_icon_add(nullptr),
.wifi_last_icon = nullptr,
.sdcard_icon_id = lvgl::statusbar_icon_add(nullptr),
.sdcard_last_icon = nullptr,
.power_icon_id = lvgl::statusbar_icon_add(nullptr),
.power_last_icon = nullptr
};
lvgl::statusbar_icon_set_visibility(data->wifi_icon_id, true);
update_wifi_icon(data);
update_sdcard_icon(data); // also updates visibility
update_power_icon(data);
return data;
}
static void service_data_free(ServiceData* data) {
tt_mutex_free(data->mutex);
thread_free(data->thread);
lvgl::statusbar_icon_remove(data->wifi_icon_id);
lvgl::statusbar_icon_remove(data->sdcard_icon_id);
lvgl::statusbar_icon_remove(data->power_icon_id);
free(data);
}
static void service_data_lock(ServiceData* data) {
tt_check(tt_mutex_acquire(data->mutex, TtWaitForever) == TtStatusOk);
}
static void service_data_unlock(ServiceData* data) {
tt_check(tt_mutex_release(data->mutex) == TtStatusOk);
}
int32_t service_main(TT_UNUSED void* parameter) {
TT_LOG_I(TAG, "Started main loop");
auto* data = (ServiceData*)parameter;
tt_assert(data != nullptr);
while (!data->service_interrupted) {
update_wifi_icon(data);
update_sdcard_icon(data);
update_power_icon(data);
delay_ms(1000);
}
return 0;
}
static void on_start(Service& service) {
ServiceData* data = service_data_alloc();
service.setData(data);
thread_set_callback(data->thread, service_main);
thread_set_current_priority(ThreadPriorityLow);
thread_set_stack_size(data->thread, 3000);
thread_set_context(data->thread, data);
thread_start(data->thread);
}
static void on_stop(Service& service) {
auto* data = static_cast<ServiceData*>(service.getData());
// Stop thread
service_data_lock(data);
data->service_interrupted = true;
service_data_unlock(data);
tt_mutex_release(data->mutex);
thread_join(data->thread);
service_data_free(data);
}
extern const ServiceManifest manifest = {
.id = "Statusbar",
.on_start = &on_start,
.on_stop = &on_stop
};
// endregion service
} // namespace
@@ -0,0 +1,13 @@
#pragma once
namespace tt::service::statusbar {
/**
* Return the relevant icon asset from assets.h for the given inputs
* @param rssi the rssi value
* @param secured whether the access point is a secured one (as in: not an open one)
* @return
*/
const char* get_status_icon_for_rssi(int rssi, bool secured);
} // namespace
+162
View File
@@ -0,0 +1,162 @@
#include "Tactility.h"
#include "AppManifestRegistry.h"
#include "LvglInit_i.h"
#include "ServiceRegistry.h"
#include "TactilityHeadless.h"
#include "Services/Loader/Loader.h"
namespace tt {
#define TAG "tactility"
static const Configuration* config_instance = NULL;
// region Default services
namespace service {
namespace gui { extern const ServiceManifest manifest; }
namespace loader { extern const ServiceManifest manifest; }
namespace screenshot { extern const ServiceManifest manifest; }
namespace statusbar { extern const ServiceManifest manifest; }
}
static const ServiceManifest* const system_services[] = {
&service::loader::manifest,
&service::gui::manifest, // depends on loader service
#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32
&service::screenshot::manifest,
#endif
&service::statusbar::manifest
};
// endregion
// region Default apps
namespace app {
namespace desktop { extern const AppManifest manifest; }
namespace files { extern const AppManifest manifest; }
namespace gpio { extern const AppManifest manifest; }
namespace image_viewer { extern const AppManifest manifest; }
namespace screenshot { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; }
namespace settings::display { extern const AppManifest manifest; }
namespace settings::power { extern const AppManifest manifest; }
namespace system_info { extern const AppManifest manifest; }
namespace text_viewer { extern const AppManifest manifest; }
namespace wifi_connect { extern const AppManifest manifest; }
namespace wifi_manage { extern const AppManifest manifest; }
}
#ifndef ESP_PLATFORM
extern const AppManifest screenshot_app;
#endif
static const AppManifest* const system_apps[] = {
&app::desktop::manifest,
&app::files::manifest,
&app::gpio::manifest,
&app::image_viewer::manifest,
&app::settings::manifest,
&app::settings::display::manifest,
&app::system_info::manifest,
&app::text_viewer::manifest,
&app::wifi_connect::manifest,
&app::wifi_manage::manifest,
#ifndef ESP_PLATFORM
&app::screenshot::manifest, // Screenshots don't work yet on ESP32
#endif
};
// endregion
static void register_system_apps() {
TT_LOG_I(TAG, "Registering default apps");
int app_count = sizeof(system_apps) / sizeof(AppManifest*);
for (int i = 0; i < app_count; ++i) {
app_manifest_registry_add(system_apps[i]);
}
if (get_config()->hardware->power != nullptr) {
app_manifest_registry_add(&app::settings::power::manifest);
}
}
static void register_user_apps(const AppManifest* const apps[TT_CONFIG_APPS_LIMIT]) {
TT_LOG_I(TAG, "Registering user apps");
for (size_t i = 0; i < TT_CONFIG_APPS_LIMIT; i++) {
const AppManifest* manifest = apps[i];
if (manifest != nullptr) {
app_manifest_registry_add(manifest);
} else {
// reached end of list
break;
}
}
}
static void register_and_start_system_services() {
TT_LOG_I(TAG, "Registering and starting system services");
int app_count = sizeof(system_services) / sizeof(ServiceManifest*);
for (int i = 0; i < app_count; ++i) {
service_registry_add(system_services[i]);
tt_check(service_registry_start(system_services[i]->id));
}
}
static void register_and_start_user_services(const ServiceManifest* const services[TT_CONFIG_SERVICES_LIMIT]) {
TT_LOG_I(TAG, "Registering and starting user services");
for (size_t i = 0; i < TT_CONFIG_SERVICES_LIMIT; i++) {
const ServiceManifest* manifest = services[i];
if (manifest != nullptr) {
service_registry_add(manifest);
tt_check(service_registry_start(manifest->id));
} else {
// reached end of list
break;
}
}
}
void init(const Configuration* config) {
TT_LOG_I(TAG, "init started");
// Assign early so starting services can use it
config_instance = config;
headless_init(config->hardware);
lvgl_init(config->hardware);
app_manifest_registry_init();
// Note: the order of starting apps and services is critical!
// System services are registered first so the apps below can find them if needed
register_and_start_system_services();
// Then we register system apps. They are not used/started yet.
register_system_apps();
// Then we register and start user services. They are started after system app
// registration just in case they want to figure out which system apps are installed.
register_and_start_user_services(config->services);
// Now we register the user apps, as they might rely on the user services.
register_user_apps(config->apps);
TT_LOG_I(TAG, "init starting desktop app");
service::loader::start_app(app::desktop::manifest.id, true, Bundle());
if (config->auto_start_app_id) {
TT_LOG_I(TAG, "init auto-starting %s", config->auto_start_app_id);
service::loader::start_app(config->auto_start_app_id, true, Bundle());
}
TT_LOG_I(TAG, "init complete");
}
const Configuration* _Nullable get_config() {
return config_instance;
}
} // namespace
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "AppManifest.h"
#include "Hal/Configuration.h"
#include "ServiceManifest.h"
#include "TactilityConfig.h"
namespace tt {
typedef struct {
const hal::Configuration* hardware;
// List of user applications
const AppManifest* const apps[TT_CONFIG_APPS_LIMIT];
const ServiceManifest* const services[TT_CONFIG_SERVICES_LIMIT];
const char* auto_start_app_id;
} Configuration;
/**
* Attempts to initialize Tactility and all configured hardware.
* @param config
*/
void init(const Configuration* config);
/**
* While technically nullable, this instance is always set if tt_init() succeeds.
* @return the Configuration instance that was passed on to tt_init() if init is successful
*/
const Configuration* _Nullable get_config();
} // namespace
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "TactilityHeadlessConfig.h"
#define TT_CONFIG_APPS_LIMIT 32
#define TT_CONFIG_FORCE_ONSCREEN_KEYBOARD false
+65
View File
@@ -0,0 +1,65 @@
#include "LabelUtils.h"
#include "TactilityCore.h"
namespace tt::lvgl {
#define TAG "tt_lv_label"
static long file_get_size(FILE* file) {
long original_offset = ftell(file);
if (fseek(file, 0, SEEK_END) != 0) {
TT_LOG_E(TAG, "fseek failed");
return -1;
}
long file_size = ftell(file);
if (file_size == -1) {
TT_LOG_E(TAG, "Could not get file length");
return -1;
}
if (fseek(file, original_offset, SEEK_SET) != 0) {
TT_LOG_E(TAG, "fseek Failed");
return -1;
}
return file_size;
}
static char* str_alloc_from_file(const char* filepath) {
FILE* file = fopen(filepath, "rb");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", filepath);
return nullptr;
}
long content_length = file_get_size(file);
auto* text_buffer = static_cast<char*>(malloc(content_length + 1));
if (text_buffer == nullptr) {
TT_LOG_E(TAG, "Insufficient memory. Failed to allocate %ldl bytes.", content_length);
return nullptr;
}
int buffer;
uint32_t buffer_offset = 0;
text_buffer[0] = 0;
while ((buffer = fgetc(file)) != EOF && buffer_offset < content_length) {
text_buffer[buffer_offset] = (char)buffer;
buffer_offset++;
}
text_buffer[buffer_offset] = 0;
fclose(file);
return text_buffer;
}
void label_set_text_file(lv_obj_t* label, const char* filepath) {
char* text = str_alloc_from_file(filepath);
lv_label_set_text(label, text);
free(text);
}
} // namespace
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "lvgl.h"
namespace tt::lvgl {
void label_set_text_file(lv_obj_t* label, const char* filepath);
} // namespace
+27
View File
@@ -0,0 +1,27 @@
#include "LvglKeypad.h"
namespace tt::lvgl {
static lv_indev_t* keyboard_device = NULL;
bool keypad_is_available() {
return keyboard_device != NULL;
}
void keypad_set_indev(lv_indev_t* device) {
keyboard_device = device;
}
void keypad_activate(lv_group_t* group) {
if (keyboard_device != NULL) {
lv_indev_set_group(keyboard_device, group);
}
}
void keypad_deactivate() {
if (keyboard_device != NULL) {
lv_indev_set_group(keyboard_device, NULL);
}
}
} // namespace
+34
View File
@@ -0,0 +1,34 @@
/**
* This code relates to the hardware keyboard support also known as "keypads" in LVGL.
*/
#pragma once
#include "lvgl.h"
namespace tt::lvgl {
/**
* @return true if LVGL is configured with a keypad
*/
bool keypad_is_available();
/**
* Set the keypad.
* @param device the keypad device
*/
void keypad_set_indev(lv_indev_t* device);
/**
* Activate the keypad for a widget group.
* @param group
*/
void keypad_activate(lv_group_t* group);
/**
* Deactivate the keypad for the current widget group (if any).
* You don't have to call this after calling _activate() because widget
* cleanup automatically removes itself from the group it belongs to.
*/
void keypad_deactivate();
} // namespace
+27
View File
@@ -0,0 +1,27 @@
#include "LvglSync.h"
namespace tt::lvgl {
static LvglLock lock_singleton = nullptr;
static LvglUnlock unlock_singleton = nullptr;
void sync_set(LvglLock lock, LvglUnlock unlock) {
lock_singleton = lock;
unlock_singleton = unlock;
}
bool lock(uint32_t timeout_ticks) {
if (lock_singleton) {
return lock_singleton(timeout_ticks);
} else {
return true;
}
}
void unlock() {
if (unlock_singleton) {
unlock_singleton();
}
}
} // namespace
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstdio>
#include <cstdint>
namespace tt::lvgl {
typedef bool (*LvglLock)(uint32_t timeout_ticks);
typedef void (*LvglUnlock)();
void sync_set(LvglLock lock, LvglUnlock unlock);
bool lock(uint32_t timeout_ticks);
void unlock();
} // namespace
+13
View File
@@ -0,0 +1,13 @@
#include "Spacer.h"
#include "Style.h"
namespace tt::lvgl {
lv_obj_t* spacer_create(lv_obj_t* parent, int32_t width, int32_t height) {
lv_obj_t* spacer = lv_obj_create(parent);
lv_obj_set_size(spacer, width, height);
obj_set_style_bg_invisible(spacer);
return spacer;
}
} // namespace
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "lvgl.h"
namespace tt::lvgl {
lv_obj_t* spacer_create(lv_obj_t* parent, int32_t width, int32_t height);
} // namespace
+226
View File
@@ -0,0 +1,226 @@
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
#include "Statusbar.h"
#include "Mutex.h"
#include "Pubsub.h"
#include "TactilityCore.h"
#include "Ui/Spacer.h"
#include "Ui/Style.h"
#include "LvglSync.h"
#include "lvgl.h"
namespace tt::lvgl {
#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 = nullptr,
.icons = {}
};
typedef struct {
lv_obj_t obj;
lv_obj_t* icons[STATUSBAR_ICON_LIMIT];
lv_obj_t* battery_icon;
PubSubSubscription* pubsub_subscription;
} Statusbar;
static void statusbar_init() {
statusbar_data.mutex = tt_mutex_alloc(MutexTypeRecursive);
statusbar_data.pubsub = tt_pubsub_alloc();
for (int i = 0; i < STATUSBAR_ICON_LIMIT; i++) {
statusbar_data.icons[i].image = nullptr;
statusbar_data.icons[i].visible = false;
statusbar_data.icons[i].claimed = false;
}
}
static void statusbar_ensure_initialized() {
if (statusbar_data.mutex == nullptr) {
statusbar_init();
}
}
static void statusbar_lock() {
statusbar_ensure_initialized();
tt_mutex_acquire(statusbar_data.mutex, TtWaitForever);
}
static 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 void update_main(Statusbar* statusbar);
static const lv_obj_class_t statusbar_class = {
.base_class = &lv_obj_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)
};
static void statusbar_pubsub_event(TT_UNUSED const void* message, void* obj) {
TT_LOG_I(TAG, "event");
auto* statusbar = static_cast<Statusbar*>(obj);
if (lock(ms_to_ticks(100))) {
update_main(statusbar);
lv_obj_invalidate(&statusbar->obj);
unlock();
}
}
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_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
LV_TRACE_OBJ_CREATE("finished");
auto* statusbar = (Statusbar*)obj;
statusbar_ensure_initialized();
statusbar->pubsub_subscription = tt_pubsub_subscribe(statusbar_data.pubsub, &statusbar_pubsub_event, statusbar);
}
static void statusbar_destructor(TT_UNUSED const lv_obj_class_t* class_p, lv_obj_t* obj) {
auto* 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 != nullptr && icon->visible && icon->claimed) {
lv_obj_set_style_image_recolor(image, lv_color_white(), 0);
lv_obj_set_style_image_recolor_opa(image, 255, 0);
lv_image_set_src(image, icon->image);
lv_obj_remove_flag(image, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(image, LV_OBJ_FLAG_HIDDEN);
}
}
lv_obj_t* 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);
auto* statusbar = (Statusbar*)obj;
lv_obj_set_width(obj, LV_PCT(100));
lv_obj_set_height(obj, STATUSBAR_HEIGHT);
obj_set_style_no_padding(obj);
lv_obj_center(obj);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
lv_obj_t* left_spacer = 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_image_create(obj);
lv_obj_set_size(image, STATUSBAR_ICON_SIZE, STATUSBAR_ICON_SIZE);
obj_set_style_no_padding(image);
obj_set_style_bg_blacken(image);
statusbar->icons[i] = image;
update_icon(image, &(statusbar_data.icons[i]));
}
statusbar_unlock();
return obj;
}
static void update_main(Statusbar* statusbar) {
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_result_t result = lv_obj_event_base(&statusbar_class, event);
if (result != LV_RES_OK) {
return;
}
lv_event_code_t code = lv_event_get_code(event);
auto* obj = static_cast<lv_obj_t*>(lv_event_get_target(event));
if (code == LV_EVENT_VALUE_CHANGED) {
lv_obj_invalidate(obj);
} else if (code == LV_EVENT_DRAW_MAIN) {
}
}
int8_t statusbar_icon_add(const char* _Nullable 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 = (image != nullptr);
statusbar_data.icons[i].image = image;
result = i;
TT_LOG_I(TAG, "id %d: added", i);
break;
}
}
tt_pubsub_publish(statusbar_data.pubsub, nullptr);
statusbar_unlock();
return result;
}
void 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 = nullptr;
tt_pubsub_publish(statusbar_data.pubsub, nullptr);
statusbar_unlock();
}
void 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, nullptr);
statusbar_unlock();
}
void 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, nullptr);
statusbar_unlock();
}
} // namespace
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "lvgl.h"
#include "App.h"
namespace tt::lvgl {
#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* statusbar_create(lv_obj_t* parent);
int8_t statusbar_icon_add(const char* _Nullable image);
void statusbar_icon_remove(int8_t id);
void statusbar_icon_set_image(int8_t id, const char* image);
void statusbar_icon_set_visibility(int8_t id, bool visible);
} // namespace
+27
View File
@@ -0,0 +1,27 @@
#include "Style.h"
namespace tt::lvgl {
void obj_set_style_bg_blacken(lv_obj_t* obj) {
lv_obj_set_style_bg_color(obj, lv_color_black(), 0);
lv_obj_set_style_border_color(obj, lv_color_black(), 0);
}
void obj_set_style_bg_invisible(lv_obj_t* obj) {
lv_obj_set_style_bg_opa(obj, 0, 0);
lv_obj_set_style_border_width(obj, 0, 0);
}
void obj_set_style_no_padding(lv_obj_t* obj) {
lv_obj_set_style_pad_all(obj, LV_STATE_DEFAULT, 0);
lv_obj_set_style_pad_gap(obj, LV_STATE_DEFAULT, 0);
}
void obj_set_style_auto_padding(lv_obj_t* obj) {
lv_obj_set_style_pad_top(obj, 8, 0);
lv_obj_set_style_pad_bottom(obj, 8, 0);
lv_obj_set_style_pad_left(obj, 16, 0);
lv_obj_set_style_pad_right(obj, 16, 0);
}
} // namespace
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "lvgl.h"
namespace tt::lvgl {
void obj_set_style_bg_blacken(lv_obj_t* obj);
void obj_set_style_bg_invisible(lv_obj_t* obj);
void obj_set_style_no_padding(lv_obj_t* obj);
/**
* This is to create automatic padding depending on the screen size.
* The larger the screen, the more padding it gets.
* TODO: It currently only applies a single basic padding, but will be improved later.
*
* @param obj
*/
void obj_set_style_auto_padding(lv_obj_t* obj);
} // namespace
+123
View File
@@ -0,0 +1,123 @@
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
#include "Toolbar.h"
#include "Services/Loader/Loader.h"
#include "Ui/Spacer.h"
#include "Ui/Style.h"
namespace tt::lvgl {
typedef struct {
lv_obj_t obj;
lv_obj_t* title_label;
lv_obj_t* close_button;
lv_obj_t* close_button_image;
lv_obj_t* action_container;
ToolbarAction* action_array[TOOLBAR_ACTION_LIMIT];
uint8_t action_count;
} Toolbar;
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
static const lv_obj_class_t toolbar_class = {
.base_class = &lv_obj_class,
.constructor_cb = &toolbar_constructor,
.destructor_cb = nullptr,
.width_def = LV_PCT(100),
.height_def = TOOLBAR_HEIGHT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
.instance_size = sizeof(Toolbar),
};
static void stop_app(TT_UNUSED lv_event_t* event) {
service::loader::stop_app();
}
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
LV_TRACE_OBJ_CREATE("finished");
}
lv_obj_t* toolbar_create(lv_obj_t* parent, const std::string& title) {
LV_LOG_INFO("begin");
lv_obj_t* obj = lv_obj_class_create_obj(&toolbar_class, parent);
lv_obj_class_init_obj(obj);
auto* toolbar = (Toolbar*)obj;
obj_set_style_no_padding(obj);
lv_obj_center(obj);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
int32_t title_offset_x = (TOOLBAR_HEIGHT - TOOLBAR_TITLE_FONT_HEIGHT - 8) / 4 * 3;
int32_t title_offset_y = (TOOLBAR_HEIGHT - TOOLBAR_TITLE_FONT_HEIGHT - 8) / 2;
toolbar->close_button = lv_button_create(obj);
lv_obj_set_size(toolbar->close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
obj_set_style_no_padding(toolbar->close_button);
toolbar->close_button_image = lv_image_create(toolbar->close_button);
lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0);
// Need spacer to avoid button press glitch animation
spacer_create(obj, title_offset_x, 1);
lv_obj_t* label_container = lv_obj_create(obj);
obj_set_style_no_padding(label_container);
lv_obj_set_style_border_width(label_container, 0, 0);
lv_obj_set_height(label_container, LV_PCT(100)); // 2% less due to 4px translate (it's not great, but it works)
lv_obj_set_flex_grow(label_container, 1);
toolbar->title_label = lv_label_create(label_container);
lv_obj_set_style_text_font(toolbar->title_label, &lv_font_montserrat_18, 0); // TODO replace with size 18
lv_obj_set_height(toolbar->title_label, TOOLBAR_TITLE_FONT_HEIGHT);
lv_label_set_text(toolbar->title_label, title.c_str());
lv_obj_set_pos(toolbar->title_label, 0, title_offset_y);
lv_obj_set_style_text_align(toolbar->title_label, LV_TEXT_ALIGN_LEFT, 0);
toolbar->action_container = lv_obj_create(obj);
lv_obj_set_width(toolbar->action_container, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(toolbar->action_container, 0, 0);
lv_obj_set_style_border_width(toolbar->action_container, 0, 0);
return obj;
}
lv_obj_t* toolbar_create_for_app(lv_obj_t* parent, App app) {
const AppManifest& manifest = tt_app_get_manifest(app);
lv_obj_t* toolbar = toolbar_create(parent, manifest.name);
toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &stop_app, nullptr);
return toolbar;
}
void toolbar_set_title(lv_obj_t* obj, const std::string& title) {
auto* toolbar = (Toolbar*)obj;
lv_label_set_text(toolbar->title_label, title.c_str());
}
void toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) {
auto* toolbar = (Toolbar*)obj;
lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_CLICKED, user_data);
lv_image_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE
}
uint8_t toolbar_add_action(lv_obj_t* obj, const char* icon, const char* text, lv_event_cb_t callback, void* user_data) {
auto* toolbar = (Toolbar*)obj;
uint8_t id = toolbar->action_count;
tt_check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
lv_obj_t* action_button = lv_button_create(toolbar->action_container);
lv_obj_set_size(action_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
obj_set_style_no_padding(action_button);
lv_obj_add_event_cb(action_button, callback, LV_EVENT_CLICKED, user_data);
lv_obj_t* action_button_image = lv_image_create(action_button);
lv_image_set_src(action_button_image, icon);
lv_obj_align(action_button_image, LV_ALIGN_CENTER, 0, 0);
return id;
}
} // namespace
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "lvgl.h"
#include "App.h"
namespace tt::lvgl {
#define TOOLBAR_HEIGHT 40
#define TOOLBAR_ACTION_LIMIT 8
#define TOOLBAR_TITLE_FONT_HEIGHT 18
typedef void(*ToolbarActionCallback)(void* _Nullable context);
typedef struct {
const char* icon;
const char* text;
ToolbarActionCallback callback;
void* _Nullable callback_context;
} ToolbarAction;
lv_obj_t* toolbar_create(lv_obj_t* parent, const std::string& title);
lv_obj_t* toolbar_create_for_app(lv_obj_t* parent, tt::App app);
void toolbar_set_title(lv_obj_t* obj, const std::string& title);
void toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data);
uint8_t toolbar_add_action(lv_obj_t* obj, const char* icon, const char* text, lv_event_cb_t callback, void* user_data);
} // namespace