committed by
GitHub
parent
6d80144e12
commit
85e26636a3
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef ESP_PLATFORM
|
||||
|
||||
int gpio_get_level(int gpio_num) {
|
||||
return gpio_num % 3;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user