Files
pyxis/lib/tdeck_ui/UI/LVGL/LVGLInit.cpp
torlando-tech ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05:00

331 lines
8.1 KiB
C++

// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#include "LVGLInit.h"
#ifdef ARDUINO
#include "esp_task_wdt.h"
#include "Log.h"
#include "../../Hardware/TDeck/Display.h"
#include "../../Hardware/TDeck/Keyboard.h"
#include "../../Hardware/TDeck/Touch.h"
#include "../../Hardware/TDeck/Trackball.h"
using namespace RNS;
using namespace Hardware::TDeck;
namespace UI {
namespace LVGL {
bool LVGLInit::_initialized = false;
lv_disp_t* LVGLInit::_display = nullptr;
lv_indev_t* LVGLInit::_keyboard = nullptr;
lv_indev_t* LVGLInit::_touch = nullptr;
lv_indev_t* LVGLInit::_trackball = nullptr;
lv_group_t* LVGLInit::_default_group = nullptr;
TaskHandle_t LVGLInit::_task_handle = nullptr;
SemaphoreHandle_t LVGLInit::_mutex = nullptr;
bool LVGLInit::init() {
if (_initialized) {
return true;
}
INFO("Initializing LVGL");
// Create recursive mutex for thread-safe LVGL access
// Recursive because LVGL callbacks may call other LVGL functions
_mutex = xSemaphoreCreateRecursiveMutex();
if (!_mutex) {
ERROR("Failed to create LVGL mutex");
return false;
}
// Initialize LVGL library
lv_init();
// LVGL 8.x logging is configured via lv_conf.h LV_USE_LOG
// No runtime callback registration needed
// Initialize display (this also sets up LVGL display driver)
if (!Display::init()) {
ERROR("Failed to initialize display for LVGL");
return false;
}
_display = lv_disp_get_default();
INFO(" Display initialized");
// Create default input group for keyboard navigation
_default_group = lv_group_create();
if (!_default_group) {
ERROR("Failed to create input group");
return false;
}
lv_group_set_default(_default_group);
// Initialize keyboard input
if (Keyboard::init()) {
_keyboard = Keyboard::get_indev();
// Associate keyboard with input group
if (_keyboard) {
lv_indev_set_group(_keyboard, _default_group);
INFO(" Keyboard registered with input group");
}
} else {
WARNING(" Keyboard initialization failed");
}
// Initialize touch input
if (Touch::init()) {
_touch = lv_indev_get_next(_keyboard);
INFO(" Touch registered");
} else {
WARNING(" Touch initialization failed");
}
// Initialize trackball input
if (Trackball::init()) {
_trackball = Trackball::get_indev();
// Associate trackball with input group for focus navigation
if (_trackball) {
lv_indev_set_group(_trackball, _default_group);
INFO(" Trackball registered with input group");
}
} else {
WARNING(" Trackball initialization failed");
}
// Set default dark theme
set_theme(true);
_initialized = true;
INFO("LVGL initialized successfully");
return true;
}
bool LVGLInit::init_display_only() {
if (_initialized) {
return true;
}
INFO("Initializing LVGL (display only)");
// Initialize LVGL library
lv_init();
// LVGL 8.x logging is configured via lv_conf.h LV_USE_LOG
// No runtime callback registration needed
// Initialize display
if (!Display::init()) {
ERROR("Failed to initialize display for LVGL");
return false;
}
_display = lv_disp_get_default();
INFO(" Display initialized");
// Set default dark theme
set_theme(true);
_initialized = true;
INFO("LVGL initialized (display only)");
return true;
}
void LVGLInit::task_handler() {
if (!_initialized) {
return;
}
// If running as a task, this is a no-op
if (_task_handle != nullptr) {
return;
}
lv_task_handler();
}
void LVGLInit::lvgl_task(void* param) {
Serial.printf("LVGL task started on core %d\n", xPortGetCoreID());
// Subscribe this task to Task Watchdog Timer
esp_task_wdt_add(nullptr); // nullptr = current task
while (true) {
// Acquire mutex before calling LVGL
#ifndef NDEBUG
// Debug builds: 5-second timeout for stuck task detection
BaseType_t result = xSemaphoreTakeRecursive(_mutex, pdMS_TO_TICKS(5000));
if (result != pdTRUE) {
WARNING("LVGL task mutex timeout (5s) - possible deadlock or stuck task");
// Per Phase 8 context: log warning and continue waiting (don't break functionality)
xSemaphoreTakeRecursive(_mutex, portMAX_DELAY);
}
#else
xSemaphoreTakeRecursive(_mutex, portMAX_DELAY);
#endif
lv_task_handler();
xSemaphoreGiveRecursive(_mutex);
// Feed watchdog and yield to other tasks
esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(5));
}
}
bool LVGLInit::start_task(int priority, int core) {
if (!_initialized) {
ERROR("Cannot start LVGL task - not initialized");
return false;
}
if (_task_handle != nullptr) {
WARNING("LVGL task already running");
return true;
}
// Create the task
BaseType_t result;
if (core >= 0) {
result = xTaskCreatePinnedToCore(
lvgl_task,
"lvgl",
8192, // Stack size (8KB should be enough for LVGL)
nullptr,
priority,
&_task_handle,
core
);
} else {
result = xTaskCreate(
lvgl_task,
"lvgl",
8192,
nullptr,
priority,
&_task_handle
);
}
if (result != pdPASS) {
ERROR("Failed to create LVGL task");
return false;
}
Serial.printf("LVGL task created with priority %d%s%d\n",
priority,
core >= 0 ? " on core " : "",
core >= 0 ? core : 0);
return true;
}
bool LVGLInit::is_task_running() {
return _task_handle != nullptr;
}
SemaphoreHandle_t LVGLInit::get_mutex() {
return _mutex;
}
uint32_t LVGLInit::get_tick() {
return millis();
}
bool LVGLInit::is_initialized() {
return _initialized;
}
void LVGLInit::set_theme(bool dark) {
if (!_initialized) {
return;
}
lv_theme_t* theme;
if (dark) {
// Dark theme with blue accents
theme = lv_theme_default_init(
_display,
lv_palette_main(LV_PALETTE_BLUE), // Primary color
lv_palette_main(LV_PALETTE_RED), // Secondary color
true, // Dark mode
&lv_font_montserrat_14 // Default font
);
} else {
// Light theme
theme = lv_theme_default_init(
_display,
lv_palette_main(LV_PALETTE_BLUE),
lv_palette_main(LV_PALETTE_RED),
false, // Light mode
&lv_font_montserrat_14
);
}
lv_disp_set_theme(_display, theme);
}
lv_disp_t* LVGLInit::get_display() {
return _display;
}
lv_indev_t* LVGLInit::get_keyboard() {
return _keyboard;
}
lv_indev_t* LVGLInit::get_touch() {
return _touch;
}
lv_indev_t* LVGLInit::get_trackball() {
return _trackball;
}
lv_group_t* LVGLInit::get_default_group() {
return _default_group;
}
void LVGLInit::focus_widget(lv_obj_t* obj) {
if (!_default_group || !obj) {
return;
}
// Remove from group first if already there (to avoid duplicates)
lv_group_remove_obj(obj);
// Add to group and focus
lv_group_add_obj(_default_group, obj);
lv_group_focus_obj(obj);
}
void LVGLInit::log_print(const char* buf) {
// Forward LVGL logs to our logging system
// LVGL logs include newlines, so strip them
String msg(buf);
msg.trim();
if (msg.length() > 0) {
// LVGL log levels: Trace, Info, Warn, Error
if (msg.indexOf("[Error]") >= 0) {
ERROR(msg.c_str());
} else if (msg.indexOf("[Warn]") >= 0) {
WARNING(msg.c_str());
} else if (msg.indexOf("[Info]") >= 0) {
INFO(msg.c_str());
} else {
TRACE(msg.c_str());
}
}
}
} // namespace LVGL
} // namespace UI
#endif // ARDUINO