C++ refactoring (#91)

This commit is contained in:
Ken Van Hoeylandt
2024-11-26 17:51:05 +01:00
committed by GitHub
parent d06137ba76
commit ca5d4b8226
159 changed files with 904 additions and 1086 deletions
+165
View File
@@ -0,0 +1,165 @@
#include "Tactility.h"
#include "service/gui/Gui_i.h"
#include "service/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* app = event->app_showing.app;
const app::Manifest& app_manifest = app::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 != nullptr);
instance->thread = new Thread(
"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);
delete 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 = gui->thread->getId();
thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW);
}
void show_app(app::App app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) {
lock();
tt_check(gui->app_view_port == nullptr);
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 != nullptr);
// 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();
gui->thread->setPriority(THREAD_PRIORITY_SERVICE);
gui->thread->start();
}
static void stop(TT_UNUSED Service& service) {
lock();
ThreadId thread_id = gui->thread->getId();
thread_flags_set(thread_id, GUI_THREAD_FLAG_EXIT);
gui->thread->join();
delete gui->thread;
unlock();
gui_free(gui);
}
extern const Manifest manifest = {
.id = "Gui",
.onStart = &start,
.onStop = &stop
};
// endregion
} // namespace
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "app/App.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 app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide);
/**
* Hide the current app's viewport.
* Does not request a re-draw because after hiding the current app,
* we always show the previous app, and there is always at least 1 app running.
*/
void hide_app();
/**
* Show the on-screen keyboard.
* @param textarea the textarea to focus the input for
*/
void keyboard_show(lv_obj_t* textarea);
/**
* Hide the on-screen keyboard.
* Has no effect when the keyboard is not visible.
*/
void keyboard_hide();
/**
* The on-screen keyboard is only shown when both of these conditions are true:
* - there is no hardware keyboard
* - TT_CONFIG_FORCE_ONSCREEN_KEYBOARD is set to true in tactility_config.h
* @return if we should show a on-screen keyboard for text input inside our apps
*/
bool keyboard_is_enabled();
/**
* Glue code for the on-screen keyboard and the hardware keyboard:
* - Attach automatic hide/show parameters for the on-screen keyboard.
* - Registers the textarea to the default lv_group_t for hardware keyboards.
* @param textarea
*/
void keyboard_add_textarea(lv_obj_t* textarea);
} // namespace
+68
View File
@@ -0,0 +1,68 @@
#include "Check.h"
#include "Log.h"
#include "service/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 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
app::Flags flags = app::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 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
+65
View File
@@ -0,0 +1,65 @@
#include "Check.h"
#include "TactilityConfig.h"
#include "service/gui/Gui_i.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
+44
View File
@@ -0,0 +1,44 @@
#include "ViewPort.h"
#include "Check.h"
#include "service/gui/ViewPort_i.h"
#include "ui/Style.h"
namespace tt::service::gui {
#define TAG "viewport"
ViewPort* view_port_alloc(
app::App app,
ViewPortShowCallback on_show,
ViewPortHideCallback on_hide
) {
auto* view_port = static_cast<ViewPort*>(malloc(sizeof(ViewPort)));
view_port->app = app;
view_port->on_show = on_show;
view_port->on_hide = on_hide;
return view_port;
}
void view_port_free(ViewPort* view_port) {
tt_assert(view_port);
free(view_port);
}
void view_port_show(ViewPort* view_port, lv_obj_t* parent) {
tt_assert(view_port);
tt_assert(parent);
if (view_port->on_show) {
lvgl::obj_set_style_no_padding(parent);
view_port->on_show(view_port->app, parent);
}
}
void view_port_hide(ViewPort* view_port) {
tt_assert(view_port);
if (view_port->on_hide) {
view_port->on_hide(view_port->app);
}
}
} // namespace
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "app/App.h"
#include "lvgl.h"
namespace tt::service::gui {
/** ViewPort Draw callback
* @warning called from GUI thread
*/
typedef void (*ViewPortShowCallback)(app::App app, lv_obj_t* parent);
typedef void (*ViewPortHideCallback)(app::App app);
// TODO: Move internally, use handle publicly
typedef struct {
app::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 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
+371
View File
@@ -0,0 +1,371 @@
#include "Tactility.h"
#include "app/Manifest.h"
#include "app/ManifestRegistry.h"
#include "service/Manifest.h"
#include "service/gui/Gui.h"
#include "service/loader/Loader_i.h"
#ifdef ESP_PLATFORM
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#else
#include "FreeRTOS.h"
#include "ui/LvglSync.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 = new Thread(
"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) * APP_STACK_SIZE);
return loader_singleton;
}
static void loader_free() {
tt_assert(loader_singleton != nullptr);
delete 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);
EventFlag* event_flag = blocking ? new EventFlag() : nullptr;
if (event_flag != nullptr) {
message.setApiLock(event_flag);
}
loader_singleton->queue.put(&message, TtWaitForever);
if (event_flag != nullptr) {
/* TODO: Check if task id is not the LVGL one,
because otherwise this fails as the apps starting logic will try to lock lvgl
to update the UI and fail. */
event_flag->wait(TT_API_LOCK_EVENT);
delete event_flag;
}
return result.value;
}
void stop_app() {
tt_check(loader_singleton);
LoaderMessage message(LoaderMessageTypeAppStop);
loader_singleton->queue.put(&message, TtWaitForever);
}
app::App _Nullable get_current_app() {
tt_assert(loader_singleton);
loader_lock();
app::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(app::State state) {
switch (state) {
case app::StateInitial:
return "initial";
case app::StateStarted:
return "started";
case app::StateShowing:
return "showing";
case app::StateHiding:
return "hiding";
case app::StateStopped:
return "stopped";
default:
return "?";
}
}
static void app_transition_to_state(app::App app, app::State state) {
const app::Manifest& manifest = app::tt_app_get_manifest(app);
const app::State old_state = app::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 app::StateInitial:
tt_app_set_state(app, app::StateInitial);
break;
case app::StateStarted:
if (manifest.on_start != nullptr) {
manifest.on_start(app);
}
tt_app_set_state(app, app::StateStarted);
break;
case app::StateShowing: {
LoaderEvent event_showing = {
.type = LoaderEventTypeApplicationShowing,
.app_showing = {
.app = static_cast<app::App*>(app)
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_showing);
tt_app_set_state(app, app::StateShowing);
break;
}
case app::StateHiding: {
LoaderEvent event_hiding = {
.type = LoaderEventTypeApplicationHiding,
.app_hiding = {
.app = static_cast<app::App*>(app)
}
};
tt_pubsub_publish(loader_singleton->pubsub_external, &event_hiding);
tt_app_set_state(app, app::StateHiding);
break;
}
case app::StateStopped:
if (manifest.on_stop) {
manifest.on_stop(app);
}
app::tt_app_set_data(app, nullptr);
tt_app_set_state(app, app::StateStopped);
break;
}
}
static LoaderStatus loader_do_start_app_with_manifest(
const app::Manifest* 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 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, app::StateInitial);
app_transition_to_state(app, app::StateStarted);
// We might have to hide the previous app first
if (previous_index != -1) {
app::App previous_app = loader_singleton->app_stack[previous_index];
app_transition_to_state(previous_app, app::StateHiding);
}
app_transition_to_state(app, app::StateShowing);
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*>(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 app::Manifest* manifest = app::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 app_to_stop = loader_singleton->app_stack[current_app_index];
const app::Manifest& manifest = app::tt_app_get_manifest(app_to_stop);
app_transition_to_state(app_to_stop, app::StateHiding);
app_transition_to_state(app_to_stop, app::StateStopped);
app::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 app_to_resume = loader_singleton->app_stack[loader_singleton->app_stack_index];
app_transition_to_state(app_to_resume, app::StateShowing);
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) {
message.api_lock->set(TT_API_LOCK_EVENT);
}
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();
loader_singleton->thread->setPriority(THREAD_PRIORITY_SERVICE);
loader_singleton->thread->start();
}
static void loader_stop(TT_UNUSED Service& service) {
tt_check(loader_singleton != nullptr);
// Send stop signal to thread and wait for thread to finish
loader_lock();
LoaderMessage message(LoaderMessageTypeServiceStop);
loader_singleton->queue.put(&message, TtWaitForever);
loader_unlock();
loader_singleton->thread->join();
delete loader_singleton->thread;
loader_free();
loader_singleton = nullptr;
}
extern const Manifest manifest = {
.id = "Loader",
.onStart = &loader_start,
.onStop = &loader_stop
};
// endregion
} // namespace
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include "app/Manifest.h"
#include "Bundle.h"
#include "Pubsub.h"
#include "service/Manifest.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* app;
} LoaderEventAppStarted;
typedef struct {
app::App* app;
} LoaderEventAppShowing;
typedef struct {
app::App* app;
} LoaderEventAppHiding;
typedef struct {
const app::Manifest* 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::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/Service.h"
#include "service/ServiceRegistry.h"
#include "TactilityCore.h"
namespace tt::service::screenshot {
#define TAG "sdcard_service"
extern const Manifest 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 = findServiceById(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 = findServiceById(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 = findServiceById(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 = findServiceById(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 Manifest manifest = {
.id = "Screenshot",
.onStart = &on_start,
.onStop = &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/App.h"
#include "Mutex.h"
#include "TactilityCore.h"
#include "Thread.h"
#include "service/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::App _Nullable app = loader::get_current_app();
if (app) {
const app::Manifest& manifest = app::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 = new Thread(
"screenshot",
8192,
&screenshot_task,
data
);
data->thread->start();
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);
data->thread->join();
task_lock(data);
delete 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,218 @@
#include "Assets.h"
#include "hal/Power.h"
#include "hal/sdcard/Sdcard.h"
#include "Mutex.h"
#include "service/Service.h"
#include "service/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::getState();
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 = getConfiguration()->hardware->power;
if (power != nullptr) {
uint8_t charge = power->getChargeLevel();
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 = new Thread(),
.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);
delete 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);
data->thread->setCallback(service_main, data);
data->thread->setPriority(Thread::PriorityLow);
data->thread->setStackSize(3000);
data->thread->start();
}
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);
data->thread->join();
service_data_free(data);
}
extern const Manifest manifest = {
.id = "Statusbar",
.onStart = &on_start,
.onStop = &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