Refactor LVGL code into kernel module (#472)

* **New Features**
  * Added a standalone LVGL module and enabled LVGL support in the simulator for richer local UI testing.

* **Refactor**
  * HAL and LVGL split into distinct modules; startup and device attach/detach flows reorganized for clearer lifecycle management.
  * Public APIs tightened with clearer nullability/documentation.

* **Bug Fixes**
  * More consistent LVGL start/stop and device attach/detach behavior for improved stability.
This commit is contained in:
Ken Van Hoeylandt
2026-02-01 22:57:45 +01:00
committed by GitHub
parent 3fe1dc0312
commit 9f721e6655
135 changed files with 1553 additions and 667 deletions
@@ -44,7 +44,7 @@ void addService(const ServiceManifest& manifest, bool autoStart) {
addService(std::make_shared<const ServiceManifest>(manifest), autoStart);
}
std::shared_ptr<const ServiceManifest> _Nullable findManifestById(const std::string& id) {
std::shared_ptr<const ServiceManifest> findManifestById(const std::string& id) {
manifest_mutex.lock();
auto iterator = service_manifest_map.find(id);
auto manifest = iterator != service_manifest_map.end() ? iterator->second : nullptr;
@@ -52,7 +52,7 @@ std::shared_ptr<const ServiceManifest> _Nullable findManifestById(const std::str
return manifest;
}
static std::shared_ptr<ServiceInstance> _Nullable findServiceInstanceById(const std::string& id) {
static std::shared_ptr<ServiceInstance> findServiceInstanceById(const std::string& id) {
manifest_mutex.lock();
auto iterator = service_instance_map.find(id);
auto service = iterator != service_instance_map.end() ? iterator->second : nullptr;
@@ -92,11 +92,11 @@ bool startService(const std::string& id) {
return true;
}
std::shared_ptr<ServiceContext> _Nullable findServiceContextById(const std::string& id) {
std::shared_ptr<ServiceContext> findServiceContextById(const std::string& id) {
return findServiceInstanceById(id);
}
std::shared_ptr<Service> _Nullable findServiceById(const std::string& id) {
std::shared_ptr<Service> findServiceById(const std::string& id) {
auto instance = findServiceInstanceById(id);
return instance != nullptr ? instance->getService() : nullptr;
}
+1 -1
View File
@@ -17,7 +17,7 @@ constexpr bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType
return (now - timeInThePast) >= expireTimeInTicks;
}
GpsService::GpsDeviceRecord* _Nullable GpsService::findGpsRecord(const std::shared_ptr<GpsDevice>& device) {
GpsService::GpsDeviceRecord* GpsService::findGpsRecord(const std::shared_ptr<GpsDevice>& device) {
auto lock = mutex.asScopedLock();
lock.lock();
+64 -46
View File
@@ -40,6 +40,47 @@ void GuiService::onLoaderEvent(LoaderService::Event event) {
int32_t GuiService::guiMain() {
auto service = findServiceById<GuiService>(manifest.id);
if (!lvgl::lock(5000)) {
LOGGER.error("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) {
LOGGER.error("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 (true) {
uint32_t flags = 0;
if (service->threadFlags.wait(GUI_THREAD_FLAG_ALL, false, true, &flags, portMAX_DELAY)) {
@@ -62,6 +103,9 @@ int32_t GuiService::guiMain() {
}
}
service->appRootWidget = nullptr;
service->statusbarWidget = nullptr;
return 0;
}
@@ -87,6 +131,12 @@ void GuiService::redraw() {
// Lock GUI and LVGL
lock();
if (appRootWidget == nullptr) {
LOGGER.warn("No root widget");
unlock();
return;
}
if (lvgl::lock(1000)) {
lv_obj_clean(appRootWidget);
@@ -113,7 +163,7 @@ void GuiService::redraw() {
lv_obj_t* container = createAppViews(appRootWidget);
appToRender->getApp()->onShow(*appToRender, container);
} else {
LOGGER.warn("nothing to draw");
LOGGER.warn("Nothing to draw");
}
// Unlock GUI and LVGL
@@ -126,18 +176,11 @@ void GuiService::redraw() {
}
bool GuiService::onStart(ServiceContext& service) {
auto* screen_root = lv_screen_active();
if (screen_root == nullptr) {
LOGGER.error("No display found");
return false;
}
thread = new Thread(
GUI_TASK_NAME,
4096, // Last known minimum was 2800 for launching desktop
[]() { return guiMain(); }
guiMain
);
thread->setPriority(THREAD_PRIORITY_SERVICE);
const auto loader = findLoaderService();
@@ -146,37 +189,8 @@ bool GuiService::onStart(ServiceContext& service) {
onLoaderEvent(event);
});
lvgl::lock(portMAX_DELAY);
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);
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);
appRootWidget = app_container;
lvgl::unlock();
isStarted = true;
thread->setPriority(THREAD_PRIORITY_SERVICE);
thread->start();
return true;
@@ -192,20 +206,24 @@ void GuiService::onStop(ServiceContext& service) {
appToRender = nullptr;
isStarted = false;
auto task_handle = thread->getTaskHandle();
threadFlags.set(GUI_THREAD_FLAG_EXIT);
thread->join();
delete thread;
unlock();
thread->join();
check(lvgl::lock(1000 / portTICK_PERIOD_MS));
lv_group_delete(keyboardGroup);
lvgl::unlock();
if (lvgl::lock()) {
if (keyboardGroup != nullptr) {
lv_group_delete(keyboardGroup);
keyboardGroup = nullptr;
}
lvgl::unlock();
} else {
LOGGER.error("Failed to lock LVGL during GUI stop");
}
delete thread;
}
void GuiService::requestDraw() {
auto task_handle = thread->getTaskHandle();
threadFlags.set(GUI_THREAD_FLAG_DRAW);
}
+3 -3
View File
@@ -18,7 +18,7 @@
namespace tt::service::loader {
static const auto LOGGER = Logger("Boot");
static const auto LOGGER = Logger("Loader");
constexpr auto LOADER_TIMEOUT = (100 / portTICK_PERIOD_MS);
@@ -297,7 +297,7 @@ void LoaderService::stopAll(const std::string& id) {
});
}
std::shared_ptr<app::AppContext> _Nullable LoaderService::getCurrentAppContext() {
std::shared_ptr<app::AppContext> LoaderService::getCurrentAppContext() {
const auto lock = mutex.asScopedLock();
lock.lock();
if (appStack.empty()) {
@@ -318,7 +318,7 @@ bool LoaderService::isRunning(const std::string& id) const {
return false;
}
std::shared_ptr<LoaderService> _Nullable findLoaderService() {
std::shared_ptr<LoaderService> findLoaderService() {
return service::findServiceById<LoaderService>(manifest.id);
}
@@ -16,7 +16,7 @@ static const auto LOGGER = Logger("ScreenshotService");
extern const ServiceManifest manifest;
std::shared_ptr<ScreenshotService> _Nullable optScreenshotService() {
std::shared_ptr<ScreenshotService> optScreenshotService() {
return service::findServiceById<ScreenshotService>(manifest.id);
}
@@ -90,7 +90,7 @@ static const char* getSdCardStatusIcon(hal::sdcard::SdCardDevice::State state) {
}
}
static _Nullable const char* getPowerStatusIcon() {
static const char* getPowerStatusIcon() {
// TODO: Support multiple power devices?
std::shared_ptr<hal::power::PowerDevice> power;
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&power](const auto& device) {
+2 -2
View File
@@ -62,9 +62,9 @@ public:
// for example: when scanning and you turn off the radio, the scan should probably stop or turning off
// the radio should disable the on/off button in the app as it is pending.
/** @brief The network interface when wifi is started */
esp_netif_t* _Nullable netif = nullptr;
esp_netif_t* netif = nullptr;
/** @brief Scanning results */
wifi_ap_record_t* _Nullable scan_list = nullptr;
wifi_ap_record_t* scan_list = nullptr;
/** @brief The current item count in scan_list (-1 when scan_list is NULL) */
uint16_t scan_list_count = 0;
/** @brief Maximum amount of records to scan (value > 0) */