Touch and display driver subsystems reworked (and more) (#302)

- Refactored `TouchDevice`: it can now start/stop LVGL separately, and it has an optional `TouchDriver` interface
- Refactored `DisplayDevice`: it can now start/stop LVGL separately, and it has an optional `DisplayDriver` interface
- Updated all boards and drivers for above changes
- LVGL can now be stopped and (re)started on the fly
- Fixed issues with restarting Gui and Statusbar services
- Refactored `Gui` service to be class and renamed `Gui` to `GuiService`
- Fixed `Statusbar` service: forgot to deregister one of the icons
- Updated `esp_lcd_st7701` to v1.1.3
- `lv_textarea_create()` now automatically registers the hardware keyboard hooks (by wrapping the function)
- Fixed and updated `tactility.py`
- Cleanup of a lot of code
- `BootInitLvglBegin` and `BootInitLvglEnd` are replaced by `LvglStarted` and `LvglStopped`.
- Introduced `tt::service::State` which is accessible via `tt::service::getState()` (and internally via `ServiceInstance`)
- Started replacing `#define TAG` with `constexpr auto TAG = "..";`
This commit is contained in:
Ken Van Hoeylandt
2025-08-16 20:22:49 +02:00
committed by GitHub
parent 15f4fbfdc6
commit d875ade8cb
127 changed files with 1907 additions and 1605 deletions
+14 -1
View File
@@ -7,6 +7,7 @@
#include <string>
#include <unordered_map>
#include <Tactility/app/AppInstance.h>
namespace tt::service {
@@ -76,7 +77,9 @@ bool startService(const std::string& id) {
service_instance_map[manifest->id] = service_instance;
instance_mutex.unlock();
service_instance->setState(State::Starting);
service_instance->getService()->onStart(*service_instance);
service_instance->setState(State::Started);
TT_LOG_I(TAG, "Started %s", id.c_str());
@@ -96,11 +99,13 @@ bool stopService(const std::string& id) {
TT_LOG_I(TAG, "Stopping %s", id.c_str());
auto service_instance = findServiceInstanceById(id);
if (service_instance == nullptr) {
TT_LOG_W(TAG, "service not running: %s", id.c_str());
TT_LOG_W(TAG, "Service not running: %s", id.c_str());
return false;
}
service_instance->setState(State::Stopping);
service_instance->getService()->onStop(*service_instance);
service_instance->setState(State::Stopped);
instance_mutex.lock();
service_instance_map.erase(id);
@@ -115,4 +120,12 @@ bool stopService(const std::string& id) {
return true;
}
State getState(const std::string& id) {
auto service_instance = findServiceInstanceById(id);
if (service_instance == nullptr) {
return State::Stopped;
}
return service_instance->getState();
}
} // namespace
-178
View File
@@ -1,178 +0,0 @@
#include "Tactility/service/gui/Gui.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/RtosCompat.h>
namespace tt::service::gui {
#define TAG "gui"
// Forward declarations
void redraw(Gui*);
static int32_t guiMain();
Gui* gui = nullptr;
void onLoaderMessage(const void* message, TT_UNUSED void* context) {
auto* event = static_cast<const loader::LoaderEvent*>(message);
if (event->type == loader::LoaderEventTypeApplicationShowing) {
auto app_instance = app::getCurrentAppContext();
showApp(app_instance);
} else if (event->type == loader::LoaderEventTypeApplicationHiding) {
hideApp();
}
}
Gui* gui_alloc() {
auto* instance = new Gui();
tt_check(instance != nullptr);
instance->thread = new Thread(
"gui",
4096, // Last known minimum was 2800 for launching desktop
[]() { return guiMain(); }
);
instance->loader_pubsub_subscription = loader::getPubsub()->subscribe(&onLoaderMessage, instance);
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
instance->keyboardGroup = lv_group_create();
auto* screen_root = lv_scr_act();
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);
instance->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);
instance->appRootWidget = app_container;
lvgl::unlock();
return instance;
}
void gui_free(Gui* instance) {
assert(instance != nullptr);
delete instance->thread;
lv_group_delete(instance->keyboardGroup);
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
lv_group_del(instance->keyboardGroup);
lvgl::unlock();
delete instance;
}
void lock() {
assert(gui);
tt_check(gui->mutex.lock(configTICK_RATE_HZ));
}
void unlock() {
assert(gui);
tt_check(gui->mutex.unlock());
}
void requestDraw() {
assert(gui);
ThreadId thread_id = gui->thread->getId();
Thread::setFlags(thread_id, GUI_THREAD_FLAG_DRAW);
}
void showApp(std::shared_ptr<app::AppContext> app) {
lock();
tt_check(gui->appToRender == nullptr);
gui->appToRender = std::move(app);
unlock();
requestDraw();
}
void hideApp() {
lock();
tt_check(gui->appToRender != 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));
gui->appToRender->getApp()->onHide(*gui->appToRender);
lvgl::unlock();
gui->appToRender = nullptr;
unlock();
}
static int32_t guiMain() {
tt_check(gui);
Gui* local_gui = gui;
while (true) {
uint32_t flags = Thread::awaitFlags(GUI_THREAD_FLAG_ALL, EventFlag::WaitAny, (uint32_t)portMAX_DELAY);
// Process and dispatch draw call
if (flags & GUI_THREAD_FLAG_DRAW) {
Thread::clearFlags(GUI_THREAD_FLAG_DRAW);
redraw(local_gui);
}
if (flags & GUI_THREAD_FLAG_EXIT) {
Thread::clearFlags(GUI_THREAD_FLAG_EXIT);
break;
}
}
return 0;
}
// region AppManifest
class GuiService : public Service {
public:
void onStart(TT_UNUSED ServiceContext& service) override {
assert(gui == nullptr);
gui = gui_alloc();
gui->thread->setPriority(THREAD_PRIORITY_SERVICE);
gui->thread->start();
}
void onStop(TT_UNUSED ServiceContext& service) override {
assert(gui != nullptr);
lock();
ThreadId thread_id = gui->thread->getId();
Thread::setFlags(thread_id, GUI_THREAD_FLAG_EXIT);
gui->thread->join();
delete gui->thread;
unlock();
gui_free(gui);
}
};
extern const ServiceManifest manifest = {
.id = "Gui",
.createService = create<GuiService>
};
// endregion
} // namespace
-75
View File
@@ -1,75 +0,0 @@
#include "Tactility/service/gui/Gui.h"
#include "Tactility/app/AppInstance.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/lvgl/Style.h"
#include <Tactility/Check.h>
#include <Tactility/Log.h>
namespace tt::service::gui {
#define TAG "gui"
static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent) {
lv_obj_send_event(gui->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()) {
gui->keyboard = lv_keyboard_create(parent);
lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
} else {
gui->keyboard = nullptr;
}
return child_container;
}
void redraw(Gui* gui) {
assert(gui);
// Lock GUI and LVGL
lock();
if (lvgl::lock(1000)) {
lv_obj_clean(gui->appRootWidget);
if (gui->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>(gui->appToRender)->getFlags();
if (flags.showStatusbar) {
lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t* container = createAppViews(gui, gui->appRootWidget);
gui->appToRender->getApp()->onShow(*gui->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();
}
} // namespace tt::service::gui
+232
View File
@@ -0,0 +1,232 @@
#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/ServiceRegistry.h>
namespace tt::service::gui {
extern const ServiceManifest manifest;
constexpr const char* TAG = "gui";
// region AppManifest
void GuiService::onLoaderMessage(const void* message, TT_UNUSED void* context) {
auto service = findService();
if (service == nullptr) {
return;
}
auto* event = static_cast<const loader::LoaderEvent*>(message);
if (event->type == loader::LoaderEventTypeApplicationShowing) {
auto app_instance = app::getCurrentAppContext();
service->showApp(app_instance);
} else if (event->type == loader::LoaderEventTypeApplicationHiding) {
service->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(&onLoaderMessage, nullptr);
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
+36 -27
View File
@@ -1,65 +1,74 @@
#include "Tactility/lvgl/Keyboard.h"
#include "Tactility/Check.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/service/gui/Gui.h"
#include "Tactility/service/gui/GuiService.h"
#include <Tactility/TactilityConfig.h>
#include <Tactility/service/espnow/EspNowService.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);
softwareKeyboardShow(target);
lv_obj_scroll_to_view(target, LV_ANIM_ON);
auto service = findService();
if (service != nullptr) {
lv_obj_t* target = lv_event_get_current_target_obj(event);
service->softwareKeyboardShow(target);
lv_obj_scroll_to_view(target, LV_ANIM_ON);
}
}
static void hide_keyboard(TT_UNUSED lv_event_t* event) {
softwareKeyboardHide();
auto service = findService();
if (service != nullptr) {
service->softwareKeyboardHide();
}
}
bool softwareKeyboardIsEnabled() {
bool GuiService::softwareKeyboardIsEnabled() {
return !lvgl::hardware_keyboard_is_available() || TT_CONFIG_FORCE_ONSCREEN_KEYBOARD;
}
void softwareKeyboardShow(lv_obj_t* textarea) {
void GuiService::softwareKeyboardShow(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);
if (isStarted && keyboard != nullptr) {
lv_obj_clear_flag(keyboard, LV_OBJ_FLAG_HIDDEN);
lv_keyboard_set_textarea(keyboard, textarea);
}
unlock();
}
void softwareKeyboardHide() {
void GuiService::softwareKeyboardHide() {
lock();
if (gui->keyboard) {
lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
if (isStarted && keyboard != nullptr) {
lv_obj_add_flag(keyboard, LV_OBJ_FLAG_HIDDEN);
}
unlock();
}
void keyboardAddTextArea(lv_obj_t* textarea) {
void GuiService::keyboardAddTextArea(lv_obj_t* textarea) {
lock();
tt_check(lvgl::lock(0), "lvgl should already be locked before calling this method");
if (softwareKeyboardIsEnabled()) {
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);
if (isStarted) {
tt_check(lvgl::lock(0), "lvgl should already be locked before calling this method");
if (softwareKeyboardIsEnabled()) {
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(keyboardGroup, textarea);
lvgl::software_keyboard_activate(keyboardGroup);
lvgl::unlock();
}
// lv_obj_t auto-remove themselves from the group when they are destroyed (last checked in LVGL 8.3)
lv_group_add_obj(gui->keyboardGroup, textarea);
lvgl::software_keyboard_activate(gui->keyboardGroup);
lvgl::unlock();
unlock();
}
@@ -130,20 +130,18 @@ static _Nullable const char* getPowerStatusIcon() {
class StatusbarService final : public Service {
private:
Mutex mutex;
std::unique_ptr<Timer> updateTimer;
int8_t gps_icon_id = lvgl::statusbar_icon_add();
int8_t gps_icon_id;
bool gps_last_state = false;
int8_t wifi_icon_id = lvgl::statusbar_icon_add();
int8_t wifi_icon_id;
const char* wifi_last_icon = nullptr;
int8_t sdcard_icon_id = lvgl::statusbar_icon_add();
int8_t sdcard_icon_id;
const char* sdcard_last_icon = nullptr;
int8_t power_icon_id = lvgl::statusbar_icon_add();
int8_t power_icon_id;
const char* power_last_icon = nullptr;
std::unique_ptr<service::Paths> paths;
std::unique_ptr<Paths> paths;
void lock() const {
mutex.lock();
@@ -154,7 +152,7 @@ private:
}
void updateGpsIcon() {
auto gps_state = service::gps::findGpsService()->getState();
auto gps_state = gps::findGpsService()->getState();
bool show_icon = (gps_state == gps::State::OnPending) || (gps_state == gps::State::On);
if (gps_last_state != show_icon) {
if (show_icon) {
@@ -199,7 +197,7 @@ private:
}
void updateSdCardIcon() {
auto sdcard = tt::hal::getConfiguration()->sdcard;
auto sdcard = hal::getConfiguration()->sdcard;
if (sdcard != nullptr) {
auto state = sdcard->getState();
if (state != hal::sdcard::SdCardDevice::State::Unknown) {
@@ -229,10 +227,18 @@ private:
public:
~StatusbarService() final {
StatusbarService() {
gps_icon_id = lvgl::statusbar_icon_add();
sdcard_icon_id = lvgl::statusbar_icon_add();
wifi_icon_id = lvgl::statusbar_icon_add();
power_icon_id = lvgl::statusbar_icon_add();
}
~StatusbarService() override {
lvgl::statusbar_icon_remove(wifi_icon_id);
lvgl::statusbar_icon_remove(sdcard_icon_id);
lvgl::statusbar_icon_remove(power_icon_id);
lvgl::statusbar_icon_remove(gps_icon_id);
}
void onStart(ServiceContext& serviceContext) override {
@@ -245,7 +251,7 @@ public:
assert(service);
onUpdate(service);
updateTimer = std::make_unique<Timer>(Timer::Type::Periodic, [service]() {
updateTimer = std::make_unique<Timer>(Timer::Type::Periodic, [service] {
onUpdate(service);
});