Files
tactility/Tactility/Source/service/gui/GuiService.cpp
Ken Van Hoeylandt f9453d8956 Migrate devices, create drivers, other improvements & fixes (#574)
Migrated devices to kernel drivers:

- guition-jc3248w535c
- m5stack-core2
- m5stack-cores3
- m5stack-papers3

New drivers:

- axp192-module
- axp2101-module

Fixes:

- Fix SD card LDO for P4 devices
- Updated PowerOff app to work with kernel displays
- Fix for `lvgl_try_lock()` timing
- Fix for touch events with slow updating displays

Improvements:

- `GuiService` now keeps trying to lock to prevent silent failures caused by drivers.
- `GuiService` now uses lvgl-module calls for locking/unlocking
- display driver now has capability `DISPLAY_CAPABILITY_SLOW_REFRESH`
2026-07-19 00:39:26 +02:00

346 lines
11 KiB
C++

#include <Tactility/service/gui/GuiService.h>
#include <cstring>
#include <Tactility/LogMessages.h>
#include <Tactility/Tactility.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/UsbHidInput.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/loader/Loader.h>
#include <tactility/log.h>
#include <tactility/lvgl_module.h>
namespace tt::service::gui {
extern const ServiceManifest manifest;
constexpr auto* TAG = "GuiService";
using namespace loader;
constexpr auto* GUI_TASK_NAME = "gui";
void warnIfRunningOnGuiTask(const char* context) {
const char* task_name = pcTaskGetName(nullptr);
if (strcmp(GUI_TASK_NAME, task_name) == 0) {
LOG_W(TAG, "%s shouldn't run on the GUI task", context);
}
}
namespace {
enum class GuiDispatchType { Show, Hide, Exit };
struct GuiDispatchItem {
GuiService* service;
GuiDispatchType type;
std::shared_ptr<app::AppInstance> appInstance; // only used for Show
};
} // namespace
// region AppManifest
void GuiService::onGuiDispatch(void* context) {
std::unique_ptr<GuiDispatchItem> item(static_cast<GuiDispatchItem*>(context));
switch (item->type) {
case GuiDispatchType::Show:
item->service->showApp(item->appInstance);
break;
case GuiDispatchType::Hide:
item->service->hideApp();
break;
case GuiDispatchType::Exit:
item->service->exitRequested = true;
break;
}
}
void GuiService::onLoaderEvent(LoaderService::Event event) {
GuiDispatchItem* item;
if (event == LoaderService::Event::ApplicationShowing) {
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance};
} else if (event == LoaderService::Event::ApplicationHiding) {
// hideDoneSem is a binary semaphore signaled by every hideApp() completion,
// including the one showApp() triggers internally (GuiDispatchType::Show, when an
// app is already being shown) - that release has no waiter and leaves a stale
// permit sitting available. Drain it before dispatching, or the acquire() below
// could consume that leftover permit instead of the one this specific Hide
// dispatch is about to produce, letting Destroyed run before the real onHide()
// for this app has finished.
hideDoneSem.acquire(0);
item = new GuiDispatchItem{this, GuiDispatchType::Hide, nullptr};
} else {
return;
}
if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) {
LOG_E(TAG, "Failed to dispatch gui event");
delete item;
return;
}
if (event == LoaderService::Event::ApplicationHiding) {
// Block here (still on the Loader thread, inside publish()'s synchronous
// subscriber call) until hideApp() has actually run to completion on the GUI
// task. LoaderService::transitionAppToState(Hiding) must not return - and
// therefore the Destroyed transition right after it, which unloads an ELF app's
// code, must not run - until App::onHide() has fully finished. Bounded so a stuck
// GUI task can't wedge app shutdown forever.
if (!hideDoneSem.acquire(pdMS_TO_TICKS(5000))) {
LOG_E(TAG, "Timed out waiting for hideApp() to complete");
}
}
}
int32_t GuiService::guiMain() {
auto service = findServiceById<GuiService>(manifest.id);
if (!lvgl_try_lock(5000)) {
LOG_E(TAG, "LVGL guiMain start failed as LVGL couldn't be locked");
return 0;
}
// The screen root is created in the main task instead of during onStart because
// it allows onStart() to succeed faster and allows widget creation to happen in the background
auto* screen_root = lv_screen_active();
if (screen_root == nullptr) {
LOG_E(TAG, "No display found, exiting GUI task");
lvgl_unlock();
return 0;
}
service->keyboardGroup = lv_group_create();
lv_obj_set_style_border_width(screen_root, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(screen_root, 0, LV_STATE_DEFAULT);
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, LV_STATE_DEFAULT);
lv_obj_set_style_pad_gap(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(vertical_container, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_border_width(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(vertical_container, 0, LV_STATE_DEFAULT);
service->statusbarWidget = lvgl::statusbar_create(vertical_container);
auto* app_container = lv_obj_create(vertical_container);
lv_obj_set_style_pad_all(app_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(app_container, 0, LV_STATE_DEFAULT);
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);
service->appRootWidget = app_container;
lvgl_unlock();
while (!service->exitRequested) {
dispatcher_consume(service->dispatcher);
}
service->appRootWidget = nullptr;
service->statusbarWidget = nullptr;
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, LV_STATE_DEFAULT);
lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_style_border_width(child_container, 0, LV_STATE_DEFAULT);
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 (appRootWidget == nullptr) {
LOG_W(TAG, "No root widget");
unlock();
return;
}
while (!lvgl_try_lock(1000)) {
LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "GuiService LVGL");
}
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.hideStatusbar) {
lv_obj_add_flag(statusbarWidget, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_remove_flag(statusbarWidget, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t* container = createAppViews(appRootWidget);
appToRender->getApp()->onShow(*appToRender, container);
} else {
LOG_W(TAG, "Nothing to draw");
}
lvgl_unlock();
unlock();
}
bool GuiService::onStart(ServiceContext& service) {
exitRequested = false;
dispatcher = dispatcher_alloc();
thread = new Thread(
GUI_TASK_NAME,
4096, // Last known minimum was 2800 for launching desktop
guiMain
);
thread->setPriority(THREAD_PRIORITY_SERVICE);
const auto loader = findLoaderService();
assert(loader != nullptr);
loader_pubsub_subscription = loader->getPubsub()->subscribe([this](auto event) {
onLoaderEvent(event);
});
isStarted = true;
lvgl::startUsbHidInput();
thread->start();
return true;
}
void GuiService::onStop(ServiceContext& service) {
lvgl::stopUsbHidInput();
lock();
const auto loader = findLoaderService();
assert(loader != nullptr);
loader->getPubsub()->unsubscribe(loader_pubsub_subscription);
appToRender = nullptr;
isStarted = false;
unlock();
auto* exit_item = new GuiDispatchItem{this, GuiDispatchType::Exit, nullptr};
if (dispatcher_dispatch(dispatcher, exit_item, onGuiDispatch) != ERROR_NONE) {
LOG_E(TAG, "Failed to dispatch gui exit event");
check(false, "Failed to dispatch exit signal to thread.");
delete exit_item;
}
thread->join();
lvgl_lock();
if (keyboardGroup != nullptr) {
lv_group_delete(keyboardGroup);
keyboardGroup = nullptr;
}
lvgl_unlock();
delete thread;
dispatcher_free(dispatcher);
dispatcher = nullptr;
}
void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) {
LOG_E(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
return;
}
if (appToRender != nullptr && appToRender->getLaunchId() == app->getLaunchId()) {
LOG_W(TAG, "Already showing %s", app->getManifest().appId.c_str());
return;
}
LOG_I(TAG, "Showing %s", app->getManifest().appId.c_str());
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
redraw();
}
void GuiService::hideApp() {
// Signals hideDoneSem on every return path (including the early-return guards below) -
// onLoaderEvent() blocks on this to know App::onHide() has actually finished before
// Loader proceeds to destroy the app (see hideDoneSem's declaration for why).
struct SignalOnExit {
Semaphore& sem;
~SignalOnExit() { sem.release(); }
} signal_on_exit { hideDoneSem };
auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) {
LOG_E(TAG, "Failed to hide app: GUI not started");
return;
}
if (appToRender == nullptr) {
LOG_W(TAG, "hideApp() called but no app is currently shown");
return;
}
// 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)
lvgl_lock();
appToRender->getApp()->onHide(*appToRender);
lvgl_unlock();
appToRender = nullptr;
}
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