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