Fixes and improvements (#620)
- Standardized keyboard input using Unicode-based key codes across supported devices and the simulator. Keyboards don't emit `LV_KEY_*` anymore. - Refactored lilygo encoder driver into a reusable GPIO rotary encoder driver (see `Drivers/gpio-encoder-module/`). Added more features to the config file. - Improved LVGL keyboard device management, including duplicate prevention and reliable reconnects. - LVGL file mutex now registers with lvgl start/stop - Improved LVGL startup/shutdown stability and memory allocation reliability. - Increased simulator LVGL memory capacity and improved USB device-class handling.
This commit is contained in:
committed by
GitHub
parent
4fea48f433
commit
db48dfe812
@@ -94,6 +94,10 @@ tactility_add_module(lvgl-module
|
||||
INCLUDE_DIRS include/
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
REQUIRES ${REQUIRES_LIST}
|
||||
# lv_mem_custom.c provides lv_mem_init/lv_malloc_core/etc, which lvgl__lvgl's own lv_init.c/
|
||||
# lv_mem.c call back into (LV_STDLIB_CUSTOM) - a reverse reference into this module that a
|
||||
# normal single-pass static link can't resolve. See module.cmake's WHOLE_ARCHIVE comment.
|
||||
WHOLE_ARCHIVE
|
||||
)
|
||||
|
||||
tactility_get_module_name("lvgl-module" MODULE_NAME)
|
||||
|
||||
@@ -37,6 +37,32 @@ Font sizes and symbols are configurable:
|
||||
|
||||
If you change an icon font size, ensure that a corresponding C file exists in `source-fonts/` (e.g., `material_symbols_shared_24.c`). These files are generated from TTF/OTF fonts using the LVGL font converter.
|
||||
|
||||
## Custom memory allocator
|
||||
|
||||
LVGL's malloc/realloc/free can be routed through a custom backend instead of its built-in pool or
|
||||
plain `malloc`. Three things are required:
|
||||
|
||||
**1. Select the backend.** On ESP32, via Kconfig (`sdkconfig`):
|
||||
```sdkconfig
|
||||
CONFIG_LV_USE_CUSTOM_MALLOC=y
|
||||
```
|
||||
On Simulator/POSIX, ESP-IDF's Kconfig doesn't apply - select it in `lv_conf.h` instead:
|
||||
```c
|
||||
#define LV_USE_STDLIB_MALLOC LV_STDLIB_CUSTOM
|
||||
```
|
||||
|
||||
**2. Force the module into the link.** LVGL's own init code calls back into the functions above
|
||||
a reverse reference a normal single-pass static-archive link can't resolve on its own:
|
||||
```cmake
|
||||
tactility_add_module(lvgl-module
|
||||
...
|
||||
WHOLE_ARCHIVE
|
||||
)
|
||||
```
|
||||
Without `WHOLE_ARCHIVE`, or without step 1 selecting the custom backend, ESP-IDF's LVGL component
|
||||
compiles its own allocator using these same symbol names - whichever one the linker happens to
|
||||
pull in first silently wins, with no error and no guarantee it's the intended one.
|
||||
|
||||
## License
|
||||
|
||||
This module is licensed under the [Apache v2.0](LICENSE-Apache-2.0.md) license.
|
||||
@@ -21,9 +21,12 @@ struct LvglSoftwareKeyboard {
|
||||
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from
|
||||
* LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly.
|
||||
*
|
||||
* @note Idempotent per device: if an indev is already bound to this device (see
|
||||
* lvgl_keyboard_find_by_device()), that indev is returned instead of creating a second one.
|
||||
*
|
||||
* @param[in] device a device of type KEYBOARD_TYPE
|
||||
* @param[in] display the display this indev should be associated with, or NULL to leave it unset
|
||||
* @param[out] out_indev the created indev, valid only when ERROR_NONE is returned
|
||||
* @param[out] out_indev the created (or already-existing) indev, valid only when ERROR_NONE is returned
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_INVALID_ARGUMENT if device or out_indev is NULL, or device is not of type KEYBOARD_TYPE
|
||||
* @retval ERROR_OUT_OF_MEMORY if allocation failed
|
||||
@@ -36,6 +39,14 @@ error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev
|
||||
*/
|
||||
void lvgl_keyboard_remove(lv_indev_t* indev);
|
||||
|
||||
/**
|
||||
* @brief Finds the indev previously created with lvgl_keyboard_add() for the given device, if any.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
* @param[in] device a device of type KEYBOARD_TYPE
|
||||
* @return the bound indev, or NULL if none is bound to this device
|
||||
*/
|
||||
lv_indev_t* lvgl_keyboard_find_by_device(struct Device* device);
|
||||
|
||||
/**
|
||||
* @brief Assigns the indev to the shared keyboard input group, so it can drive focus
|
||||
* navigation and input for focused widgets.
|
||||
|
||||
@@ -41,6 +41,26 @@ void lvgl_keyboard_on_stop_lvgl() {
|
||||
keyboard_group = nullptr;
|
||||
}
|
||||
|
||||
// KeyboardKeyData::key is always a Unicode codepoint (see its doc comment / the CodePoint enum) -
|
||||
// drivers never emit LV_KEY_* directly, including for pure focus-navigation concepts that have no
|
||||
// ordinary character of their own. LVGL itself hardcodes specific sentinel values in its own
|
||||
// indev/group/textarea code that don't match the real Unicode codepoint chosen for the same key,
|
||||
// so those are translated here rather than each driver having to know about LVGL's internals.
|
||||
// CODEPOINT_BACKSPACE/TAB/ESCAPE/DELETE already equal their LV_KEY_* counterpart numerically, so
|
||||
// they need no case below - they fall through `default` unchanged.
|
||||
static uint32_t codepoint_to_lv_key(uint32_t key) {
|
||||
switch (key) {
|
||||
case CODEPOINT_ENTER: return LV_KEY_ENTER;
|
||||
case CODEPOINT_ARROW_LEFT: return LV_KEY_LEFT;
|
||||
case CODEPOINT_ARROW_UP: return LV_KEY_PREV;
|
||||
case CODEPOINT_ARROW_RIGHT: return LV_KEY_RIGHT;
|
||||
case CODEPOINT_ARROW_DOWN: return LV_KEY_NEXT;
|
||||
case CODEPOINT_HOME: return LV_KEY_HOME;
|
||||
case CODEPOINT_END: return LV_KEY_END;
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
|
||||
static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
|
||||
|
||||
@@ -51,8 +71,7 @@ static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// KeyboardKeyData deliberately mirrors lv_indev_data_t's key/continue_reading fields, so no translation is needed.
|
||||
data->key = key_data.key;
|
||||
data->key = codepoint_to_lv_key(key_data.key);
|
||||
data->state = key_data.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
data->continue_reading = key_data.continue_reading;
|
||||
}
|
||||
@@ -65,6 +84,18 @@ error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// A device can reach here twice: lvgl_devices_attach()'s boot scan binds every KEYBOARD_TYPE
|
||||
// device unconditionally (started or not), and a device that wasn't started yet at that point
|
||||
// fires DEVICE_EVENT_STARTED later, driving a second call through
|
||||
// KeyboardDeviceListener::onKeyboardDeviceStarted(). Without this check that would create a
|
||||
// second indev for the same device, and onKeyboardDeviceStopped() only ever removes one of
|
||||
// them, leaving the other dangling - polling a destructed device on the next LVGL tick.
|
||||
lv_indev_t* existing = lvgl_keyboard_find_by_device(device);
|
||||
if (existing != NULL) {
|
||||
*out_indev = existing;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
auto* wrapper = new(std::nothrow) LvglDeviceContext(nullptr);
|
||||
if (wrapper == NULL) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
@@ -100,6 +131,20 @@ void lvgl_keyboard_remove(lv_indev_t* indev) {
|
||||
delete wrapper;
|
||||
}
|
||||
|
||||
lv_indev_t* lvgl_keyboard_find_by_device(Device* device) {
|
||||
lv_indev_t* indev = lv_indev_get_next(nullptr);
|
||||
while (indev != nullptr) {
|
||||
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_KEYPAD) {
|
||||
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
|
||||
if (wrapper != nullptr && wrapper->device == device) {
|
||||
return indev;
|
||||
}
|
||||
}
|
||||
indev = lv_indev_get_next(indev);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void lvgl_keyboard_enable(lv_indev_t* indev) {
|
||||
check(keyboard_group != nullptr);
|
||||
lv_indev_set_group(indev, keyboard_group);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// LVGL's custom stdlib allocator backend (LV_STDLIB_CUSTOM / CONFIG_LV_USE_CUSTOM_MALLOC) - routes
|
||||
// lv_malloc()/lv_realloc()/lv_free() through the kernel's memory_*_with_policy() functions instead
|
||||
// of a fixed-size private pool (LV_STDLIB_BUILTIN) or plain malloc (LV_STDLIB_CLIB), so LVGL grows
|
||||
// dynamically and prefers PSRAM when available instead of contending with everything else for a
|
||||
// small, fixed internal-RAM arena.
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <tactility/memory.h>
|
||||
|
||||
// PSRAM is preferred, not required: MEMORY_CAPABILITY_EXTERNAL is `desired`, so
|
||||
// memory_alloc_with_policy()/memory_realloc_with_policy() fall back to internal RAM automatically
|
||||
// on boards without PSRAM, or if PSRAM is exhausted.
|
||||
static const struct MemoryPolicy LVGL_MEMORY_POLICY = {
|
||||
.required = 0,
|
||||
.desired = MEMORY_CAPABILITY_EXTERNAL,
|
||||
.alignment = 0,
|
||||
};
|
||||
|
||||
void lv_mem_init(void) {
|
||||
}
|
||||
|
||||
void lv_mem_deinit(void) {
|
||||
}
|
||||
|
||||
lv_mem_pool_t lv_mem_add_pool(void* mem, size_t bytes) {
|
||||
// Not supported - memory_*_with_policy() owns allocation, LVGL doesn't need to manage its own
|
||||
// pools on top of it.
|
||||
(void)mem;
|
||||
(void)bytes;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void lv_mem_remove_pool(lv_mem_pool_t pool) {
|
||||
(void)pool;
|
||||
}
|
||||
|
||||
void* lv_malloc_core(size_t size) {
|
||||
return memory_alloc_with_policy(size, &LVGL_MEMORY_POLICY);
|
||||
}
|
||||
|
||||
void* lv_realloc_core(void* p, size_t new_size) {
|
||||
return memory_realloc_with_policy(p, new_size, &LVGL_MEMORY_POLICY);
|
||||
}
|
||||
|
||||
void lv_free_core(void* p) {
|
||||
memory_free(p);
|
||||
}
|
||||
|
||||
void lv_mem_monitor_core(lv_mem_monitor_t* mon_p) {
|
||||
// Not supported - memory_*_with_policy() doesn't expose LVGL-specific usage/fragmentation
|
||||
// stats (memory_print_stats() covers overall heap state instead).
|
||||
(void)mon_p;
|
||||
}
|
||||
|
||||
lv_result_t lv_mem_test_core(void) {
|
||||
return LV_RESULT_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user