C++ refactoring (#91)
This commit is contained in:
committed by
GitHub
parent
d06137ba76
commit
ca5d4b8226
@@ -0,0 +1,102 @@
|
||||
#include "app/App.h"
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
#define TAG "app"
|
||||
|
||||
void AppInstance::setState(State newState) {
|
||||
mutex.acquire(TtWaitForever);
|
||||
state = newState;
|
||||
mutex.release();
|
||||
}
|
||||
|
||||
State AppInstance::getState() {
|
||||
mutex.acquire(TtWaitForever);
|
||||
auto result = state;
|
||||
mutex.release();
|
||||
return result;
|
||||
}
|
||||
|
||||
/** TODO: Make this thread-safe.
|
||||
* In practice, the bundle is writeable, so someone could be writing to it
|
||||
* while it is being accessed from another thread.
|
||||
* Consider creating MutableBundle vs Bundle.
|
||||
* Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it.
|
||||
*/
|
||||
const Manifest& AppInstance::getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
Flags AppInstance::getFlags() {
|
||||
mutex.acquire(TtWaitForever);
|
||||
auto result = flags;
|
||||
mutex.release();
|
||||
return result;
|
||||
}
|
||||
|
||||
void AppInstance::setFlags(Flags newFlags) {
|
||||
mutex.acquire(TtWaitForever);
|
||||
flags = newFlags;
|
||||
mutex.release();
|
||||
}
|
||||
|
||||
_Nullable void* AppInstance::getData() {
|
||||
mutex.acquire(TtWaitForever);
|
||||
auto result = data;
|
||||
mutex.release();
|
||||
return result;
|
||||
}
|
||||
|
||||
void AppInstance::setData(void* newData) {
|
||||
mutex.acquire(TtWaitForever);
|
||||
data = newData;
|
||||
mutex.release();
|
||||
}
|
||||
|
||||
const Bundle& AppInstance::getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
App tt_app_alloc(const Manifest& manifest, const Bundle& parameters) {
|
||||
auto* instance = new AppInstance(manifest, parameters);
|
||||
return static_cast<App>(instance);
|
||||
}
|
||||
|
||||
void tt_app_free(App app) {
|
||||
auto* instance = static_cast<AppInstance*>(app);
|
||||
delete instance;
|
||||
}
|
||||
|
||||
void tt_app_set_state(App app, State state) {
|
||||
static_cast<AppInstance*>(app)->setState(state);
|
||||
}
|
||||
|
||||
State tt_app_get_state(App app) {
|
||||
return static_cast<AppInstance*>(app)->getState();
|
||||
}
|
||||
|
||||
const Manifest& tt_app_get_manifest(App app) {
|
||||
return static_cast<AppInstance*>(app)->getManifest();
|
||||
}
|
||||
|
||||
Flags tt_app_get_flags(App app) {
|
||||
return static_cast<AppInstance*>(app)->getFlags();
|
||||
}
|
||||
|
||||
void tt_app_set_flags(App app, Flags flags) {
|
||||
return static_cast<AppInstance*>(app)->setFlags(flags);
|
||||
}
|
||||
|
||||
void* tt_app_get_data(App app) {
|
||||
return static_cast<AppInstance*>(app)->getData();
|
||||
}
|
||||
|
||||
void tt_app_set_data(App app, void* data) {
|
||||
return static_cast<AppInstance*>(app)->setData(data);
|
||||
}
|
||||
|
||||
const Bundle& tt_app_get_parameters(App app) {
|
||||
return static_cast<AppInstance*>(app)->getParameters();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include "Manifest.h"
|
||||
#include "Bundle.h"
|
||||
#include "Mutex.h"
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
typedef enum {
|
||||
StateInitial, // App is being activated in loader
|
||||
StateStarted, // App is in memory
|
||||
StateShowing, // App view is created
|
||||
StateHiding, // App view is destroyed
|
||||
StateStopped // App is not in memory
|
||||
} State;
|
||||
|
||||
typedef union {
|
||||
struct {
|
||||
bool show_statusbar : 1;
|
||||
};
|
||||
unsigned char flags;
|
||||
} Flags;
|
||||
|
||||
|
||||
class AppInstance {
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex = Mutex(MutexTypeNormal);
|
||||
const Manifest& manifest;
|
||||
State state = StateInitial;
|
||||
Flags flags = { .show_statusbar = true };
|
||||
/** @brief Optional parameters to start the app with
|
||||
* When these are stored in the app struct, the struct takes ownership.
|
||||
* Do not mutate after app creation.
|
||||
*/
|
||||
tt::Bundle parameters;
|
||||
/** @brief @brief Contextual data related to the running app's instance
|
||||
* The app can attach its data to this.
|
||||
* The lifecycle is determined by the on_start and on_stop methods in the AppManifest.
|
||||
* These manifest methods can optionally allocate/free data that is attached here.
|
||||
*/
|
||||
void* _Nullable data = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
AppInstance(const Manifest& manifest) :
|
||||
manifest(manifest) {}
|
||||
|
||||
AppInstance(const Manifest& manifest, const Bundle& parameters) :
|
||||
manifest(manifest),
|
||||
parameters(parameters) {}
|
||||
|
||||
void setState(State state);
|
||||
State getState();
|
||||
|
||||
const Manifest& getManifest();
|
||||
|
||||
Flags getFlags();
|
||||
void setFlags(Flags flags);
|
||||
|
||||
_Nullable void* getData();
|
||||
void setData(void* data);
|
||||
|
||||
const Bundle& getParameters();
|
||||
};
|
||||
|
||||
/** @brief Create an app
|
||||
* @param manifest
|
||||
* @param parameters optional bundle. memory ownership is transferred to App
|
||||
* @return
|
||||
*/
|
||||
[[deprecated("use class")]]
|
||||
App tt_app_alloc(const Manifest& manifest, const Bundle& parameters);
|
||||
[[deprecated("use class")]]
|
||||
void tt_app_free(App app);
|
||||
|
||||
[[deprecated("use class")]]
|
||||
void tt_app_set_state(App app, State state);
|
||||
[[deprecated("use class")]]
|
||||
State tt_app_get_state(App app);
|
||||
|
||||
[[deprecated("use class")]]
|
||||
const Manifest& tt_app_get_manifest(App app);
|
||||
|
||||
[[deprecated("use class")]]
|
||||
Flags tt_app_get_flags(App app);
|
||||
[[deprecated("use class")]]
|
||||
void tt_app_set_flags(App app, Flags flags);
|
||||
|
||||
[[deprecated("use class")]]
|
||||
void* _Nullable tt_app_get_data(App app);
|
||||
[[deprecated("use class")]]
|
||||
void tt_app_set_data(App app, void* data);
|
||||
|
||||
[[deprecated("use class")]]
|
||||
const Bundle& tt_app_get_parameters(App app);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "app/ManifestRegistry.h"
|
||||
#include "Assets.h"
|
||||
#include "Check.h"
|
||||
#include "lvgl.h"
|
||||
#include <algorithm>
|
||||
#include "service/loader/Loader.h"
|
||||
|
||||
namespace tt::app::desktop {
|
||||
|
||||
static void on_app_pressed(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
const auto* manifest = static_cast<const Manifest*>(lv_event_get_user_data(e));
|
||||
service::loader::start_app(manifest->id, false, Bundle());
|
||||
}
|
||||
}
|
||||
|
||||
static void create_app_widget(const Manifest* manifest, void* parent) {
|
||||
tt_check(parent);
|
||||
auto* list = static_cast<lv_obj_t*>(parent);
|
||||
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
|
||||
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
|
||||
lv_obj_add_event_cb(btn, &on_app_pressed, LV_EVENT_CLICKED, (void*)manifest);
|
||||
}
|
||||
|
||||
static void desktop_show(TT_UNUSED App app, lv_obj_t* parent) {
|
||||
lv_obj_t* list = lv_list_create(parent);
|
||||
lv_obj_set_size(list, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_center(list);
|
||||
|
||||
auto manifests = app_manifest_registry_get();
|
||||
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
|
||||
|
||||
lv_list_add_text(list, "User");
|
||||
for (const auto& manifest: manifests) {
|
||||
if (manifest->type == TypeUser) {
|
||||
create_app_widget(manifest, list);
|
||||
}
|
||||
}
|
||||
|
||||
lv_list_add_text(list, "System");
|
||||
for (const auto& manifest: manifests) {
|
||||
if (manifest->type == TypeSystem) {
|
||||
create_app_widget(manifest, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Desktop",
|
||||
.name = "Desktop",
|
||||
.type = TypeDesktop,
|
||||
.on_show = &desktop_show,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "app/App.h"
|
||||
#include "Assets.h"
|
||||
#include "DisplayPreferences.h"
|
||||
#include "Tactility.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace tt::app::settings::display {
|
||||
|
||||
#define TAG "display"
|
||||
|
||||
static bool backlight_duty_set = false;
|
||||
static uint8_t backlight_duty = 255;
|
||||
|
||||
static void slider_event_cb(lv_event_t* event) {
|
||||
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
const Configuration* config = getConfiguration();
|
||||
hal::SetBacklightDuty set_backlight_duty = config->hardware->display.setBacklightDuty;
|
||||
|
||||
if (set_backlight_duty != nullptr) {
|
||||
int32_t slider_value = lv_slider_get_value(slider);
|
||||
|
||||
backlight_duty = (uint8_t)slider_value;
|
||||
backlight_duty_set = true;
|
||||
|
||||
set_backlight_duty(backlight_duty);
|
||||
}
|
||||
}
|
||||
|
||||
#define ORIENTATION_LANDSCAPE 0
|
||||
#define ORIENTATION_LANDSCAPE_FLIPPED 1
|
||||
#define ORIENTATION_PORTRAIT_LEFT 2
|
||||
#define ORIENTATION_PORTRAIT_RIGHT 3
|
||||
|
||||
static lv_display_rotation_t orientation_setting_to_display_rotation(uint32_t setting) {
|
||||
if (setting == ORIENTATION_LANDSCAPE_FLIPPED) {
|
||||
return LV_DISPLAY_ROTATION_180;
|
||||
} else if (setting == ORIENTATION_PORTRAIT_LEFT) {
|
||||
return LV_DISPLAY_ROTATION_270;
|
||||
} else if (setting == ORIENTATION_PORTRAIT_RIGHT) {
|
||||
return LV_DISPLAY_ROTATION_90;
|
||||
} else {
|
||||
return LV_DISPLAY_ROTATION_0;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t display_rotation_to_orientation_setting(lv_display_rotation_t orientation) {
|
||||
if (orientation == LV_DISPLAY_ROTATION_90) {
|
||||
return ORIENTATION_PORTRAIT_RIGHT;
|
||||
} else if (orientation == LV_DISPLAY_ROTATION_180) {
|
||||
return ORIENTATION_LANDSCAPE_FLIPPED;
|
||||
} else if (orientation == LV_DISPLAY_ROTATION_270) {
|
||||
return ORIENTATION_PORTRAIT_LEFT;
|
||||
} else {
|
||||
return ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_orientation_set(lv_event_t* event) {
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t selected = lv_dropdown_get_selected(dropdown);
|
||||
TT_LOG_I(TAG, "Selected %ld", selected);
|
||||
lv_display_rotation_t rotation = orientation_setting_to_display_rotation(selected);
|
||||
if (lv_display_get_rotation(lv_display_get_default()) != rotation) {
|
||||
lv_display_set_rotation(lv_display_get_default(), rotation);
|
||||
preferences_set_rotation(rotation);
|
||||
}
|
||||
}
|
||||
|
||||
static void app_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
|
||||
lv_obj_t* brightness_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(brightness_label, "Brightness");
|
||||
|
||||
lv_obj_t* brightness_slider = lv_slider_create(wrapper);
|
||||
lv_obj_set_width(brightness_slider, LV_PCT(50));
|
||||
lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
|
||||
lv_slider_set_range(brightness_slider, 0, 255);
|
||||
lv_obj_add_event_cb(brightness_slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
|
||||
const Configuration* config = getConfiguration();
|
||||
hal::SetBacklightDuty set_backlight_duty = config->hardware->display.setBacklightDuty;
|
||||
if (set_backlight_duty == nullptr) {
|
||||
lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF);
|
||||
lv_obj_add_state(brightness_slider, LV_STATE_DISABLED);
|
||||
} else {
|
||||
uint8_t value = preferences_get_backlight_duty();
|
||||
lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
lv_obj_t* orientation_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(orientation_label, "Orientation");
|
||||
lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 40);
|
||||
|
||||
lv_obj_t* orientation_dropdown = lv_dropdown_create(wrapper);
|
||||
lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right");
|
||||
lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 32);
|
||||
lv_obj_add_event_cb(orientation_dropdown, on_orientation_set, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
uint32_t orientation_selected = display_rotation_to_orientation_setting(
|
||||
lv_display_get_rotation(lv_display_get_default())
|
||||
);
|
||||
lv_dropdown_set_selected(orientation_dropdown, orientation_selected);
|
||||
}
|
||||
|
||||
static void app_hide(TT_UNUSED App app) {
|
||||
if (backlight_duty_set) {
|
||||
preferences_set_backlight_duty(backlight_duty);
|
||||
}
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Display",
|
||||
.name = "Display",
|
||||
.icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
.on_show = &app_show,
|
||||
.on_hide = &app_hide
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "DisplayPreferences.h"
|
||||
#include "Preferences.h"
|
||||
|
||||
namespace tt::app::settings::display {
|
||||
|
||||
tt::Preferences preferences("display");
|
||||
|
||||
#define BACKLIGHT_DUTY_KEY "backlight_duty"
|
||||
#define ROTATION_KEY "rotation"
|
||||
|
||||
void preferences_set_backlight_duty(uint8_t value) {
|
||||
preferences.putInt32(BACKLIGHT_DUTY_KEY, (int32_t)value);
|
||||
}
|
||||
|
||||
uint8_t preferences_get_backlight_duty() {
|
||||
int32_t result;
|
||||
if (preferences.optInt32(BACKLIGHT_DUTY_KEY, result)) {
|
||||
return (uint8_t)(result % 255);
|
||||
} else {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
void preferences_set_rotation(lv_display_rotation_t rotation) {
|
||||
preferences.putInt32(ROTATION_KEY, (int32_t)rotation);
|
||||
}
|
||||
|
||||
lv_display_rotation_t preferences_get_rotation() {
|
||||
int32_t rotation;
|
||||
if (preferences.optInt32(ROTATION_KEY, rotation)) {
|
||||
return (lv_display_rotation_t)rotation;
|
||||
} else {
|
||||
return LV_DISPLAY_ROTATION_0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <src/display/lv_display.h>
|
||||
|
||||
namespace tt::app::settings::display {
|
||||
|
||||
void preferences_set_backlight_duty(uint8_t value);
|
||||
uint8_t preferences_get_backlight_duty();
|
||||
void preferences_set_rotation(lv_display_rotation_t rotation);
|
||||
lv_display_rotation_t preferences_get_rotation();
|
||||
|
||||
} // namespace
|
||||
@@ -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 "Tactility.h"
|
||||
#include "app/App.h"
|
||||
#include "Assets.h"
|
||||
#include "Check.h"
|
||||
#include "FileUtils.h"
|
||||
#include "StringUtils.h"
|
||||
#include "app/ImageViewer/ImageViewer.h"
|
||||
#include "app/TextViewer/TextViewer.h"
|
||||
#include "lvgl.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "ui/Toolbar.h"
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
#define TAG "files_app"
|
||||
|
||||
/**
|
||||
* Case-insensitive check to see if the given file matches the provided file extension.
|
||||
* @param path the full path to the file
|
||||
* @param extension the extension to look for, including the period symbol, in lower case
|
||||
* @return true on match
|
||||
*/
|
||||
static bool has_file_extension(const char* path, const char* extension) {
|
||||
size_t postfix_len = strlen(extension);
|
||||
size_t base_len = strlen(path);
|
||||
if (base_len < postfix_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = (int)postfix_len - 1; i >= 0; i--) {
|
||||
if (tolower(path[base_len - postfix_len + i]) != tolower(extension[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_supported_image_file(const char* filename) {
|
||||
// Currently only the PNG library is built into Tactility
|
||||
return has_file_extension(filename, ".png");
|
||||
}
|
||||
|
||||
static bool is_supported_text_file(const char* filename) {
|
||||
return has_file_extension(filename, ".txt") ||
|
||||
has_file_extension(filename, ".ini") ||
|
||||
has_file_extension(filename, ".json") ||
|
||||
has_file_extension(filename, ".yaml") ||
|
||||
has_file_extension(filename, ".yml") ||
|
||||
has_file_extension(filename, ".lua") ||
|
||||
has_file_extension(filename, ".js") ||
|
||||
has_file_extension(filename, ".properties");
|
||||
}
|
||||
|
||||
// region Views
|
||||
|
||||
static void update_views(Data* data);
|
||||
|
||||
static void on_navigate_up_pressed(lv_event_t* event) {
|
||||
auto* files_data = (Data*)lv_event_get_user_data(event);
|
||||
if (strcmp(files_data->current_path, "/") != 0) {
|
||||
TT_LOG_I(TAG, "Navigating upwards");
|
||||
char new_absolute_path[MAX_PATH_LENGTH];
|
||||
if (string_get_path_parent(files_data->current_path, new_absolute_path)) {
|
||||
data_set_entries_for_path(files_data, new_absolute_path);
|
||||
}
|
||||
}
|
||||
update_views(files_data);
|
||||
}
|
||||
|
||||
static void on_exit_app_pressed(TT_UNUSED lv_event_t* event) {
|
||||
service::loader::stop_app();
|
||||
}
|
||||
|
||||
static void view_file(const char* path, const char* filename) {
|
||||
size_t path_len = strlen(path);
|
||||
size_t filename_len = strlen(filename);
|
||||
char* filepath = static_cast<char*>(malloc(path_len + filename_len + 2));
|
||||
sprintf(filepath, "%s/%s", path, filename);
|
||||
|
||||
// For PC we need to make the path relative to the current work directory,
|
||||
// because that's how LVGL maps its 'drive letter' to the file system.
|
||||
char* processed_filepath;
|
||||
if (get_platform() == PlatformPc) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
|
||||
TT_LOG_E(TAG, "Failed to get current working directory");
|
||||
return;
|
||||
}
|
||||
if (!strstr(filepath, cwd)) {
|
||||
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
|
||||
return;
|
||||
}
|
||||
char* substr = filepath + strlen(cwd);
|
||||
processed_filepath = substr;
|
||||
} else {
|
||||
processed_filepath = filepath;
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "Clicked %s", filepath);
|
||||
|
||||
if (is_supported_image_file(filename)) {
|
||||
Bundle bundle;
|
||||
bundle.putString(IMAGE_VIEWER_FILE_ARGUMENT, processed_filepath);
|
||||
service::loader::start_app("ImageViewer", false, bundle);
|
||||
} else if (is_supported_text_file(filename)) {
|
||||
Bundle bundle;
|
||||
if (get_platform() == PlatformEsp) {
|
||||
bundle.putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath);
|
||||
} else {
|
||||
// Remove forward slash, because we need a relative path
|
||||
bundle.putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath + 1);
|
||||
}
|
||||
service::loader::start_app("TextViewer", false, bundle);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "opening files of this type is not supported");
|
||||
}
|
||||
|
||||
free(filepath);
|
||||
}
|
||||
|
||||
static void on_file_pressed(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
lv_obj_t* button = lv_event_get_current_target_obj(event);
|
||||
auto* files_data = static_cast<Data*>(lv_obj_get_user_data(button));
|
||||
|
||||
auto* dir_entry = static_cast<dirent*>(lv_event_get_user_data(event));
|
||||
TT_LOG_I(TAG, "Pressed %s %d", dir_entry->d_name, dir_entry->d_type);
|
||||
|
||||
switch (dir_entry->d_type) {
|
||||
case TT_DT_DIR:
|
||||
data_set_entries_for_child_path(files_data, dir_entry->d_name);
|
||||
update_views(files_data);
|
||||
break;
|
||||
case TT_DT_LNK:
|
||||
TT_LOG_W(TAG, "opening links is not supported");
|
||||
break;
|
||||
case TT_DT_REG:
|
||||
view_file(files_data->current_path, dir_entry->d_name);
|
||||
break;
|
||||
default:
|
||||
// Assume it's a file
|
||||
// TODO: Find a better way to identify a file
|
||||
view_file(files_data->current_path, dir_entry->d_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void create_file_widget(Data* files_data, lv_obj_t* parent, struct dirent* dir_entry) {
|
||||
tt_check(parent);
|
||||
auto* list = (lv_obj_t*)parent;
|
||||
const char* symbol;
|
||||
if (dir_entry->d_type == TT_DT_DIR) {
|
||||
symbol = LV_SYMBOL_DIRECTORY;
|
||||
} else if (is_supported_image_file(dir_entry->d_name)) {
|
||||
symbol = LV_SYMBOL_IMAGE;
|
||||
} else if (dir_entry->d_type == TT_DT_LNK) {
|
||||
symbol = LV_SYMBOL_LOOP;
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry->d_name);
|
||||
lv_obj_set_user_data(button, files_data);
|
||||
lv_obj_add_event_cb(button, &on_file_pressed, LV_EVENT_CLICKED, (void*)dir_entry);
|
||||
}
|
||||
|
||||
static void update_views(Data* data) {
|
||||
lv_obj_clean(data->list);
|
||||
for (int i = 0; i < data->dir_entries_count; ++i) {
|
||||
create_file_widget(data, data->list, data->dir_entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Views
|
||||
|
||||
// region Lifecycle
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
auto* data = static_cast<Data*>(tt_app_get_data(app));
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, "Files");
|
||||
lvgl::toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &on_exit_app_pressed, nullptr);
|
||||
lvgl::toolbar_add_action(toolbar, LV_SYMBOL_UP, "Navigate up", &on_navigate_up_pressed, data);
|
||||
|
||||
data->list = lv_list_create(parent);
|
||||
lv_obj_set_width(data->list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(data->list, 1);
|
||||
|
||||
update_views(data);
|
||||
}
|
||||
|
||||
static void on_start(App app) {
|
||||
auto* data = data_alloc();
|
||||
// PC platform is bound to current work directory because of the LVGL file system mapping
|
||||
if (get_platform() == PlatformPc) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
|
||||
data_set_entries_for_path(data, cwd);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to get current work directory files");
|
||||
data_set_entries_for_path(data, "/");
|
||||
}
|
||||
} else {
|
||||
data_set_entries_for_path(data, "/");
|
||||
}
|
||||
|
||||
tt_app_set_data(app, data);
|
||||
}
|
||||
|
||||
static void on_stop(App app) {
|
||||
auto* data = static_cast<Data*>(tt_app_get_data(app));
|
||||
data_free(data);
|
||||
}
|
||||
|
||||
// endregion Lifecycle
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Files",
|
||||
.name = "Files",
|
||||
.icon = TT_ASSETS_APP_ICON_FILES,
|
||||
.type = TypeSystem,
|
||||
.on_start = &on_start,
|
||||
.on_stop = &on_stop,
|
||||
.on_show = &on_show,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -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,214 @@
|
||||
#include "Mutex.h"
|
||||
#include "Thread.h"
|
||||
#include "Tactility.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
#include "GpioHal.h"
|
||||
#include "ui/LvglSync.h"
|
||||
|
||||
namespace tt::app::gpio {
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t* lv_pins[GPIO_NUM_MAX];
|
||||
uint8_t pin_states[GPIO_NUM_MAX];
|
||||
Thread* thread;
|
||||
Mutex* mutex;
|
||||
bool thread_interrupted;
|
||||
} Gpio;
|
||||
|
||||
static void lock(Gpio* gpio) {
|
||||
tt_check(tt_mutex_acquire(gpio->mutex, 1000) == TtStatusOk);
|
||||
}
|
||||
|
||||
static void unlock(Gpio* gpio) {
|
||||
tt_check(tt_mutex_release(gpio->mutex) == TtStatusOk);
|
||||
}
|
||||
|
||||
static void update_pin_states(Gpio* gpio) {
|
||||
lock(gpio);
|
||||
// Update pin states
|
||||
for (int i = 0; i < GPIO_NUM_MAX; ++i) {
|
||||
#ifdef ESP_PLATFORM
|
||||
gpio->pin_states[i] = gpio_get_level((gpio_num_t)i);
|
||||
#else
|
||||
gpio->pin_states[i] = gpio_get_level(i);
|
||||
#endif
|
||||
}
|
||||
unlock(gpio);
|
||||
}
|
||||
|
||||
static void update_pin_widgets(Gpio* gpio) {
|
||||
if (lvgl::lock(100)) {
|
||||
lock(gpio);
|
||||
for (int j = 0; j < GPIO_NUM_MAX; ++j) {
|
||||
int level = gpio->pin_states[j];
|
||||
lv_obj_t* label = gpio->lv_pins[j];
|
||||
void* label_user_data = lv_obj_get_user_data(label);
|
||||
// The user data stores the state, so we can avoid unnecessary updates
|
||||
if ((void*)level != label_user_data) {
|
||||
lv_obj_set_user_data(label, (void*)level);
|
||||
if (level == 0) {
|
||||
lv_obj_set_style_text_color(label, lv_color_black(), 0);
|
||||
} else {
|
||||
lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
lvgl::unlock();
|
||||
unlock(gpio);
|
||||
}
|
||||
}
|
||||
|
||||
static lv_obj_t* create_gpio_row_wrapper(lv_obj_t* parent) {
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// region Task
|
||||
|
||||
static int32_t gpio_task(void* context) {
|
||||
Gpio* gpio = (Gpio*)context;
|
||||
bool interrupted = false;
|
||||
|
||||
while (!interrupted) {
|
||||
delay_ms(100);
|
||||
|
||||
update_pin_states(gpio);
|
||||
update_pin_widgets(gpio);
|
||||
|
||||
lock(gpio);
|
||||
interrupted = gpio->thread_interrupted;
|
||||
unlock(gpio);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void task_start(Gpio* gpio) {
|
||||
tt_assert(gpio->thread == nullptr);
|
||||
lock(gpio);
|
||||
gpio->thread = new Thread(
|
||||
"gpio",
|
||||
4096,
|
||||
&gpio_task,
|
||||
gpio
|
||||
);
|
||||
gpio->thread_interrupted = false;
|
||||
gpio->thread->start();
|
||||
unlock(gpio);
|
||||
}
|
||||
|
||||
static void task_stop(Gpio* gpio) {
|
||||
tt_assert(gpio->thread);
|
||||
lock(gpio);
|
||||
gpio->thread_interrupted = true;
|
||||
unlock(gpio);
|
||||
|
||||
gpio->thread->join();
|
||||
|
||||
lock(gpio);
|
||||
delete gpio->thread;
|
||||
gpio->thread = nullptr;
|
||||
unlock(gpio);
|
||||
}
|
||||
|
||||
// endregion Task
|
||||
|
||||
// region App lifecycle
|
||||
|
||||
static void app_show(App app, lv_obj_t* parent) {
|
||||
auto* gpio = static_cast<Gpio*>(tt_app_get_data(app));
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
// Main content wrapper, enables scrolling content without scrolling the toolbar
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
|
||||
uint8_t column = 0;
|
||||
uint8_t column_limit = 10;
|
||||
int32_t x_spacing = 20;
|
||||
|
||||
lv_obj_t* row_wrapper = create_gpio_row_wrapper(wrapper);
|
||||
lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lock(gpio);
|
||||
for (int i = GPIO_NUM_MIN; i < GPIO_NUM_MAX; ++i) {
|
||||
|
||||
// Add the GPIO number before the first item on a row
|
||||
if (column == 0) {
|
||||
lv_obj_t* prefix = lv_label_create(row_wrapper);
|
||||
lv_label_set_text_fmt(prefix, "%02d", i);
|
||||
}
|
||||
|
||||
// Add a new GPIO status indicator
|
||||
lv_obj_t* status_label = lv_label_create(row_wrapper);
|
||||
lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0);
|
||||
lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP);
|
||||
gpio->lv_pins[i] = status_label;
|
||||
|
||||
column++;
|
||||
|
||||
if (column >= column_limit) {
|
||||
// Add the GPIO number after the last item on a row
|
||||
lv_obj_t* postfix = lv_label_create(row_wrapper);
|
||||
lv_label_set_text_fmt(postfix, "%02d", i);
|
||||
lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing), 0);
|
||||
|
||||
// Add a new row wrapper underneath the last one
|
||||
lv_obj_t* new_row_wrapper = create_gpio_row_wrapper(wrapper);
|
||||
lv_obj_align_to(new_row_wrapper, row_wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 4);
|
||||
row_wrapper = new_row_wrapper;
|
||||
|
||||
column = 0;
|
||||
}
|
||||
}
|
||||
unlock(gpio);
|
||||
|
||||
task_start(gpio);
|
||||
}
|
||||
|
||||
static void on_hide(App app) {
|
||||
auto* gpio = static_cast<Gpio*>(tt_app_get_data(app));
|
||||
task_stop(gpio);
|
||||
}
|
||||
|
||||
static void on_start(App app) {
|
||||
auto* gpio = static_cast<Gpio*>(malloc(sizeof(Gpio)));
|
||||
*gpio = (Gpio) {
|
||||
.lv_pins = { nullptr },
|
||||
.pin_states = { 0 },
|
||||
.thread = nullptr,
|
||||
.mutex = tt_mutex_alloc(MutexTypeNormal),
|
||||
.thread_interrupted = true,
|
||||
};
|
||||
tt_app_set_data(app, gpio);
|
||||
}
|
||||
|
||||
static void on_stop(App app) {
|
||||
auto* gpio = static_cast<Gpio*>(tt_app_get_data(app));
|
||||
tt_mutex_free(gpio->mutex);
|
||||
free(gpio);
|
||||
}
|
||||
|
||||
// endregion App lifecycle
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Gpio",
|
||||
.name = "GPIO",
|
||||
.type = TypeSystem,
|
||||
.on_start = &on_start,
|
||||
.on_stop = &on_stop,
|
||||
.on_show = &app_show,
|
||||
.on_hide = &on_hide
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -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,96 @@
|
||||
#include "Assets.h"
|
||||
#include "hal/i2c/I2c.h"
|
||||
#include "Tactility.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace tt::app::settings::i2c {
|
||||
|
||||
static void on_switch_toggle(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* state_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
const hal::i2c::Configuration* configuration = static_cast<hal::i2c::Configuration*>(lv_event_get_user_data(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool should_enable = lv_obj_get_state(state_switch) & LV_STATE_CHECKED;
|
||||
bool is_enabled = hal::i2c::isStarted(configuration->port);
|
||||
if (is_enabled && !should_enable) {
|
||||
if (!hal::i2c::stop(configuration->port)) {
|
||||
lv_obj_add_state(state_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
} else if (!is_enabled && should_enable) {
|
||||
if (!hal::i2c::start(configuration->port)) {
|
||||
lv_obj_remove_state(state_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void show(lv_obj_t* parent, const hal::i2c::Configuration& configuration) {
|
||||
lv_obj_t* card = lv_obj_create(parent);
|
||||
lv_obj_set_height(card, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_margin_hor(card, 16, 0);
|
||||
lv_obj_set_style_margin_bottom(card, 16, 0);
|
||||
lv_obj_set_width(card, LV_PCT(100));
|
||||
lv_obj_t* port_label = lv_label_create(card);
|
||||
const char* name = configuration.name.empty() ? "Unnamed" : configuration.name.c_str();
|
||||
lv_label_set_text_fmt(port_label, "%s (port %d)", name, configuration.port);
|
||||
|
||||
// On-off switch
|
||||
|
||||
if (configuration.canReinit) {
|
||||
lv_obj_t* state_switch = lv_switch_create(card);
|
||||
lv_obj_align(state_switch, LV_ALIGN_TOP_RIGHT, 0, 0);
|
||||
|
||||
if (hal::i2c::isStarted(configuration.port)) {
|
||||
lv_obj_add_state(state_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
lv_obj_add_event_cb(state_switch, on_switch_toggle, LV_EVENT_VALUE_CHANGED, (void*)&configuration);
|
||||
}
|
||||
|
||||
// SDA label
|
||||
lv_obj_t* sda_pin_label = lv_label_create(card);
|
||||
const char* sda_pullup_text = configuration.config.sda_pullup_en ? "yes" : "no";
|
||||
lv_label_set_text_fmt(sda_pin_label, "SDA: pin %d, pullup: %s", configuration.config.sda_io_num, sda_pullup_text );
|
||||
lv_obj_align_to(sda_pin_label, port_label, LV_ALIGN_OUT_BOTTOM_LEFT, 16, 8);
|
||||
|
||||
// SCL label
|
||||
lv_obj_t* scl_pin_label = lv_label_create(card);
|
||||
const char* scl_pullup_text = configuration.config.scl_pullup_en ? "yes" : "no";
|
||||
lv_label_set_text_fmt(scl_pin_label, "SCL: pin %d, pullup: %s", configuration.config.scl_io_num, scl_pullup_text);
|
||||
lv_obj_align_to(scl_pin_label, sda_pin_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
|
||||
|
||||
// Frequency:
|
||||
if (configuration.config.mode == I2C_MODE_MASTER) {
|
||||
lv_obj_t* frequency_label = lv_label_create(card);
|
||||
lv_label_set_text_fmt(frequency_label, "Frequency: %lu Hz", configuration.config.master.clk_speed);
|
||||
lv_obj_align_to(frequency_label, scl_pin_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::obj_set_style_no_padding(wrapper);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
|
||||
for (const auto& configuration: getConfiguration()->hardware->i2c) {
|
||||
show(wrapper, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "I2cSettings",
|
||||
.name = "I2C",
|
||||
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.on_show = &on_show
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "ImageViewer.h"
|
||||
#include "Log.h"
|
||||
#include "lvgl.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
namespace tt::app::image_viewer {
|
||||
|
||||
#define TAG "image_viewer"
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::obj_set_style_no_padding(wrapper);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
|
||||
lv_obj_t* image = lv_img_create(wrapper);
|
||||
lv_obj_align(image, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
const Bundle& bundle = tt_app_get_parameters(app);
|
||||
std::string file_argument;
|
||||
if (bundle.optString(IMAGE_VIEWER_FILE_ARGUMENT, file_argument)) {
|
||||
std::string prefixed_path = "A:" + file_argument;
|
||||
TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str());
|
||||
lv_img_set_src(image, prefixed_path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "ImageViewer",
|
||||
.name = "Image Viewer",
|
||||
.type = TypeHidden,
|
||||
.on_show = &on_show
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define IMAGE_VIEWER_FILE_ARGUMENT "file"
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "CoreDefines.h"
|
||||
|
||||
// Forward declarations
|
||||
typedef struct _lv_obj_t lv_obj_t;
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
typedef void* App;
|
||||
|
||||
typedef enum {
|
||||
/** A desktop app sits at the root of the app stack managed by the Loader service */
|
||||
TypeDesktop,
|
||||
/** Apps that generally aren't started from the desktop (e.g. image viewer) */
|
||||
TypeHidden,
|
||||
/** Standard apps, provided by the system. */
|
||||
TypeSystem,
|
||||
/** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
|
||||
TypeSettings,
|
||||
/** User-provided apps. */
|
||||
TypeUser
|
||||
} Type;
|
||||
|
||||
typedef void (*AppOnStart)(App app);
|
||||
typedef void (*AppOnStop)(App app);
|
||||
typedef void (*AppOnShow)(App app, lv_obj_t* parent);
|
||||
typedef void (*AppOnHide)(App app);
|
||||
|
||||
typedef struct Manifest {
|
||||
/**
|
||||
* The identifier by which the app is launched by the system and other apps.
|
||||
*/
|
||||
std::string id;
|
||||
|
||||
/**
|
||||
* The user-readable name of the app. Used in UI.
|
||||
*/
|
||||
std::string name;
|
||||
|
||||
/**
|
||||
* Optional icon.
|
||||
*/
|
||||
std::string icon = {};
|
||||
|
||||
/**
|
||||
* App type affects launch behaviour.
|
||||
*/
|
||||
const Type type = TypeUser;
|
||||
|
||||
/**
|
||||
* Non-blocking method to call when app is started.
|
||||
*/
|
||||
const AppOnStart on_start = nullptr;
|
||||
|
||||
/**
|
||||
* Non-blocking method to call when app is stopped.
|
||||
*/
|
||||
const AppOnStop _Nullable on_stop = nullptr;
|
||||
|
||||
/**
|
||||
* Non-blocking method to create the GUI
|
||||
*/
|
||||
const AppOnShow _Nullable on_show = nullptr;
|
||||
|
||||
/**
|
||||
* Non-blocking method, called before gui is destroyed
|
||||
*/
|
||||
const AppOnHide _Nullable on_hide = nullptr;
|
||||
} AppManifest;
|
||||
|
||||
struct {
|
||||
bool operator()(const Manifest* a, const Manifest* b) const { return a->name < b->name; }
|
||||
} SortAppManifestByName;
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "ManifestRegistry.h"
|
||||
#include "Mutex.h"
|
||||
#include "TactilityCore.h"
|
||||
#include <unordered_map>
|
||||
|
||||
#define TAG "app_registry"
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
typedef std::unordered_map<std::string, const Manifest*> AppManifestMap;
|
||||
|
||||
static AppManifestMap app_manifest_map;
|
||||
static Mutex* hash_mutex = nullptr;
|
||||
|
||||
static void app_registry_lock() {
|
||||
tt_assert(hash_mutex != nullptr);
|
||||
tt_mutex_acquire(hash_mutex, TtWaitForever);
|
||||
}
|
||||
|
||||
static void app_registry_unlock() {
|
||||
tt_assert(hash_mutex != nullptr);
|
||||
tt_mutex_release(hash_mutex);
|
||||
}
|
||||
|
||||
void app_manifest_registry_init() {
|
||||
tt_assert(hash_mutex == nullptr);
|
||||
hash_mutex = tt_mutex_alloc(MutexTypeNormal);
|
||||
}
|
||||
void app_manifest_registry_add(const Manifest* manifest) {
|
||||
TT_LOG_I(TAG, "adding %s", manifest->id.c_str());
|
||||
|
||||
app_registry_lock();
|
||||
app_manifest_map[manifest->id] = manifest;
|
||||
app_registry_unlock();
|
||||
}
|
||||
|
||||
_Nullable const Manifest * app_manifest_registry_find_by_id(const std::string& id) {
|
||||
app_registry_lock();
|
||||
auto iterator = app_manifest_map.find(id);
|
||||
_Nullable const Manifest* result = iterator != app_manifest_map.end() ? iterator->second : nullptr;
|
||||
app_registry_unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<const Manifest*> app_manifest_registry_get() {
|
||||
std::vector<const Manifest*> manifests;
|
||||
app_registry_lock();
|
||||
for (const auto& item: app_manifest_map) {
|
||||
manifests.push_back(item.second);
|
||||
}
|
||||
app_registry_unlock();
|
||||
return manifests;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "Manifest.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
void app_manifest_registry_init();
|
||||
void app_manifest_registry_add(const Manifest* manifest);
|
||||
void app_manifest_registry_remove(const Manifest* manifest);
|
||||
const Manifest _Nullable* app_manifest_registry_find_by_id(const std::string& id);
|
||||
std::vector<const Manifest*> app_manifest_registry_get();
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "app/App.h"
|
||||
#include "Assets.h"
|
||||
#include "lvgl.h"
|
||||
#include "Tactility.h"
|
||||
#include "Timer.h"
|
||||
#include "ui/LvglSync.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
namespace tt::app::settings::power {
|
||||
|
||||
#define TAG "power"
|
||||
|
||||
typedef struct {
|
||||
Timer* update_timer;
|
||||
const hal::Power* power;
|
||||
lv_obj_t* enable_switch;
|
||||
lv_obj_t* charge_state;
|
||||
lv_obj_t* charge_level;
|
||||
lv_obj_t* current;
|
||||
} AppData;
|
||||
|
||||
static void app_update_ui(void* callbackContext) {
|
||||
auto* data = (AppData*)callbackContext;
|
||||
bool charging_enabled = data->power->isChargingEnabled();
|
||||
const char* charge_state = data->power->isCharging() ? "yes" : "no";
|
||||
uint8_t charge_level = data->power->getChargeLevel();
|
||||
uint16_t charge_level_scaled = (int16_t)charge_level * 100 / 255;
|
||||
int32_t current = data->power->getCurrent();
|
||||
|
||||
lvgl::lock(ms_to_ticks(1000));
|
||||
lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled);
|
||||
lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state);
|
||||
lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level_scaled);
|
||||
#ifdef ESP_PLATFORM
|
||||
lv_label_set_text_fmt(data->current, "Current: %ld mAh", current);
|
||||
#else
|
||||
lv_label_set_text_fmt(data->current, "Current: %d mAh", current);
|
||||
#endif
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
static void on_power_enabled_change(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
auto* data = static_cast<AppData*>(lv_event_get_user_data(event));
|
||||
if (data->power->isChargingEnabled() != is_on) {
|
||||
data->power->setChargingEnabled(is_on);
|
||||
app_update_ui(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void app_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
auto* data = static_cast<AppData*>(tt_app_get_data(app));
|
||||
|
||||
// Top row: enable/disable
|
||||
lv_obj_t* switch_container = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(switch_container, LV_PCT(100));
|
||||
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_no_padding(switch_container);
|
||||
lvgl::obj_set_style_bg_invisible(switch_container);
|
||||
|
||||
lv_obj_t* enable_label = lv_label_create(switch_container);
|
||||
lv_label_set_text(enable_label, "Charging enabled");
|
||||
lv_obj_set_align(enable_label, LV_ALIGN_LEFT_MID);
|
||||
|
||||
lv_obj_t* enable_switch = lv_switch_create(switch_container);
|
||||
lv_obj_add_event_cb(enable_switch, on_power_enabled_change, LV_EVENT_ALL, data);
|
||||
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
|
||||
|
||||
data->enable_switch = enable_switch;
|
||||
data->charge_state = lv_label_create(wrapper);
|
||||
data->charge_level = lv_label_create(wrapper);
|
||||
data->current = lv_label_create(wrapper);
|
||||
|
||||
app_update_ui(data);
|
||||
data->update_timer->start(ms_to_ticks(1000));
|
||||
}
|
||||
|
||||
static void app_hide(TT_UNUSED App app) {
|
||||
auto* data = static_cast<AppData*>(tt_app_get_data(app));
|
||||
data->update_timer->stop();
|
||||
}
|
||||
|
||||
static void app_start(App app) {
|
||||
auto* data = new AppData();
|
||||
data->update_timer = new Timer(Timer::TypePeriodic, &app_update_ui, data);
|
||||
data->power = getConfiguration()->hardware->power;
|
||||
assert(data->power != nullptr); // The Power app only shows up on supported devices
|
||||
tt_app_set_data(app, data);
|
||||
}
|
||||
|
||||
static void app_stop(App app) {
|
||||
auto* data = static_cast<AppData*>(tt_app_get_data(app));
|
||||
delete data->update_timer;
|
||||
delete data;
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Power",
|
||||
.name = "Power",
|
||||
.icon = TT_ASSETS_APP_ICON_POWER_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.on_start = &app_start,
|
||||
.on_stop = &app_stop,
|
||||
.on_show = &app_show,
|
||||
.on_hide = &app_hide
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "ScreenshotUi.h"
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
auto* ui = static_cast<ScreenshotUi*>(tt_app_get_data(app));
|
||||
create_ui(app, ui, parent);
|
||||
}
|
||||
|
||||
static void on_start(App app) {
|
||||
auto* ui = static_cast<ScreenshotUi*>(malloc(sizeof(ScreenshotUi)));
|
||||
tt_app_set_data(app, ui);
|
||||
}
|
||||
|
||||
static void on_stop(App app) {
|
||||
auto* ui = static_cast<ScreenshotUi*>(tt_app_get_data(app));
|
||||
free(ui);
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Screenshot",
|
||||
.name = "_Screenshot", // So it gets put at the bottom of the desktop and becomes less visible on small screen devices
|
||||
.icon = LV_SYMBOL_IMAGE,
|
||||
.type = TypeSystem,
|
||||
.on_start = &on_start,
|
||||
.on_stop = &on_stop,
|
||||
.on_show = &on_show,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "ScreenshotUi.h"
|
||||
|
||||
#include "TactilityCore.h"
|
||||
#include "hal/sdcard/Sdcard.h"
|
||||
#include "service/gui/Gui.h"
|
||||
#include "service/screenshot/Screenshot.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
|
||||
#define TAG "screenshot_ui"
|
||||
|
||||
static void update_mode(ScreenshotUi* ui) {
|
||||
lv_obj_t* label = ui->start_stop_button_label;
|
||||
if (service::screenshot::is_started()) {
|
||||
lv_label_set_text(label, "Stop");
|
||||
} else {
|
||||
lv_label_set_text(label, "Start");
|
||||
}
|
||||
|
||||
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown);
|
||||
if (selected == 0) { // Timer
|
||||
lv_obj_remove_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_mode_set(lv_event_t* event) {
|
||||
auto* ui = (ScreenshotUi*)lv_event_get_user_data(event);
|
||||
update_mode(ui);
|
||||
}
|
||||
|
||||
static void on_start_pressed(lv_event_t* event) {
|
||||
auto* ui = static_cast<ScreenshotUi*>(lv_event_get_user_data(event));
|
||||
|
||||
if (service::screenshot::is_started()) {
|
||||
TT_LOG_I(TAG, "Stop screenshot");
|
||||
service::screenshot::stop();
|
||||
} else {
|
||||
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown);
|
||||
const char* path = lv_textarea_get_text(ui->path_textarea);
|
||||
if (selected == 0) {
|
||||
TT_LOG_I(TAG, "Start timed screenshots");
|
||||
const char* delay_text = lv_textarea_get_text(ui->delay_textarea);
|
||||
int delay = atoi(delay_text);
|
||||
if (delay > 0) {
|
||||
service::screenshot::start_timed(path, delay, 1);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "Ignored screenshot start because delay was 0");
|
||||
}
|
||||
} else {
|
||||
TT_LOG_I(TAG, "Start app screenshots");
|
||||
service::screenshot::start_apps(path);
|
||||
}
|
||||
}
|
||||
|
||||
update_mode(ui);
|
||||
}
|
||||
|
||||
static void create_mode_setting_ui(ScreenshotUi* ui, lv_obj_t* parent) {
|
||||
lv_obj_t* mode_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(mode_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(mode_wrapper, 0, 0);
|
||||
|
||||
lv_obj_t* mode_label = lv_label_create(mode_wrapper);
|
||||
lv_label_set_text(mode_label, "Mode:");
|
||||
lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* mode_dropdown = lv_dropdown_create(mode_wrapper);
|
||||
lv_dropdown_set_options(mode_dropdown, "Timer\nApp start");
|
||||
lv_obj_align_to(mode_dropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
|
||||
lv_obj_add_event_cb(mode_dropdown, on_mode_set, LV_EVENT_VALUE_CHANGED, ui);
|
||||
ui->mode_dropdown = mode_dropdown;
|
||||
service::screenshot::ScreenshotMode mode = service::screenshot::get_mode();
|
||||
if (mode == service::screenshot::ScreenshotModeApps) {
|
||||
lv_dropdown_set_selected(mode_dropdown, 1);
|
||||
}
|
||||
|
||||
lv_obj_t* button = lv_button_create(mode_wrapper);
|
||||
lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_t* button_label = lv_label_create(button);
|
||||
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
ui->start_stop_button_label = button_label;
|
||||
lv_obj_add_event_cb(button, &on_start_pressed, LV_EVENT_CLICKED, ui);
|
||||
}
|
||||
|
||||
static void create_path_ui(ScreenshotUi* ui, lv_obj_t* parent) {
|
||||
lv_obj_t* path_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(path_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(path_wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW);
|
||||
|
||||
lv_obj_t* label_wrapper = lv_obj_create(path_wrapper);
|
||||
lv_obj_set_style_border_width(label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(label_wrapper, 0, 0);
|
||||
lv_obj_set_size(label_wrapper, 44, 36);
|
||||
lv_obj_t* path_label = lv_label_create(label_wrapper);
|
||||
lv_label_set_text(path_label, "Path:");
|
||||
lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* path_textarea = lv_textarea_create(path_wrapper);
|
||||
lv_textarea_set_one_line(path_textarea, true);
|
||||
lv_obj_set_flex_grow(path_textarea, 1);
|
||||
ui->path_textarea = path_textarea;
|
||||
if (get_platform() == PlatformEsp) {
|
||||
if (hal::sdcard::getState() == hal::sdcard::StateMounted) {
|
||||
lv_textarea_set_text(path_textarea, "A:/sdcard");
|
||||
} else {
|
||||
lv_textarea_set_text(path_textarea, "Error: no SD card");
|
||||
}
|
||||
} else { // PC
|
||||
lv_textarea_set_text(path_textarea, "A:");
|
||||
}
|
||||
}
|
||||
|
||||
static void create_timer_settings_ui(ScreenshotUi* ui, lv_obj_t* parent) {
|
||||
lv_obj_t* timer_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(timer_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(timer_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(timer_wrapper, 0, 0);
|
||||
ui->timer_wrapper = timer_wrapper;
|
||||
|
||||
lv_obj_t* delay_wrapper = lv_obj_create(timer_wrapper);
|
||||
lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(delay_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(delay_wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW);
|
||||
|
||||
lv_obj_t* delay_label_wrapper = lv_obj_create(delay_wrapper);
|
||||
lv_obj_set_style_border_width(delay_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0);
|
||||
lv_obj_set_size(delay_label_wrapper, 44, 36);
|
||||
lv_obj_t* delay_label = lv_label_create(delay_label_wrapper);
|
||||
lv_label_set_text(delay_label, "Delay:");
|
||||
lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* delay_textarea = lv_textarea_create(delay_wrapper);
|
||||
lv_textarea_set_one_line(delay_textarea, true);
|
||||
lv_textarea_set_accepted_chars(delay_textarea, "0123456789");
|
||||
lv_textarea_set_text(delay_textarea, "10");
|
||||
lv_obj_set_flex_grow(delay_textarea, 1);
|
||||
ui->delay_textarea = delay_textarea;
|
||||
|
||||
lv_obj_t* delay_unit_label_wrapper = lv_obj_create(delay_wrapper);
|
||||
lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0);
|
||||
lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36);
|
||||
lv_obj_t* delay_unit_label = lv_label_create(delay_unit_label_wrapper);
|
||||
lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_label_set_text(delay_unit_label, "seconds");
|
||||
}
|
||||
|
||||
void create_ui(App app, ScreenshotUi* ui, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
create_mode_setting_ui(ui, wrapper);
|
||||
create_path_ui(ui, wrapper);
|
||||
create_timer_settings_ui(ui, wrapper);
|
||||
|
||||
service::gui::keyboard_add_textarea(ui->delay_textarea);
|
||||
service::gui::keyboard_add_textarea(ui->path_textarea);
|
||||
|
||||
update_mode(ui);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/App.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t* mode_dropdown;
|
||||
lv_obj_t* path_textarea;
|
||||
lv_obj_t* start_stop_button_label;
|
||||
lv_obj_t* timer_wrapper;
|
||||
lv_obj_t* delay_textarea;
|
||||
} ScreenshotUi;
|
||||
|
||||
void create_ui(App app, ScreenshotUi* ui, lv_obj_t* parent);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "app/ManifestRegistry.h"
|
||||
#include "Assets.h"
|
||||
#include "Check.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "ui/Toolbar.h"
|
||||
#include "lvgl.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace tt::app::settings {
|
||||
|
||||
static void on_app_pressed(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
const auto* manifest = static_cast<const Manifest*>(lv_event_get_user_data(e));
|
||||
service::loader::start_app(manifest->id, false, Bundle());
|
||||
}
|
||||
}
|
||||
|
||||
static void create_app_widget(const Manifest* manifest, void* parent) {
|
||||
tt_check(parent);
|
||||
auto* list = (lv_obj_t*)parent;
|
||||
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
|
||||
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
|
||||
lv_obj_add_event_cb(btn, &on_app_pressed, LV_EVENT_CLICKED, (void*)manifest);
|
||||
}
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* list = lv_list_create(parent);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
|
||||
auto manifests = app_manifest_registry_get();
|
||||
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
|
||||
for (const auto& manifest: manifests) {
|
||||
if (manifest->type == TypeSettings) {
|
||||
create_app_widget(manifest, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "Settings",
|
||||
.name = "Settings",
|
||||
.icon = TT_ASSETS_APP_ICON_SETTINGS,
|
||||
.type = TypeSystem,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
.on_show = &on_show,
|
||||
.on_hide = nullptr
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "Assets.h"
|
||||
#include "lvgl.h"
|
||||
#include "Tactility.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
namespace tt::app::system_info {
|
||||
|
||||
static size_t get_heap_free() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
#else
|
||||
return 4096 * 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
static size_t get_heap_total() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
|
||||
#else
|
||||
return 8192 * 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
static size_t get_spi_free() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||
#else
|
||||
return 4096 * 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
static size_t get_spi_total() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
|
||||
#else
|
||||
return 8192 * 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void add_memory_bar(lv_obj_t* parent, const char* label, size_t used, size_t total) {
|
||||
lv_obj_t* container = lv_obj_create(parent);
|
||||
lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(container, 0, 0);
|
||||
lv_obj_set_style_border_width(container, 0, 0);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
|
||||
|
||||
lv_obj_t* left_label = lv_label_create(container);
|
||||
lv_label_set_text(left_label, label);
|
||||
lv_obj_set_width(left_label, 60);
|
||||
|
||||
lv_obj_t* bar = lv_bar_create(container);
|
||||
lv_obj_set_flex_grow(bar, 1);
|
||||
|
||||
if (total > 0) {
|
||||
lv_bar_set_range(bar, 0, (int32_t)total);
|
||||
} else {
|
||||
lv_bar_set_range(bar, 0, 1);
|
||||
}
|
||||
|
||||
lv_bar_set_value(bar, (int32_t)used, LV_ANIM_OFF);
|
||||
|
||||
lv_obj_t* bottom_label = lv_label_create(parent);
|
||||
lv_label_set_text_fmt(bottom_label, "%u / %u kB", (used / 1024), (total / 1024));
|
||||
lv_obj_set_width(bottom_label, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
}
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
// This wrapper automatically has its children added vertically underneath eachother
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
|
||||
// Wrapper for the memory usage bars
|
||||
lv_obj_t* memory_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(memory_label, "Memory usage");
|
||||
lv_obj_t* memory_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
add_memory_bar(memory_wrapper, "Heap", get_heap_total() - get_heap_free(), get_heap_total());
|
||||
add_memory_bar(memory_wrapper, "SPI", get_spi_total() - get_spi_free(), get_spi_total());
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
// Build info
|
||||
lv_obj_t* build_info_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(build_info_label, "Build info");
|
||||
lv_obj_t* build_info_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
lv_obj_t* esp_idf_version = lv_label_create(build_info_wrapper);
|
||||
lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "SystemInfo",
|
||||
.name = "System Info",
|
||||
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
|
||||
.type = TypeSystem,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
.on_show = &on_show
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "Log.h"
|
||||
#include "TextViewer.h"
|
||||
#include "lvgl.h"
|
||||
#include "ui/LabelUtils.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
#define TAG "text_viewer"
|
||||
|
||||
namespace tt::app::text_viewer {
|
||||
|
||||
static void on_show(App app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::obj_set_style_no_padding(wrapper);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
|
||||
lv_obj_t* label = lv_label_create(wrapper);
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
const Bundle& bundle = tt_app_get_parameters(app);
|
||||
std::string file_argument;
|
||||
if (bundle.optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) {
|
||||
std::string prefixed_path = "A:" + file_argument;
|
||||
TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str());
|
||||
lvgl::label_set_text_file(label, prefixed_path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "TextViewer",
|
||||
.name = "Text Viewer",
|
||||
.type = TypeHidden,
|
||||
.on_show = &on_show
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define TEXT_VIEWER_FILE_ARGUMENT "file"
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "WifiConnect.h"
|
||||
|
||||
#include "app/App.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "WifiConnectStateUpdating.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "service/wifi/Wifi.h"
|
||||
#include "ui/LvglSync.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
#define TAG "wifi_connect"
|
||||
|
||||
// Forward declarations
|
||||
static void event_callback(const void* message, void* context);
|
||||
|
||||
static void on_connect(const service::wifi::settings::WifiApSettings* ap_settings, bool remember, TT_UNUSED void* parameter) {
|
||||
auto* wifi = static_cast<WifiConnect*>(parameter);
|
||||
state_set_ap_settings(wifi, ap_settings);
|
||||
state_set_connecting(wifi, true);
|
||||
service::wifi::connect(ap_settings, remember);
|
||||
}
|
||||
|
||||
static WifiConnect* wifi_connect_alloc() {
|
||||
auto* wifi = static_cast<WifiConnect*>(malloc(sizeof(WifiConnect)));
|
||||
|
||||
PubSub* wifi_pubsub = service::wifi::get_pubsub();
|
||||
wifi->wifi_subscription = tt_pubsub_subscribe(wifi_pubsub, &event_callback, wifi);
|
||||
wifi->mutex = tt_mutex_alloc(MutexTypeNormal);
|
||||
wifi->state = (WifiConnectState) {
|
||||
.settings = {
|
||||
.ssid = { 0 },
|
||||
.password = { 0 },
|
||||
.auto_connect = false,
|
||||
},
|
||||
.connection_error = false,
|
||||
.is_connecting = false
|
||||
};
|
||||
wifi->bindings = (WifiConnectBindings) {
|
||||
.on_connect_ssid = &on_connect,
|
||||
.on_connect_ssid_context = wifi,
|
||||
};
|
||||
wifi->view_enabled = false;
|
||||
|
||||
return wifi;
|
||||
}
|
||||
|
||||
static void wifi_connect_free(WifiConnect* wifi) {
|
||||
PubSub* wifi_pubsub = service::wifi::get_pubsub();
|
||||
tt_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription);
|
||||
tt_mutex_free(wifi->mutex);
|
||||
|
||||
free(wifi);
|
||||
}
|
||||
|
||||
void lock(WifiConnect* wifi) {
|
||||
tt_assert(wifi);
|
||||
tt_assert(wifi->mutex);
|
||||
tt_mutex_acquire(wifi->mutex, TtWaitForever);
|
||||
}
|
||||
|
||||
void unlock(WifiConnect* wifi) {
|
||||
tt_assert(wifi);
|
||||
tt_assert(wifi->mutex);
|
||||
tt_mutex_release(wifi->mutex);
|
||||
}
|
||||
|
||||
void request_view_update(WifiConnect* wifi) {
|
||||
lock(wifi);
|
||||
if (wifi->view_enabled) {
|
||||
if (lvgl::lock(1000)) {
|
||||
view_update(&wifi->view, &wifi->bindings, &wifi->state);
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to lock lvgl");
|
||||
}
|
||||
}
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
static void event_callback(const void* message, void* context) {
|
||||
auto* event = static_cast<const service::wifi::WifiEvent*>(message);
|
||||
auto* wifi = static_cast<WifiConnect*>(context);
|
||||
switch (event->type) {
|
||||
case service::wifi::WifiEventTypeConnectionFailed:
|
||||
if (wifi->state.is_connecting) {
|
||||
state_set_connecting(wifi, false);
|
||||
state_set_radio_error(wifi, true);
|
||||
request_view_update(wifi);
|
||||
}
|
||||
break;
|
||||
case service::wifi::WifiEventTypeConnectionSuccess:
|
||||
if (wifi->state.is_connecting) {
|
||||
state_set_connecting(wifi, false);
|
||||
service::loader::stop_app();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
request_view_update(wifi);
|
||||
}
|
||||
|
||||
static void app_show(App app, lv_obj_t* parent) {
|
||||
auto* wifi = static_cast<WifiConnect*>(tt_app_get_data(app));
|
||||
|
||||
lock(wifi);
|
||||
wifi->view_enabled = true;
|
||||
view_create(app, wifi, parent);
|
||||
view_update(&wifi->view, &wifi->bindings, &wifi->state);
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
static void app_hide(App app) {
|
||||
auto* wifi = static_cast<WifiConnect*>(tt_app_get_data(app));
|
||||
// No need to lock view, as this is called from within Gui's LVGL context
|
||||
view_destroy(&wifi->view);
|
||||
lock(wifi);
|
||||
wifi->view_enabled = false;
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
static void app_start(App app) {
|
||||
auto* wifi_connect = wifi_connect_alloc();
|
||||
tt_app_set_data(app, wifi_connect);
|
||||
}
|
||||
|
||||
static void app_stop(App app) {
|
||||
auto* wifi = static_cast<WifiConnect*>(tt_app_get_data(app));
|
||||
tt_assert(wifi != nullptr);
|
||||
wifi_connect_free(wifi);
|
||||
tt_app_set_data(app, nullptr);
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "WifiConnect",
|
||||
.name = "Wi-Fi Connect",
|
||||
.icon = LV_SYMBOL_WIFI,
|
||||
.type = TypeSettings,
|
||||
.on_start = &app_start,
|
||||
.on_stop = &app_stop,
|
||||
.on_show = &app_show,
|
||||
.on_hide = &app_hide
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "Mutex.h"
|
||||
#include "WifiConnectBindings.h"
|
||||
#include "WifiConnectState.h"
|
||||
#include "WifiConnectView.h"
|
||||
#include "service/wifi/Wifi.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
typedef struct {
|
||||
PubSubSubscription* wifi_subscription;
|
||||
Mutex* mutex;
|
||||
WifiConnectState state;
|
||||
WifiConnectView view;
|
||||
bool view_enabled;
|
||||
WifiConnectBindings bindings;
|
||||
} WifiConnect;
|
||||
|
||||
void lock(WifiConnect* wifi);
|
||||
|
||||
void unlock(WifiConnect* wifi);
|
||||
|
||||
void view_update(WifiConnect* wifi);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "service/wifi/WifiSettings.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
typedef void (*OnConnectSsid)(const service::wifi::settings::WifiApSettings* settings, bool store, void* context);
|
||||
|
||||
typedef struct {
|
||||
OnConnectSsid on_connect_ssid;
|
||||
void* on_connect_ssid_context;
|
||||
} WifiConnectBindings;
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define WIFI_CONNECT_PARAM_SSID "ssid" // String
|
||||
#define WIFI_CONNECT_PARAM_PASSWORD "password" // String
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "service/wifi/Wifi.h"
|
||||
#include "service/wifi/WifiSettings.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
/**
|
||||
* View's state
|
||||
*/
|
||||
typedef struct {
|
||||
service::wifi::settings::WifiApSettings settings;
|
||||
bool connection_error;
|
||||
bool is_connecting;
|
||||
} WifiConnectState;
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "WifiConnectStateUpdating.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
void state_set_radio_error(WifiConnect* wifi, bool error) {
|
||||
lock(wifi);
|
||||
wifi->state.connection_error = error;
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
void state_set_ap_settings(WifiConnect* wifi, const service::wifi::settings::WifiApSettings* settings) {
|
||||
lock(wifi);
|
||||
memcpy(&(wifi->state.settings), settings, sizeof(service::wifi::settings::WifiApSettings));
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
void state_set_connecting(WifiConnect* wifi, bool is_connecting) {
|
||||
lock(wifi);
|
||||
wifi->state.is_connecting = is_connecting;
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "WifiConnect.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
void state_set_radio_error(WifiConnect* wifi, bool error);
|
||||
void state_set_ap_settings(WifiConnect* wifi, const service::wifi::settings::WifiApSettings* settings);
|
||||
void state_set_connecting(WifiConnect* wifi, bool is_connecting);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,227 @@
|
||||
#include "WifiConnectView.h"
|
||||
|
||||
#include "Log.h"
|
||||
#include "WifiConnect.h"
|
||||
#include "WifiConnectBundle.h"
|
||||
#include "WifiConnectState.h"
|
||||
#include "WifiConnectStateUpdating.h"
|
||||
#include "lvgl.h"
|
||||
#include "service/gui/Gui.h"
|
||||
#include "service/wifi/WifiSettings.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
#define TAG "wifi_connect"
|
||||
|
||||
static void view_set_loading(WifiConnectView* view, bool loading);
|
||||
|
||||
static void reset_errors(WifiConnectView* view) {
|
||||
lv_obj_add_flag(view->password_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(view->ssid_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(view->connection_error, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
static void on_connect(lv_event_t* event) {
|
||||
WifiConnect* wifi = (WifiConnect*)lv_event_get_user_data(event);
|
||||
WifiConnectView* view = &wifi->view;
|
||||
|
||||
state_set_radio_error(wifi, false);
|
||||
reset_errors(view);
|
||||
|
||||
const char* ssid = lv_textarea_get_text(view->ssid_textarea);
|
||||
size_t ssid_len = strlen(ssid);
|
||||
if (ssid_len > TT_WIFI_SSID_LIMIT) {
|
||||
TT_LOG_E(TAG, "SSID too long");
|
||||
lv_label_set_text(view->ssid_error, "SSID too long");
|
||||
lv_obj_remove_flag(view->ssid_error, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* password = lv_textarea_get_text(view->password_textarea);
|
||||
size_t password_len = strlen(password);
|
||||
if (password_len > TT_WIFI_CREDENTIALS_PASSWORD_LIMIT) {
|
||||
TT_LOG_E(TAG, "Password too long");
|
||||
lv_label_set_text(view->password_error, "Password too long");
|
||||
lv_obj_remove_flag(view->password_error, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
bool store = lv_obj_get_state(view->remember_switch) & LV_STATE_CHECKED;
|
||||
|
||||
view_set_loading(view, true);
|
||||
|
||||
service::wifi::settings::WifiApSettings settings;
|
||||
strcpy((char*)settings.password, password);
|
||||
strcpy((char*)settings.ssid, ssid);
|
||||
settings.auto_connect = TT_WIFI_AUTO_CONNECT; // No UI yet, so use global setting:w
|
||||
|
||||
WifiConnectBindings* bindings = &wifi->bindings;
|
||||
bindings->on_connect_ssid(
|
||||
&settings,
|
||||
store,
|
||||
bindings->on_connect_ssid_context
|
||||
);
|
||||
}
|
||||
|
||||
static void view_set_loading(WifiConnectView* view, bool loading) {
|
||||
if (loading) {
|
||||
lv_obj_add_flag(view->connect_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(view->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_state(view->password_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(view->ssid_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(view->remember_switch, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_remove_flag(view->connect_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(view->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_state(view->password_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_remove_state(view->ssid_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_remove_state(view->remember_switch, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void view_create_bottom_buttons(WifiConnect* wifi, lv_obj_t* parent) {
|
||||
WifiConnectView* view = &wifi->view;
|
||||
|
||||
lv_obj_t* button_container = lv_obj_create(parent);
|
||||
lv_obj_set_width(button_container, LV_PCT(100));
|
||||
lv_obj_set_height(button_container, LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_no_padding(button_container);
|
||||
lv_obj_set_style_border_width(button_container, 0, 0);
|
||||
|
||||
view->remember_switch = lv_switch_create(button_container);
|
||||
lv_obj_add_state(view->remember_switch, LV_STATE_CHECKED);
|
||||
lv_obj_align(view->remember_switch, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* remember_label = lv_label_create(button_container);
|
||||
lv_label_set_text(remember_label, "Remember");
|
||||
lv_obj_align(remember_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_align_to(remember_label, view->remember_switch, LV_ALIGN_OUT_RIGHT_MID, 4, 0);
|
||||
|
||||
view->connecting_spinner = lv_spinner_create(button_container);
|
||||
lv_obj_set_size(view->connecting_spinner, 32, 32);
|
||||
lv_obj_align(view->connecting_spinner, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_flag(view->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
view->connect_button = lv_btn_create(button_container);
|
||||
lv_obj_t* connect_label = lv_label_create(view->connect_button);
|
||||
lv_label_set_text(connect_label, "Connect");
|
||||
lv_obj_align(view->connect_button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(view->connect_button, &on_connect, LV_EVENT_CLICKED, wifi);
|
||||
}
|
||||
|
||||
// TODO: Standardize dialogs
|
||||
void view_create(App app, void* wifi, lv_obj_t* parent) {
|
||||
WifiConnect* wifi_connect = (WifiConnect*)wifi;
|
||||
WifiConnectView* view = &wifi_connect->view;
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// SSID
|
||||
|
||||
lv_obj_t* ssid_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(ssid_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(ssid_wrapper, LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_no_padding(ssid_wrapper);
|
||||
lv_obj_set_style_border_width(ssid_wrapper, 0, 0);
|
||||
|
||||
lv_obj_t* ssid_label_wrapper = lv_obj_create(ssid_wrapper);
|
||||
lv_obj_set_width(ssid_label_wrapper, LV_PCT(50));
|
||||
lv_obj_set_height(ssid_label_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_align(ssid_label_wrapper, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_style_border_width(ssid_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_left(ssid_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_right(ssid_label_wrapper, 0, 0);
|
||||
|
||||
lv_obj_t* ssid_label = lv_label_create(ssid_label_wrapper);
|
||||
lv_label_set_text(ssid_label, "Network:");
|
||||
|
||||
view->ssid_textarea = lv_textarea_create(ssid_wrapper);
|
||||
lv_textarea_set_one_line(view->ssid_textarea, true);
|
||||
lv_obj_align(view->ssid_textarea, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_width(view->ssid_textarea, LV_PCT(50));
|
||||
|
||||
view->ssid_error = lv_label_create(wrapper);
|
||||
lv_obj_set_style_text_color(view->ssid_error, lv_color_make(255, 50, 50), 0);
|
||||
lv_obj_add_flag(view->ssid_error, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Password
|
||||
|
||||
lv_obj_t* password_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(password_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(password_wrapper, LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_no_padding(password_wrapper);
|
||||
lv_obj_set_style_border_width(password_wrapper, 0, 0);
|
||||
|
||||
lv_obj_t* password_label_wrapper = lv_obj_create(password_wrapper);
|
||||
lv_obj_set_width(password_label_wrapper, LV_PCT(50));
|
||||
lv_obj_set_height(password_label_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_align_to(password_label_wrapper, password_wrapper, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_style_border_width(password_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_left(password_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_right(password_label_wrapper, 0, 0);
|
||||
|
||||
lv_obj_t* password_label = lv_label_create(password_label_wrapper);
|
||||
lv_label_set_text(password_label, "Password:");
|
||||
|
||||
view->password_textarea = lv_textarea_create(password_wrapper);
|
||||
lv_textarea_set_one_line(view->password_textarea, true);
|
||||
lv_textarea_set_password_mode(view->password_textarea, true);
|
||||
lv_obj_align(view->password_textarea, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_width(view->password_textarea, LV_PCT(50));
|
||||
|
||||
view->password_error = lv_label_create(wrapper);
|
||||
lv_obj_set_style_text_color(view->password_error, lv_color_make(255, 50, 50), 0);
|
||||
lv_obj_add_flag(view->password_error, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Connection error
|
||||
view->connection_error = lv_label_create(wrapper);
|
||||
lv_obj_set_style_text_color(view->connection_error, lv_color_make(255, 50, 50), 0);
|
||||
lv_obj_add_flag(view->connection_error, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
// Bottom buttons
|
||||
view_create_bottom_buttons(wifi_connect, wrapper);
|
||||
|
||||
// Keyboard bindings
|
||||
service::gui::keyboard_add_textarea(view->ssid_textarea);
|
||||
service::gui::keyboard_add_textarea(view->password_textarea);
|
||||
|
||||
// Init from app parameters
|
||||
const Bundle& bundle = tt_app_get_parameters(app);
|
||||
std::string ssid;
|
||||
if (bundle.optString(WIFI_CONNECT_PARAM_SSID, ssid)) {
|
||||
lv_textarea_set_text(view->ssid_textarea, ssid.c_str());
|
||||
}
|
||||
|
||||
std::string password;
|
||||
if (bundle.optString(WIFI_CONNECT_PARAM_PASSWORD, password)) {
|
||||
lv_textarea_set_text(view->password_textarea, password.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void view_destroy(TT_UNUSED WifiConnectView* view) {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
void view_update(
|
||||
WifiConnectView* view,
|
||||
TT_UNUSED WifiConnectBindings* bindings,
|
||||
WifiConnectState* state
|
||||
) {
|
||||
if (state->connection_error) {
|
||||
view_set_loading(view, false);
|
||||
reset_errors(view);
|
||||
lv_label_set_text(view->connection_error, "Connection failed");
|
||||
lv_obj_remove_flag(view->connection_error, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/App.h"
|
||||
#include "WifiConnectBindings.h"
|
||||
#include "WifiConnectState.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace tt::app::wifi_connect {
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t* ssid_textarea;
|
||||
lv_obj_t* ssid_error;
|
||||
lv_obj_t* password_textarea;
|
||||
lv_obj_t* password_error;
|
||||
lv_obj_t* connect_button;
|
||||
lv_obj_t* cancel_button;
|
||||
lv_obj_t* remember_switch;
|
||||
lv_obj_t* connecting_spinner;
|
||||
lv_obj_t* connection_error;
|
||||
lv_group_t* group;
|
||||
} WifiConnectView;
|
||||
|
||||
void view_create(App app, void* wifi, lv_obj_t* parent);
|
||||
void view_update(WifiConnectView* view, WifiConnectBindings* bindings, WifiConnectState* state);
|
||||
void view_destroy(WifiConnectView* view);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "WifiManage.h"
|
||||
|
||||
#include "app/App.h"
|
||||
#include "app/WifiConnect/WifiConnectBundle.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "service/wifi/WifiSettings.h"
|
||||
#include "ui/LvglSync.h"
|
||||
#include "WifiManageStateUpdating.h"
|
||||
#include "WifiManageView.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
#define TAG "wifi_manage"
|
||||
|
||||
// Forward declarations
|
||||
static void event_callback(const void* message, void* context);
|
||||
|
||||
static void on_connect(const char* ssid) {
|
||||
service::wifi::settings::WifiApSettings settings;
|
||||
if (service::wifi::settings::load(ssid, &settings)) {
|
||||
TT_LOG_I(TAG, "Connecting with known credentials");
|
||||
service::wifi::connect(&settings, false);
|
||||
} else {
|
||||
TT_LOG_I(TAG, "Starting connection dialog");
|
||||
Bundle bundle;
|
||||
bundle.putString(WIFI_CONNECT_PARAM_SSID, ssid);
|
||||
bundle.putString(WIFI_CONNECT_PARAM_PASSWORD, "");
|
||||
service::loader::start_app("WifiConnect", false, bundle);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_disconnect() {
|
||||
service::wifi::disconnect();
|
||||
}
|
||||
|
||||
static void on_wifi_toggled(bool enabled) {
|
||||
service::wifi::set_enabled(enabled);
|
||||
}
|
||||
|
||||
static WifiManage* wifi_manage_alloc() {
|
||||
auto* wifi = static_cast<WifiManage*>(malloc(sizeof(WifiManage)));
|
||||
|
||||
wifi->wifi_subscription = nullptr;
|
||||
wifi->mutex = tt_mutex_alloc(MutexTypeNormal);
|
||||
wifi->state = (WifiManageState) {
|
||||
.scanning = service::wifi::is_scanning(),
|
||||
.radio_state = service::wifi::get_radio_state(),
|
||||
.connect_ssid = { 0 },
|
||||
.ap_records = { },
|
||||
.ap_records_count = 0
|
||||
};
|
||||
wifi->view_enabled = false;
|
||||
wifi->bindings = (WifiManageBindings) {
|
||||
.on_wifi_toggled = &on_wifi_toggled,
|
||||
.on_connect_ssid = &on_connect,
|
||||
.on_disconnect = &on_disconnect
|
||||
};
|
||||
|
||||
return wifi;
|
||||
}
|
||||
|
||||
static void wifi_manage_free(WifiManage* wifi) {
|
||||
tt_mutex_free(wifi->mutex);
|
||||
|
||||
free(wifi);
|
||||
}
|
||||
|
||||
void lock(WifiManage* wifi) {
|
||||
tt_assert(wifi);
|
||||
tt_assert(wifi->mutex);
|
||||
tt_mutex_acquire(wifi->mutex, TtWaitForever);
|
||||
}
|
||||
|
||||
void unlock(WifiManage* wifi) {
|
||||
tt_assert(wifi);
|
||||
tt_assert(wifi->mutex);
|
||||
tt_mutex_release(wifi->mutex);
|
||||
}
|
||||
|
||||
void request_view_update(WifiManage* wifi) {
|
||||
lock(wifi);
|
||||
if (wifi->view_enabled) {
|
||||
if (lvgl::lock(1000)) {
|
||||
view_update(&wifi->view, &wifi->bindings, &wifi->state);
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
TT_LOG_E(TAG, "failed to lock lvgl");
|
||||
}
|
||||
}
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
static void wifi_manage_event_callback(const void* message, void* context) {
|
||||
auto* event = (service::wifi::WifiEvent*)message;
|
||||
auto* wifi = (WifiManage*)context;
|
||||
TT_LOG_I(TAG, "Update with state %d", service::wifi::get_radio_state());
|
||||
state_set_radio_state(wifi, service::wifi::get_radio_state());
|
||||
switch (event->type) {
|
||||
case tt::service::wifi::WifiEventTypeScanStarted:
|
||||
state_set_scanning(wifi, true);
|
||||
break;
|
||||
case tt::service::wifi::WifiEventTypeScanFinished:
|
||||
state_set_scanning(wifi, false);
|
||||
state_update_scanned_records(wifi);
|
||||
break;
|
||||
case tt::service::wifi::WifiEventTypeRadioStateOn:
|
||||
if (!service::wifi::is_scanning()) {
|
||||
service::wifi::scan();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
request_view_update(wifi);
|
||||
}
|
||||
|
||||
static void app_show(App app, lv_obj_t* parent) {
|
||||
auto* wifi = (WifiManage*)tt_app_get_data(app);
|
||||
|
||||
PubSub* wifi_pubsub = service::wifi::get_pubsub();
|
||||
wifi->wifi_subscription = tt_pubsub_subscribe(wifi_pubsub, &wifi_manage_event_callback, wifi);
|
||||
|
||||
// State update (it has its own locking)
|
||||
state_set_radio_state(wifi, service::wifi::get_radio_state());
|
||||
state_set_scanning(wifi, service::wifi::is_scanning());
|
||||
state_update_scanned_records(wifi);
|
||||
|
||||
// View update
|
||||
lock(wifi);
|
||||
wifi->view_enabled = true;
|
||||
strcpy((char*)wifi->state.connect_ssid, "Connected"); // TODO update with proper SSID
|
||||
view_create(app, &wifi->view, &wifi->bindings, parent);
|
||||
view_update(&wifi->view, &wifi->bindings, &wifi->state);
|
||||
unlock(wifi);
|
||||
|
||||
service::wifi::WifiRadioState radio_state = service::wifi::get_radio_state();
|
||||
bool can_scan = radio_state == service::wifi::WIFI_RADIO_ON ||
|
||||
radio_state == service::wifi::WIFI_RADIO_CONNECTION_PENDING ||
|
||||
radio_state == service::wifi::WIFI_RADIO_CONNECTION_ACTIVE;
|
||||
if (can_scan && !service::wifi::is_scanning()) {
|
||||
service::wifi::scan();
|
||||
}
|
||||
}
|
||||
|
||||
static void app_hide(App app) {
|
||||
auto* wifi = (WifiManage*)tt_app_get_data(app);
|
||||
lock(wifi);
|
||||
PubSub* wifi_pubsub = service::wifi::get_pubsub();
|
||||
tt_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription);
|
||||
wifi->wifi_subscription = nullptr;
|
||||
wifi->view_enabled = false;
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
static void app_start(App app) {
|
||||
WifiManage* wifi = wifi_manage_alloc();
|
||||
tt_app_set_data(app, wifi);
|
||||
}
|
||||
|
||||
static void app_stop(App app) {
|
||||
auto* wifi = (WifiManage*)tt_app_get_data(app);
|
||||
tt_assert(wifi != nullptr);
|
||||
wifi_manage_free(wifi);
|
||||
tt_app_set_data(app, nullptr);
|
||||
}
|
||||
|
||||
extern const Manifest manifest = {
|
||||
.id = "WifiManage",
|
||||
.name = "Wi-Fi",
|
||||
.icon = LV_SYMBOL_WIFI,
|
||||
.type = TypeSettings,
|
||||
.on_start = &app_start,
|
||||
.on_stop = &app_stop,
|
||||
.on_show = &app_show,
|
||||
.on_hide = &app_hide
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "Mutex.h"
|
||||
#include "WifiManageView.h"
|
||||
#include "service/wifi/Wifi.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
typedef struct {
|
||||
PubSubSubscription* wifi_subscription;
|
||||
Mutex* mutex;
|
||||
WifiManageState state;
|
||||
WifiManageView view;
|
||||
bool view_enabled;
|
||||
WifiManageBindings bindings;
|
||||
} WifiManage;
|
||||
|
||||
void lock(WifiManage* wifi);
|
||||
|
||||
void unlock(WifiManage* wifi);
|
||||
|
||||
void request_view_update(WifiManage* wifi);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
typedef void (*OnWifiToggled)(bool enable);
|
||||
typedef void (*OnConnectSsid)(const char* ssid);
|
||||
typedef void (*OnDisconnect)();
|
||||
|
||||
typedef struct {
|
||||
OnWifiToggled on_wifi_toggled;
|
||||
OnConnectSsid on_connect_ssid;
|
||||
OnDisconnect on_disconnect;
|
||||
} WifiManageBindings;
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "service/wifi/Wifi.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
#define WIFI_SCAN_AP_RECORD_COUNT 16
|
||||
|
||||
/**
|
||||
* View's state
|
||||
*/
|
||||
typedef struct {
|
||||
bool scanning;
|
||||
service::wifi::WifiRadioState radio_state;
|
||||
uint8_t connect_ssid[33];
|
||||
service::wifi::WifiApRecord ap_records[WIFI_SCAN_AP_RECORD_COUNT];
|
||||
uint16_t ap_records_count;
|
||||
} WifiManageState;
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "WifiManage.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
void state_set_scanning(WifiManage* wifi, bool is_scanning) {
|
||||
lock(wifi);
|
||||
wifi->state.scanning = is_scanning;
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
void state_set_radio_state(WifiManage* wifi, service::wifi::WifiRadioState state) {
|
||||
lock(wifi);
|
||||
wifi->state.radio_state = state;
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
void state_update_scanned_records(WifiManage* wifi) {
|
||||
lock(wifi);
|
||||
service::wifi::get_scan_results(
|
||||
wifi->state.ap_records,
|
||||
WIFI_SCAN_AP_RECORD_COUNT,
|
||||
&wifi->state.ap_records_count
|
||||
);
|
||||
unlock(wifi);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "WifiManage.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
void state_set_scanning(WifiManage* wifi, bool is_scanning);
|
||||
void state_set_radio_state(WifiManage* wifi, service::wifi::WifiRadioState state);
|
||||
void state_update_scanned_records(WifiManage* wifi);
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,207 @@
|
||||
#include "WifiManageView.h"
|
||||
|
||||
#include "Log.h"
|
||||
#include "WifiManageState.h"
|
||||
#include "service/statusbar/Statusbar.h"
|
||||
#include "service/wifi/Wifi.h"
|
||||
#include "ui/Style.h"
|
||||
#include "ui/Toolbar.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
#define TAG "wifi_main_view"
|
||||
#define SPINNER_HEIGHT 40
|
||||
|
||||
static void on_enable_switch_changed(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
auto* bindings = static_cast<WifiManageBindings*>(lv_event_get_user_data(event));
|
||||
bindings->on_wifi_toggled(is_on);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_disconnect_pressed(lv_event_t* event) {
|
||||
auto* bindings = static_cast<WifiManageBindings*>(lv_event_get_user_data(event));
|
||||
bindings->on_disconnect();
|
||||
}
|
||||
|
||||
// region Secondary updates
|
||||
|
||||
static void connect(lv_event_t* event) {
|
||||
lv_obj_t* button = lv_event_get_current_target_obj(event);
|
||||
// Assumes that the second child of the button is a label ... risky
|
||||
lv_obj_t* label = lv_obj_get_child(button, 1);
|
||||
// We get the SSID from the button label because it's safer than alloc'ing
|
||||
// our own and passing it as the event data
|
||||
const char* ssid = lv_label_get_text(label);
|
||||
TT_LOG_I(TAG, "Clicked AP: %s", ssid);
|
||||
auto* bindings = static_cast<WifiManageBindings*>(lv_event_get_user_data(event));
|
||||
bindings->on_connect_ssid(ssid);
|
||||
}
|
||||
|
||||
static void create_network_button(WifiManageView* view, WifiManageBindings* bindings, service::wifi::WifiApRecord* record) {
|
||||
const char* ssid = (const char*)record->ssid;
|
||||
const char* icon = service::statusbar::get_status_icon_for_rssi(record->rssi, record->auth_mode != WIFI_AUTH_OPEN);
|
||||
lv_obj_t* ap_button = lv_list_add_btn(
|
||||
view->networks_list,
|
||||
icon,
|
||||
ssid
|
||||
);
|
||||
lv_obj_add_event_cb(ap_button, &connect, LV_EVENT_CLICKED, bindings);
|
||||
}
|
||||
|
||||
static void update_network_list(WifiManageView* view, WifiManageState* state, WifiManageBindings* bindings) {
|
||||
lv_obj_clean(view->networks_list);
|
||||
switch (state->radio_state) {
|
||||
case service::wifi::WIFI_RADIO_ON_PENDING:
|
||||
case service::wifi::WIFI_RADIO_ON:
|
||||
case service::wifi::WIFI_RADIO_CONNECTION_PENDING:
|
||||
case service::wifi::WIFI_RADIO_CONNECTION_ACTIVE: {
|
||||
lv_obj_clear_flag(view->networks_label, LV_OBJ_FLAG_HIDDEN);
|
||||
if (state->ap_records_count > 0) {
|
||||
for (int i = 0; i < state->ap_records_count; ++i) {
|
||||
create_network_button(view, bindings, &state->ap_records[i]);
|
||||
}
|
||||
lv_obj_clear_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
} else if (state->scanning) {
|
||||
lv_obj_add_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_clear_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_t* label = lv_label_create(view->networks_list);
|
||||
lv_label_set_text(label, "No networks found.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case service::wifi::WIFI_RADIO_OFF_PENDING:
|
||||
case service::wifi::WIFI_RADIO_OFF: {
|
||||
lv_obj_add_flag(view->networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(view->networks_label, LV_OBJ_FLAG_HIDDEN);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_scanning(WifiManageView* view, WifiManageState* state) {
|
||||
if (state->radio_state == service::wifi::WIFI_RADIO_ON && state->scanning) {
|
||||
lv_obj_clear_flag(view->scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(view->scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_wifi_toggle(WifiManageView* view, WifiManageState* state) {
|
||||
lv_obj_clear_state(view->enable_switch, LV_STATE_ANY);
|
||||
switch (state->radio_state) {
|
||||
case service::wifi::WIFI_RADIO_ON:
|
||||
case service::wifi::WIFI_RADIO_CONNECTION_PENDING:
|
||||
case service::wifi::WIFI_RADIO_CONNECTION_ACTIVE:
|
||||
lv_obj_add_state(view->enable_switch, LV_STATE_CHECKED);
|
||||
break;
|
||||
case service::wifi::WIFI_RADIO_ON_PENDING:
|
||||
lv_obj_add_state(view->enable_switch, LV_STATE_CHECKED | LV_STATE_DISABLED);
|
||||
break;
|
||||
case service::wifi::WIFI_RADIO_OFF:
|
||||
lv_obj_remove_state(view->enable_switch, LV_STATE_CHECKED | LV_STATE_DISABLED);
|
||||
break;
|
||||
case service::wifi::WIFI_RADIO_OFF_PENDING:
|
||||
lv_obj_remove_state(view->enable_switch, LV_STATE_CHECKED);
|
||||
lv_obj_add_state(view->enable_switch, LV_STATE_DISABLED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_connected_ap(WifiManageView* view, WifiManageState* state, TT_UNUSED WifiManageBindings* bindings) {
|
||||
switch (state->radio_state) {
|
||||
case service::wifi::WIFI_RADIO_CONNECTION_PENDING:
|
||||
case service::wifi::WIFI_RADIO_CONNECTION_ACTIVE:
|
||||
lv_obj_clear_flag(view->connected_ap_container, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_label_set_text(view->connected_ap_label, (const char*)state->connect_ssid);
|
||||
break;
|
||||
default:
|
||||
lv_obj_add_flag(view->connected_ap_container, LV_OBJ_FLAG_HIDDEN);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Secondary updates
|
||||
|
||||
// region Main
|
||||
|
||||
void view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) {
|
||||
view->root = parent;
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Top row: enable/disable
|
||||
lv_obj_t* switch_container = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(switch_container, LV_PCT(100));
|
||||
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_no_padding(switch_container);
|
||||
lvgl::obj_set_style_bg_invisible(switch_container);
|
||||
|
||||
lv_obj_t* enable_label = lv_label_create(switch_container);
|
||||
lv_label_set_text(enable_label, "Wi-Fi");
|
||||
lv_obj_set_align(enable_label, LV_ALIGN_LEFT_MID);
|
||||
|
||||
view->enable_switch = lv_switch_create(switch_container);
|
||||
lv_obj_add_event_cb(view->enable_switch, on_enable_switch_changed, LV_EVENT_ALL, bindings);
|
||||
lv_obj_set_align(view->enable_switch, LV_ALIGN_RIGHT_MID);
|
||||
|
||||
view->connected_ap_container = lv_obj_create(wrapper);
|
||||
lv_obj_set_size(view->connected_ap_container, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_min_height(view->connected_ap_container, SPINNER_HEIGHT, 0);
|
||||
lvgl::obj_set_style_no_padding(view->connected_ap_container);
|
||||
lv_obj_set_style_border_width(view->connected_ap_container, 0, 0);
|
||||
|
||||
view->connected_ap_label = lv_label_create(view->connected_ap_container);
|
||||
lv_obj_align(view->connected_ap_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* disconnect_button = lv_btn_create(view->connected_ap_container);
|
||||
lv_obj_add_event_cb(disconnect_button, &on_disconnect_pressed, LV_EVENT_CLICKED, bindings);
|
||||
lv_obj_t* disconnect_label = lv_label_create(disconnect_button);
|
||||
lv_label_set_text(disconnect_label, "Disconnect");
|
||||
lv_obj_align(disconnect_button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
|
||||
// Networks
|
||||
|
||||
lv_obj_t* networks_header = lv_obj_create(wrapper);
|
||||
lv_obj_set_size(networks_header, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_min_height(networks_header, SPINNER_HEIGHT, 0);
|
||||
lvgl::obj_set_style_no_padding(networks_header);
|
||||
lv_obj_set_style_border_width(networks_header, 0, 0);
|
||||
|
||||
view->networks_label = lv_label_create(networks_header);
|
||||
lv_label_set_text(view->networks_label, "Networks");
|
||||
lv_obj_align(view->networks_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
view->scanning_spinner = lv_spinner_create(networks_header);
|
||||
lv_spinner_set_anim_params(view->scanning_spinner, 1000, 60);
|
||||
lv_obj_set_size(view->scanning_spinner, SPINNER_HEIGHT, SPINNER_HEIGHT);
|
||||
lv_obj_set_style_pad_top(view->scanning_spinner, 4, 0);
|
||||
lv_obj_set_style_pad_bottom(view->scanning_spinner, 4, 0);
|
||||
lv_obj_align_to(view->scanning_spinner, view->networks_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
|
||||
|
||||
view->networks_list = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(view->networks_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(view->networks_list, LV_PCT(100));
|
||||
lv_obj_set_height(view->networks_list, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_top(view->networks_list, 8, 0);
|
||||
lv_obj_set_style_pad_bottom(view->networks_list, 8, 0);
|
||||
}
|
||||
|
||||
void view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state) {
|
||||
update_wifi_toggle(view, state);
|
||||
update_scanning(view, state);
|
||||
update_network_list(view, state, bindings);
|
||||
update_connected_ap(view, state, bindings);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/App.h"
|
||||
#include "WifiManageBindings.h"
|
||||
#include "WifiManageState.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
namespace tt::app::wifi_manage {
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t* root;
|
||||
lv_obj_t* enable_switch;
|
||||
lv_obj_t* scanning_spinner;
|
||||
lv_obj_t* networks_label;
|
||||
lv_obj_t* networks_list;
|
||||
lv_obj_t* connected_ap_container;
|
||||
lv_obj_t* connected_ap_label;
|
||||
} WifiManageView;
|
||||
|
||||
void view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent);
|
||||
void view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state);
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user