f9453d8956
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`
346 lines
11 KiB
C++
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
|