- Add app path get() functions to `TactilityC` - Improved `Dispatcher` and `DispatcherThread` - Improved `PubSub` (type safety) - Created test for `DispatcherThread` and `PubSub` - Save properties files on app exit (various apps) by posting it to the main dispatcher (fixes UI hanging briefly on app exit) - Fixed bug with `SystemSettings` being read from the wrong file path. - `loadPropertiesFile()` now uses `file::readLines()` instead of doing that manually - Increased timer task stack size (required due to issues when reading a properties file for the very first time) - General cleanup - Created `EstimatedPower` driver that uses an ADC pin to measure voltage and estimate the battery charge that is left. - Cleanup of T-Deck board (updated to new style)
232 lines
6.7 KiB
C++
232 lines
6.7 KiB
C++
#include "Tactility/service/gui/GuiService.h"
|
|
#include "Tactility/lvgl/LvglSync.h"
|
|
#include "Tactility/lvgl/Statusbar.h"
|
|
#include "Tactility/lvgl/Style.h"
|
|
#include "Tactility/service/loader/Loader.h"
|
|
|
|
#include <Tactility/Tactility.h>
|
|
#include <Tactility/app/AppInstance.h>
|
|
#include <Tactility/service/ServiceRegistration.h>
|
|
|
|
namespace tt::service::gui {
|
|
|
|
extern const ServiceManifest manifest;
|
|
|
|
constexpr auto* TAG = "GuiService";
|
|
|
|
// region AppManifest
|
|
|
|
void GuiService::onLoaderEvent(loader::LoaderEvent event) {
|
|
if (event == loader::LoaderEvent::ApplicationShowing) {
|
|
auto app_instance = app::getCurrentAppContext();
|
|
showApp(app_instance);
|
|
} else if (event == loader::LoaderEvent::ApplicationHiding) {
|
|
hideApp();
|
|
}
|
|
}
|
|
|
|
int32_t GuiService::guiMain() {
|
|
while (true) {
|
|
uint32_t flags = Thread::awaitFlags(GUI_THREAD_FLAG_ALL, EventFlag::WaitAny, (uint32_t)portMAX_DELAY);
|
|
|
|
// When service not started or starting -> exit
|
|
State service_state = getState(manifest.id);
|
|
if (service_state != State::Started && service_state != State::Starting) {
|
|
break;
|
|
}
|
|
|
|
// Process and dispatch draw call
|
|
if (flags & GUI_THREAD_FLAG_DRAW) {
|
|
Thread::clearFlags(GUI_THREAD_FLAG_DRAW);
|
|
auto service = findService();
|
|
if (service != nullptr) {
|
|
service->redraw();
|
|
}
|
|
}
|
|
|
|
if (flags & GUI_THREAD_FLAG_EXIT) {
|
|
Thread::clearFlags(GUI_THREAD_FLAG_EXIT);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
lv_obj_t* GuiService::createAppViews(lv_obj_t* parent) {
|
|
lv_obj_send_event(statusbarWidget, LV_EVENT_DRAW_MAIN, nullptr);
|
|
lv_obj_t* child_container = lv_obj_create(parent);
|
|
lv_obj_set_style_pad_all(child_container, 0, 0);
|
|
lv_obj_set_width(child_container, LV_PCT(100));
|
|
lv_obj_set_flex_grow(child_container, 1);
|
|
|
|
if (softwareKeyboardIsEnabled()) {
|
|
keyboard = lv_keyboard_create(parent);
|
|
lv_obj_add_flag(keyboard, LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
keyboard = nullptr;
|
|
}
|
|
|
|
return child_container;
|
|
}
|
|
|
|
void GuiService::redraw() {
|
|
// Lock GUI and LVGL
|
|
lock();
|
|
|
|
if (lvgl::lock(1000)) {
|
|
lv_obj_clean(appRootWidget);
|
|
|
|
if (appToRender != nullptr) {
|
|
|
|
// Create a default group which adds all objects automatically,
|
|
// and assign all indevs to it.
|
|
// This enables navigation with limited input, such as encoder wheels.
|
|
lv_group_t* group = lv_group_create();
|
|
auto* indev = lv_indev_get_next(nullptr);
|
|
while (indev) {
|
|
lv_indev_set_group(indev, group);
|
|
indev = lv_indev_get_next(indev);
|
|
}
|
|
lv_group_set_default(group);
|
|
|
|
app::Flags flags = std::static_pointer_cast<app::AppInstance>(appToRender)->getFlags();
|
|
if (flags.showStatusbar) {
|
|
lv_obj_remove_flag(statusbarWidget, LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
lv_obj_add_flag(statusbarWidget, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
lv_obj_t* container = createAppViews(appRootWidget);
|
|
appToRender->getApp()->onShow(*appToRender, container);
|
|
} else {
|
|
TT_LOG_W(TAG, "nothing to draw");
|
|
}
|
|
|
|
// Unlock GUI and LVGL
|
|
lvgl::unlock();
|
|
} else {
|
|
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
|
|
}
|
|
|
|
unlock();
|
|
}
|
|
|
|
void GuiService::onStart(TT_UNUSED ServiceContext& service) {
|
|
thread = new Thread(
|
|
"gui",
|
|
4096, // Last known minimum was 2800 for launching desktop
|
|
[]() { return guiMain(); }
|
|
);
|
|
|
|
loader_pubsub_subscription = loader::getPubsub()->subscribe([this](auto event) {
|
|
onLoaderEvent(event);
|
|
});
|
|
|
|
|
|
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
|
|
keyboardGroup = lv_group_create();
|
|
auto* screen_root = lv_screen_active();
|
|
assert(screen_root != nullptr);
|
|
|
|
lvgl::obj_set_style_bg_blacken(screen_root);
|
|
|
|
lv_obj_t* vertical_container = lv_obj_create(screen_root);
|
|
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_all(vertical_container, 0, 0);
|
|
lv_obj_set_style_pad_gap(vertical_container, 0, 0);
|
|
lvgl::obj_set_style_bg_blacken(vertical_container);
|
|
|
|
statusbarWidget = lvgl::statusbar_create(vertical_container);
|
|
|
|
auto* app_container = lv_obj_create(vertical_container);
|
|
lv_obj_set_style_pad_all(app_container, 0, 0);
|
|
lv_obj_set_style_border_width(app_container, 0, 0);
|
|
lvgl::obj_set_style_bg_blacken(app_container);
|
|
lv_obj_set_width(app_container, LV_PCT(100));
|
|
lv_obj_set_flex_grow(app_container, 1);
|
|
lv_obj_set_flex_flow(app_container, LV_FLEX_FLOW_COLUMN);
|
|
|
|
appRootWidget = app_container;
|
|
|
|
lvgl::unlock();
|
|
|
|
isStarted = true;
|
|
|
|
thread->setPriority(THREAD_PRIORITY_SERVICE);
|
|
thread->start();
|
|
}
|
|
|
|
void GuiService::onStop(TT_UNUSED ServiceContext& service) {
|
|
lock();
|
|
|
|
loader::getPubsub()->unsubscribe(loader_pubsub_subscription);
|
|
|
|
appToRender = nullptr;
|
|
isStarted = false;
|
|
|
|
ThreadId thread_id = thread->getId();
|
|
Thread::setFlags(thread_id, GUI_THREAD_FLAG_EXIT);
|
|
thread->join();
|
|
delete thread;
|
|
|
|
unlock();
|
|
|
|
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
|
|
lv_group_delete(keyboardGroup);
|
|
lvgl::unlock();
|
|
}
|
|
|
|
void GuiService::requestDraw() {
|
|
ThreadId thread_id = thread->getId();
|
|
Thread::setFlags(thread_id, GUI_THREAD_FLAG_DRAW);
|
|
}
|
|
|
|
void GuiService::showApp(std::shared_ptr<app::AppContext> app) {
|
|
lock();
|
|
if (!isStarted) {
|
|
TT_LOG_W(TAG, "Failed to show app %s: GUI not started", app->getManifest().id.c_str());
|
|
} else {
|
|
// Ensure previous app triggers onHide() logic
|
|
if (appToRender != nullptr) {
|
|
hideApp();
|
|
}
|
|
appToRender = std::move(app);
|
|
}
|
|
unlock();
|
|
requestDraw();
|
|
}
|
|
|
|
void GuiService::hideApp() {
|
|
lock();
|
|
if (!isStarted) {
|
|
TT_LOG_W(TAG, "Failed to hide app: GUI not started");
|
|
} else if (appToRender == nullptr) {
|
|
TT_LOG_W(TAG, "hideApp() called but no app is currently shown");
|
|
} else {
|
|
// 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));
|
|
appToRender->getApp()->onHide(*appToRender);
|
|
lvgl::unlock();
|
|
appToRender = nullptr;
|
|
}
|
|
unlock();
|
|
}
|
|
|
|
std::shared_ptr<GuiService> findService() {
|
|
return std::static_pointer_cast<GuiService>(
|
|
findServiceById(manifest.id)
|
|
);
|
|
}
|
|
|
|
extern const ServiceManifest manifest = {
|
|
.id = "Gui",
|
|
.createService = create<GuiService>
|
|
};
|
|
|
|
// endregion
|
|
|
|
} // namespace
|