C++ refactoring (#91)

This commit is contained in:
Ken Van Hoeylandt
2024-11-26 17:51:05 +01:00
committed by GitHub
parent d06137ba76
commit ca5d4b8226
159 changed files with 904 additions and 1086 deletions
+102
View File
@@ -0,0 +1,102 @@
#include "app/App.h"
namespace tt::app {
#define TAG "app"
void AppInstance::setState(State newState) {
mutex.acquire(TtWaitForever);
state = newState;
mutex.release();
}
State 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 Manifest& AppInstance::getManifest() {
return manifest;
}
Flags AppInstance::getFlags() {
mutex.acquire(TtWaitForever);
auto result = flags;
mutex.release();
return result;
}
void AppInstance::setFlags(Flags 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 Manifest& 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, State state) {
static_cast<AppInstance*>(app)->setState(state);
}
State tt_app_get_state(App app) {
return static_cast<AppInstance*>(app)->getState();
}
const Manifest& tt_app_get_manifest(App app) {
return static_cast<AppInstance*>(app)->getManifest();
}
Flags tt_app_get_flags(App app) {
return static_cast<AppInstance*>(app)->getFlags();
}
void tt_app_set_flags(App app, Flags 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
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include "Manifest.h"
#include "Bundle.h"
#include "Mutex.h"
namespace tt::app {
typedef enum {
StateInitial, // App is being activated in loader
StateStarted, // App is in memory
StateShowing, // App view is created
StateHiding, // App view is destroyed
StateStopped // App is not in memory
} State;
typedef union {
struct {
bool show_statusbar : 1;
};
unsigned char flags;
} Flags;
class AppInstance {
private:
Mutex mutex = Mutex(MutexTypeNormal);
const Manifest& manifest;
State state = StateInitial;
Flags 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 Manifest& manifest) :
manifest(manifest) {}
AppInstance(const Manifest& manifest, const Bundle& parameters) :
manifest(manifest),
parameters(parameters) {}
void setState(State state);
State getState();
const Manifest& getManifest();
Flags getFlags();
void setFlags(Flags 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 Manifest& manifest, const Bundle& parameters);
[[deprecated("use class")]]
void tt_app_free(App app);
[[deprecated("use class")]]
void tt_app_set_state(App app, State state);
[[deprecated("use class")]]
State tt_app_get_state(App app);
[[deprecated("use class")]]
const Manifest& tt_app_get_manifest(App app);
[[deprecated("use class")]]
Flags tt_app_get_flags(App app);
[[deprecated("use class")]]
void tt_app_set_flags(App app, Flags 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
+56
View File
@@ -0,0 +1,56 @@
#include "app/ManifestRegistry.h"
#include "Assets.h"
#include "Check.h"
#include "lvgl.h"
#include <algorithm>
#include "service/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 Manifest*>(lv_event_get_user_data(e));
service::loader::start_app(manifest->id, false, Bundle());
}
}
static void create_app_widget(const Manifest* 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);
auto manifests = app_manifest_registry_get();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
lv_list_add_text(list, "User");
for (const auto& manifest: manifests) {
if (manifest->type == TypeUser) {
create_app_widget(manifest, list);
}
}
lv_list_add_text(list, "System");
for (const auto& manifest: manifests) {
if (manifest->type == TypeSystem) {
create_app_widget(manifest, list);
}
}
}
extern const Manifest manifest = {
.id = "Desktop",
.name = "Desktop",
.type = TypeDesktop,
.on_show = &desktop_show,
};
} // namespace
+136
View File
@@ -0,0 +1,136 @@
#include "app/App.h"
#include "Assets.h"
#include "DisplayPreferences.h"
#include "Tactility.h"
#include "ui/Style.h"
#include "ui/Toolbar.h"
#include "lvgl.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 = getConfiguration();
hal::SetBacklightDuty set_backlight_duty = config->hardware->display.setBacklightDuty;
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(parent, app);
lv_obj_t* main_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
lv_obj_t* wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_pad_all(wrapper, 8, 0);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_t* brightness_label = lv_label_create(wrapper);
lv_label_set_text(brightness_label, "Brightness");
lv_obj_t* brightness_slider = lv_slider_create(wrapper);
lv_obj_set_width(brightness_slider, LV_PCT(50));
lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
const Configuration* config = getConfiguration();
hal::SetBacklightDuty set_backlight_duty = config->hardware->display.setBacklightDuty;
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_label = lv_label_create(wrapper);
lv_label_set_text(orientation_label, "Orientation");
lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 40);
lv_obj_t* orientation_dropdown = lv_dropdown_create(wrapper);
lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right");
lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 32);
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 Manifest manifest = {
.id = "Display",
.name = "Display",
.icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS,
.type = TypeSettings,
.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 "Tactility.h"
#include "app/App.h"
#include "Assets.h"
#include "Check.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include "app/ImageViewer/ImageViewer.h"
#include "app/TextViewer/TextViewer.h"
#include "lvgl.h"
#include "service/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 Manifest manifest = {
.id = "Files",
.name = "Files",
.icon = TT_ASSETS_APP_ICON_FILES,
.type = TypeSystem,
.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
+214
View File
@@ -0,0 +1,214 @@
#include "Mutex.h"
#include "Thread.h"
#include "Tactility.h"
#include "service/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 = new Thread(
"gpio",
4096,
&gpio_task,
gpio
);
gpio->thread_interrupted = false;
gpio->thread->start();
unlock(gpio);
}
static void task_stop(Gpio* gpio) {
tt_assert(gpio->thread);
lock(gpio);
gpio->thread_interrupted = true;
unlock(gpio);
gpio->thread->join();
lock(gpio);
delete 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(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 Manifest manifest = {
.id = "Gpio",
.name = "GPIO",
.type = TypeSystem,
.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,96 @@
#include "Assets.h"
#include "hal/i2c/I2c.h"
#include "Tactility.h"
#include "ui/Style.h"
#include "ui/Toolbar.h"
#include "lvgl.h"
namespace tt::app::settings::i2c {
static void on_switch_toggle(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event);
auto* state_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
const hal::i2c::Configuration* configuration = static_cast<hal::i2c::Configuration*>(lv_event_get_user_data(event));
if (code == LV_EVENT_VALUE_CHANGED) {
bool should_enable = lv_obj_get_state(state_switch) & LV_STATE_CHECKED;
bool is_enabled = hal::i2c::isStarted(configuration->port);
if (is_enabled && !should_enable) {
if (!hal::i2c::stop(configuration->port)) {
lv_obj_add_state(state_switch, LV_STATE_CHECKED);
}
} else if (!is_enabled && should_enable) {
if (!hal::i2c::start(configuration->port)) {
lv_obj_remove_state(state_switch, LV_STATE_CHECKED);
}
}
}
}
static void show(lv_obj_t* parent, const hal::i2c::Configuration& configuration) {
lv_obj_t* card = lv_obj_create(parent);
lv_obj_set_height(card, LV_SIZE_CONTENT);
lv_obj_set_style_margin_hor(card, 16, 0);
lv_obj_set_style_margin_bottom(card, 16, 0);
lv_obj_set_width(card, LV_PCT(100));
lv_obj_t* port_label = lv_label_create(card);
const char* name = configuration.name.empty() ? "Unnamed" : configuration.name.c_str();
lv_label_set_text_fmt(port_label, "%s (port %d)", name, configuration.port);
// On-off switch
if (configuration.canReinit) {
lv_obj_t* state_switch = lv_switch_create(card);
lv_obj_align(state_switch, LV_ALIGN_TOP_RIGHT, 0, 0);
if (hal::i2c::isStarted(configuration.port)) {
lv_obj_add_state(state_switch, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(state_switch, on_switch_toggle, LV_EVENT_VALUE_CHANGED, (void*)&configuration);
}
// SDA label
lv_obj_t* sda_pin_label = lv_label_create(card);
const char* sda_pullup_text = configuration.config.sda_pullup_en ? "yes" : "no";
lv_label_set_text_fmt(sda_pin_label, "SDA: pin %d, pullup: %s", configuration.config.sda_io_num, sda_pullup_text );
lv_obj_align_to(sda_pin_label, port_label, LV_ALIGN_OUT_BOTTOM_LEFT, 16, 8);
// SCL label
lv_obj_t* scl_pin_label = lv_label_create(card);
const char* scl_pullup_text = configuration.config.scl_pullup_en ? "yes" : "no";
lv_label_set_text_fmt(scl_pin_label, "SCL: pin %d, pullup: %s", configuration.config.scl_io_num, scl_pullup_text);
lv_obj_align_to(scl_pin_label, sda_pin_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
// Frequency:
if (configuration.config.mode == I2C_MODE_MASTER) {
lv_obj_t* frequency_label = lv_label_create(card);
lv_label_set_text_fmt(frequency_label, "Frequency: %lu Hz", configuration.config.master.clk_speed);
lv_obj_align_to(frequency_label, scl_pin_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
}
}
static void on_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(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);
for (const auto& configuration: getConfiguration()->hardware->i2c) {
show(wrapper, configuration);
}
}
extern const Manifest manifest = {
.id = "I2cSettings",
.name = "I2C",
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
.type = TypeSettings,
.on_show = &on_show
};
} // namespace
@@ -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(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 Manifest manifest = {
.id = "ImageViewer",
.name = "Image Viewer",
.type = TypeHidden,
.on_show = &on_show
};
} // namespace
@@ -0,0 +1,3 @@
#pragma once
#define IMAGE_VIEWER_FILE_ARGUMENT "file"
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <string>
#include "CoreDefines.h"
// Forward declarations
typedef struct _lv_obj_t lv_obj_t;
namespace tt::app {
typedef void* App;
typedef enum {
/** A desktop app sits at the root of the app stack managed by the Loader service */
TypeDesktop,
/** Apps that generally aren't started from the desktop (e.g. image viewer) */
TypeHidden,
/** Standard apps, provided by the system. */
TypeSystem,
/** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
TypeSettings,
/** User-provided apps. */
TypeUser
} Type;
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 Manifest {
/**
* 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 Type type = TypeUser;
/**
* 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;
struct {
bool operator()(const Manifest* a, const Manifest* b) const { return a->name < b->name; }
} SortAppManifestByName;
} // namespace
+55
View File
@@ -0,0 +1,55 @@
#include "ManifestRegistry.h"
#include "Mutex.h"
#include "TactilityCore.h"
#include <unordered_map>
#define TAG "app_registry"
namespace tt::app {
typedef std::unordered_map<std::string, const Manifest*> 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 Manifest* 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 Manifest * app_manifest_registry_find_by_id(const std::string& id) {
app_registry_lock();
auto iterator = app_manifest_map.find(id);
_Nullable const Manifest* result = iterator != app_manifest_map.end() ? iterator->second : nullptr;
app_registry_unlock();
return result;
}
std::vector<const Manifest*> app_manifest_registry_get() {
std::vector<const Manifest*> manifests;
app_registry_lock();
for (const auto& item: app_manifest_map) {
manifests.push_back(item.second);
}
app_registry_unlock();
return manifests;
}
} // namespace
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "Manifest.h"
#include <string>
#include <vector>
namespace tt::app {
void app_manifest_registry_init();
void app_manifest_registry_add(const Manifest* manifest);
void app_manifest_registry_remove(const Manifest* manifest);
const Manifest _Nullable* app_manifest_registry_find_by_id(const std::string& id);
std::vector<const Manifest*> app_manifest_registry_get();
} // namespace
+123
View File
@@ -0,0 +1,123 @@
#include "app/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(void* callbackContext) {
auto* data = (AppData*)callbackContext;
bool charging_enabled = data->power->isChargingEnabled();
const char* charge_state = data->power->isCharging() ? "yes" : "no";
uint8_t charge_level = data->power->getChargeLevel();
uint16_t charge_level_scaled = (int16_t)charge_level * 100 / 255;
int32_t current = data->power->getCurrent();
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);
auto* data = static_cast<AppData*>(lv_event_get_user_data(event));
if (data->power->isChargingEnabled() != is_on) {
data->power->setChargingEnabled(is_on);
app_update_ui(data);
}
}
}
static void app_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(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, data);
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(data);
data->update_timer->start(ms_to_ticks(1000));
}
static void app_hide(TT_UNUSED App app) {
auto* data = static_cast<AppData*>(tt_app_get_data(app));
data->update_timer->stop();
}
static void app_start(App app) {
auto* data = new AppData();
data->update_timer = new Timer(Timer::TypePeriodic, &app_update_ui, data);
data->power = getConfiguration()->hardware->power;
assert(data->power != nullptr); // The Power app only shows up on supported devices
tt_app_set_data(app, data);
}
static void app_stop(App app) {
auto* data = static_cast<AppData*>(tt_app_get_data(app));
delete data->update_timer;
delete data;
}
extern const Manifest manifest = {
.id = "Power",
.name = "Power",
.icon = TT_ASSETS_APP_ICON_POWER_SETTINGS,
.type = TypeSettings,
.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 Manifest manifest = {
.id = "Screenshot",
.name = "_Screenshot", // So it gets put at the bottom of the desktop and becomes less visible on small screen devices
.icon = LV_SYMBOL_IMAGE,
.type = TypeSystem,
.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/Sdcard.h"
#include "service/gui/Gui.h"
#include "service/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::getState() == 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(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/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,56 @@
#include "app/ManifestRegistry.h"
#include "Assets.h"
#include "Check.h"
#include "service/loader/Loader.h"
#include "ui/Toolbar.h"
#include "lvgl.h"
#include <algorithm>
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 Manifest*>(lv_event_get_user_data(e));
service::loader::start_app(manifest->id, false, Bundle());
}
}
static void create_app_widget(const Manifest* 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(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);
auto manifests = app_manifest_registry_get();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
for (const auto& manifest: manifests) {
if (manifest->type == TypeSettings) {
create_app_widget(manifest, list);
}
}
}
extern const Manifest manifest = {
.id = "Settings",
.name = "Settings",
.icon = TT_ASSETS_APP_ICON_SETTINGS,
.type = TypeSystem,
.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(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 Manifest manifest = {
.id = "SystemInfo",
.name = "System Info",
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
.type = TypeSystem,
.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(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 Manifest manifest = {
.id = "TextViewer",
.name = "Text Viewer",
.type = TypeHidden,
.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/App.h"
#include "TactilityCore.h"
#include "WifiConnectStateUpdating.h"
#include "service/loader/Loader.h"
#include "service/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 Manifest manifest = {
.id = "WifiConnect",
.name = "Wi-Fi Connect",
.icon = LV_SYMBOL_WIFI,
.type = TypeSettings,
.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 "service/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 "service/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,17 @@
#pragma once
#include "service/wifi/Wifi.h"
#include "service/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 "service/gui/Gui.h"
#include "service/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(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,27 @@
#pragma once
#include "app/App.h"
#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/App.h"
#include "app/WifiConnect/WifiConnectBundle.h"
#include "TactilityCore.h"
#include "service/loader/Loader.h"
#include "service/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("WifiConnect", 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 Manifest manifest = {
.id = "WifiManage",
.name = "Wi-Fi",
.icon = LV_SYMBOL_WIFI,
.type = TypeSettings,
.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 "service/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 "service/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 "service/statusbar/Statusbar.h"
#include "service/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(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/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