Improvements and fixes

- custom lvgl alloc
- keyboard drivers now uses codepoints only (no more hardwired LV_KEY_*)
- fix lvgl file lock when lvgl is stopped
This commit is contained in:
Ken Van Hoeylandt 2026-08-22 22:50:58 +02:00
parent 4b6d2b3d8f
commit 5707c27882
28 changed files with 472 additions and 166 deletions

View File

@ -11,18 +11,33 @@ macro(tactility_get_module_name NAME OUT_NAME)
endmacro()
macro(tactility_add_module NAME)
set(options)
# WHOLE_ARCHIVE: force every object file in this module into the final link unconditionally,
# instead of only the ones some other already-scanned archive currently has a pending
# undefined reference to. Needed when this module provides symbols a component it depends on
# (e.g. lvgl__lvgl's custom-allocator hooks) calls back into - a reverse reference a normal
# single-pass static-archive link can't resolve, since that component is scanned after this
# one's archive has already been passed once. POSIX builds link everything as plain OBJECT
# libraries (no archive-pruning to begin with), so this is a no-op there.
set(options WHOLE_ARCHIVE)
set(oneValueArgs)
set(multiValueArgs SRCS INCLUDE_DIRS PRIV_INCLUDE_DIRS REQUIRES PRIV_REQUIRES)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (DEFINED ENV{ESP_IDF_VERSION})
# idf_component_register's WHOLE_ARCHIVE is a presence-based flag (no value) - only pass
# the token at all when requested, rather than passing ARG_WHOLE_ARCHIVE's TRUE/FALSE as
# a value, which idf_component_register doesn't expect.
set(whole_archive_arg)
if (ARG_WHOLE_ARCHIVE)
set(whole_archive_arg WHOLE_ARCHIVE)
endif()
idf_component_register(
SRCS ${ARG_SRCS}
INCLUDE_DIRS ${ARG_INCLUDE_DIRS}
PRIV_INCLUDE_DIRS ${ARG_PRIV_INCLUDE_DIRS}
REQUIRES ${ARG_REQUIRES}
PRIV_REQUIRES ${ARG_PRIV_REQUIRES}
${whole_archive_arg}
)
else()
add_library(${NAME} OBJECT)

View File

@ -19,7 +19,9 @@ CONFIG_LV_FS_STDIO_PATH=""
CONFIG_LV_FS_STDIO_CACHE_SIZE=4096
CONFIG_LV_USE_LODEPNG=y
CONFIG_LV_USE_BUILTIN_MALLOC=n
CONFIG_LV_USE_CLIB_MALLOC=y
# Routes lv_malloc/realloc/free through Modules/lvgl-module/source/lv_mem_custom.c, which prefers
# PSRAM (falls back to internal RAM automatically) instead of a fixed-size pool or plain malloc.
CONFIG_LV_USE_CUSTOM_MALLOC=y
CONFIG_LV_USE_MSGBOX=n
CONFIG_LV_USE_SPINNER=n
CONFIG_LV_USE_WIN=n

View File

@ -44,31 +44,31 @@
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i
111 112 10 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
111 112 13 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
0 97 115 100 102 103 104 106 107 0 // a s d f g h j k
108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU
122 120 99 118 98 32 32 110 109 0 // z x c v b n m
20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN
20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN
];
keymap-uc = [
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
9 81 87 69 82 84 89 85 73 0 // TAB Q W E R T Y U I
79 80 10 40 41 45 59 58 3 0 // O P ENTER ( ) - ; : STOP
79 80 13 40 41 45 59 58 3 0 // O P ENTER ( ) - ; : STOP
0 65 83 68 70 71 72 74 75 0 // A S D F G H J K
76 17 35 123 125 42 44 46 2 0 // L UP # { } * , . MENU
90 88 67 86 66 32 32 78 77 0 // Z X C V B N M
20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN
20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN
];
keymap-sy = [
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i
111 112 10 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
111 112 13 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
0 97 115 100 102 103 104 106 107 0 // a s d f g h j k
108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU
122 120 99 118 98 32 32 110 109 0 // z x c v b n m
20 18 19 60 62 47 92 61 10 0 // LEFT DOWN RIGHT < > / \ = RUN
20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN
];
shift-row = <4>;
shift-col = <0>;

View File

@ -74,19 +74,19 @@
keymap-lc = [
113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p
97 115 100 102 103 104 106 107 108 8 // a s d f g h j k l BACKSPACE
0 122 120 99 118 98 110 109 36 10 // z x c v b n m $ ENTER
0 122 120 99 118 98 110 109 36 13 // z x c v b n m $ ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
keymap-uc = [
81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P
65 83 68 70 71 72 74 75 76 8 // A S D F G H J K L BACKSPACE
0 90 88 67 86 66 78 77 36 10 // Z X C V B N M $ ENTER
0 90 88 67 86 66 78 77 36 13 // Z X C V B N M $ ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
keymap-sy = [
49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0
64 35 43 45 42 47 40 41 95 8 // @ # + - * / ( ) _ BACKSPACE
0 33 63 59 58 39 34 44 46 10 // ! ? ; : ' " , . ENTER
0 33 63 59 58 39 34 44 46 13 // ! ? ; : ' " , . ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
shift-row = <2>;

View File

@ -61,19 +61,19 @@
keymap-lc = [
113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p
97 115 100 102 103 104 106 107 108 8 // a s d f g h j k l BACKSPACE
0 122 120 99 118 98 110 109 36 10 // z x c v b n m $ ENTER
0 122 120 99 118 98 110 109 36 13 // z x c v b n m $ ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
keymap-uc = [
81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P
65 83 68 70 71 72 74 75 76 8 // A S D F G H J K L BACKSPACE
0 90 88 67 86 66 78 77 36 10 // Z X C V B N M $ ENTER
0 90 88 67 86 66 78 77 36 13 // Z X C V B N M $ ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
keymap-sy = [
49 50 51 52 53 54 55 56 57 48 // 1 2 3 4 5 6 7 8 9 0
64 35 43 45 42 47 40 41 95 8 // @ # + - * / ( ) _ BACKSPACE
0 33 63 59 58 39 34 44 46 10 // ! ? ; : ' " , . ENTER
0 33 63 59 58 39 34 44 46 13 // ! ? ; : ' " , . ENTER
0 0 0 0 0 11 48 32 0 9 // PREV 0 SPC NEXT
];
shift-row = <2>;

View File

@ -60,13 +60,13 @@
columns = <10>;
keymap-lc = [
113 119 101 114 116 121 117 105 111 112 // q w e r t y u i o p
97 115 100 102 103 104 106 107 108 10 // a s d f g h j k l ENTER
97 115 100 102 103 104 106 107 108 13 // a s d f g h j k l ENTER
0 122 120 99 118 98 110 109 0 8 // z x c v b n m BACKSPACE
32 0 0 0 0 0 0 0 0 0 // SPC
];
keymap-uc = [
81 87 69 82 84 89 85 73 79 80 // Q W E R T Y U I O P
65 83 68 70 71 72 74 75 76 10 // A S D F G H J K L ENTER
65 83 68 70 71 72 74 75 76 13 // A S D F G H J K L ENTER
0 90 88 67 86 66 78 77 0 8 // Z X C V B N M BACKSPACE
32 0 0 0 0 0 0 0 0 0 // SPC
];

View File

@ -17,8 +17,6 @@
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <lvgl.h>
#include <cstdlib>
#include <cstring>
@ -119,36 +117,38 @@ static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
};
// ---------------------------------------------------------------------------
// HID usage code + modifier → LVGL key
// Covers all codes present in the Tab5 matrix tables above. LV_KEY_* are plain uint32_t
// constants - matching KeyboardKeyData::key's driver-defined contract and the same convention
// m5stack-module's cardputer_keyboard.cpp kernel driver already uses.
// HID usage code + modifier → key (Unicode codepoint)
// Covers all codes present in the Tab5 matrix tables above. Every key returns a real Unicode
// codepoint per KeyboardKeyData::key's contract - never an LV_KEY_* constant. Keys with no
// ordinary character of their own (arrows, Tab/Shift+Tab and Ctrl+arrow as focus nav) use the
// CodePoint enum's standard Unicode symbols for the concept (CODEPOINT_FOCUS_PREV/NEXT reused for
// Ctrl+arrow's focus-move aliases). lvgl-module's keyboard.cpp translates all of these back to
// LVGL's own sentinels (CODEPOINT_ESCAPE/BACKSPACE/DELETE already equal their LV_KEY_* counterpart
// numerically, so no translation is needed for those).
//
// `ctrl` only selects the LVGL focus-navigation aliases for the arrow keys. Ctrl chords on
// ordinary keys are NOT folded into the returned value - the C0 control codes a terminal wants
// (Ctrl+C = 0x03, Ctrl+K = 0x0B, ...) collide with the LVGL constants returned here (LV_KEY_END = 3,
// LV_KEY_PREV = 11, ...), so Ctrl is reported out-of-band via KeyboardKeyData::ctrl instead and
// consumers that want control codes derive them themselves.
// Ctrl chords on ordinary keys are NOT folded into the returned value - the C0 control codes a
// terminal wants (Ctrl+C = 0x03, Ctrl+K = 0x0B, ...) would collide with a codepoint-based scheme,
// so Ctrl is reported out-of-band via KeyboardKeyData::ctrl instead and consumers that want
// control codes derive them themselves.
// ---------------------------------------------------------------------------
static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl) {
const bool shift = (modifier & 0x22U) != 0U;
// Navigation → LVGL key constants
// Navigation → key (Unicode codepoint)
switch (keycode) {
case 0x29: return LV_KEY_ESC;
case 0x28: return LV_KEY_ENTER;
case 0x2A: return LV_KEY_BACKSPACE;
case 0x4C: return LV_KEY_DEL;
case 0x2B: return '\t';
// Arrows: Ctrl+arrow = focus navigation, plain arrow = raw cursor movement
case 0x52: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_UP;
case 0x51: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_DOWN;
case 0x50: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_LEFT;
case 0x4F: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_RIGHT;
case 0x29: return CODEPOINT_ESCAPE;
case 0x28: return CODEPOINT_ENTER;
case 0x2A: return CODEPOINT_BACKSPACE;
case 0x4C: return CODEPOINT_DELETE;
case 0x2B: return CODEPOINT_TAB;
case 0x52: return CODEPOINT_ARROW_UP;
case 0x51: return CODEPOINT_ARROW_DOWN;
case 0x50: return CODEPOINT_ARROW_LEFT;
case 0x4F: return CODEPOINT_ARROW_RIGHT;
default: break;
}
// F1-F12 (Sym layer over the number row) have no LVGL/ASCII representation - callers that
// F1-F12 (Sym layer over the number row) have no Unicode/LVGL representation - callers that
// want them read KeyboardKeyData::hid_keycode instead (see drain_events()), which is
// populated for every key regardless of whether `key` itself has a meaningful value.
@ -378,9 +378,9 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
if (internal->ctrl_held) modifier |= 0x01U; // HID LeftCtrl
if (internal->alt_held) modifier |= 0x04U; // HID LeftAlt
const uint32_t lv_key = tab5_translate_key(m.keycode, modifier, internal->ctrl_held);
// Queue whenever there's a HID keycode, even if this key has no LVGL/ASCII
// Queue whenever there's a HID keycode, even if this key has no Unicode/LVGL
// representation (e.g. F1-F12) - lv_key stays 0 for those, callers that only
// care about LVGL/ASCII text ignore a 0 key the same way they always have.
// care about the codepoint ignore a 0 key the same way they always have.
if (pressed) {
// A real key was pressed — this hold is a chord, not a tap
internal->aa_tapped = false;

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include "sdl_input.h"
#include <lvgl.h>
#include <tactility/drivers/keyboard.h>
#include <SDL2/SDL.h>
@ -27,22 +27,27 @@ void push_key(uint32_t key) {
key_queue_count++;
}
// Mirrors LVGL's own lv_sdl_keyboard.c keycode_to_ctrl_key(): maps navigation/control keys to
// LV_KEY_* constants. Printable characters arrive separately via SDL_TEXTINPUT.
uint32_t keycode_to_key(SDL_Keycode sdl_key) {
// Mirrors LVGL's own lv_sdl_keyboard.c keycode_to_ctrl_key(): every key returns a real Unicode
// codepoint per KeyboardKeyData::key's contract - never an LV_KEY_* constant, even for keys with
// no ordinary character of their own (arrows, Tab/Shift+Tab as focus nav, Home/End), which use the
// CodePoint enum's standard Unicode symbols for the concept. lvgl-module's keyboard.cpp translates
// all of these back to LVGL's own sentinels (CODEPOINT_ESCAPE/BACKSPACE/DELETE already equal their
// LV_KEY_* counterpart numerically, so no translation is needed for those). Printable characters
// arrive separately via SDL_TEXTINPUT.
uint32_t keycode_to_key(SDL_Keycode sdl_key, bool shift) {
switch (sdl_key) {
case SDLK_RIGHT: return LV_KEY_RIGHT;
case SDLK_LEFT: return LV_KEY_LEFT;
case SDLK_UP: return LV_KEY_UP;
case SDLK_DOWN: return LV_KEY_DOWN;
case SDLK_ESCAPE: return LV_KEY_ESC;
case SDLK_BACKSPACE: return LV_KEY_BACKSPACE;
case SDLK_DELETE: return LV_KEY_DEL;
case SDLK_RIGHT: return CODEPOINT_ARROW_RIGHT;
case SDLK_LEFT: return CODEPOINT_ARROW_LEFT;
case SDLK_UP: return CODEPOINT_ARROW_UP;
case SDLK_DOWN: return CODEPOINT_ARROW_DOWN;
case SDLK_ESCAPE: return CODEPOINT_ESCAPE;
case SDLK_BACKSPACE: return CODEPOINT_BACKSPACE;
case SDLK_DELETE: return CODEPOINT_DELETE;
case SDLK_RETURN:
case SDLK_KP_ENTER: return LV_KEY_ENTER;
case SDLK_TAB: return LV_KEY_NEXT;
case SDLK_HOME: return LV_KEY_HOME;
case SDLK_END: return LV_KEY_END;
case SDLK_KP_ENTER: return CODEPOINT_ENTER;
case SDLK_TAB: return CODEPOINT_TAB;
case SDLK_HOME: return CODEPOINT_HOME;
case SDLK_END: return CODEPOINT_END;
default: return 0;
}
}
@ -75,7 +80,7 @@ void sdl_input_pump() {
}
break;
case SDL_KEYDOWN:
push_key(keycode_to_key(event.key.keysym.sym));
push_key(keycode_to_key(event.key.keysym.sym, (event.key.keysym.mod & KMOD_SHIFT) != 0));
break;
case SDL_TEXTINPUT:
// ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard.

View File

@ -12,8 +12,6 @@
#include <tactility/log.h>
#include <tactility/time.h>
#include <lvgl.h>
#include <cstdlib>
#define TAG "ButtonControl"
@ -105,8 +103,8 @@ static error_t start(Device* device) {
error_t error = acquire_button(
config->pin_primary,
two_button_mode ? LV_KEY_ENTER : LV_KEY_NEXT,
two_button_mode ? LV_KEY_ESC : LV_KEY_ENTER,
two_button_mode ? (uint32_t)CODEPOINT_ENTER : (uint32_t)CODEPOINT_ARROW_DOWN,
two_button_mode ? (uint32_t)CODEPOINT_ESCAPE : (uint32_t)CODEPOINT_ENTER,
&internal->primary
);
if (error != ERROR_NONE) {
@ -114,7 +112,7 @@ static error_t start(Device* device) {
return error;
}
error = acquire_button(config->pin_secondary, LV_KEY_NEXT, LV_KEY_PREV, &internal->secondary);
error = acquire_button(config->pin_secondary, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_UP, &internal->secondary);
if (error != ERROR_NONE) {
if (internal->primary.in_use) {
gpio_descriptor_release(internal->primary.descriptor);
@ -176,7 +174,7 @@ static void poll_button(const ButtonControlConfig* config, ButtonControlInternal
}
// Release: decide short vs. long press by elapsed hold duration, then queue a synthetic
// key tap (press followed by release) for the LVGL key this gesture maps to.
// key tap (press followed by release) for the key this gesture maps to.
uint32_t held_ms = now - state->press_start_time;
uint32_t key = held_ms < config->long_press_ms ? state->short_press_key : state->long_press_key;
push_pending(internal, key, true);

View File

@ -14,8 +14,6 @@
#include <tactility/error.h>
#include <tactility/log.h>
#include <lvgl.h>
#include <cstdlib>
static constexpr const char* TAG = "CardputerAdvKeyboard";
@ -46,24 +44,24 @@ static constexpr int CARDPUTER_ADV_COLS = 14;
// [row][col] on the 4x14 grid, matching the base Cardputer's physical layout. 0 means the cell
// emits nothing (used for the sym/shift cells themselves, and unwired cells on this board).
static const uint32_t cardputer_adv_keymap_lc[CARDPUTER_ADV_ROWS][CARDPUTER_ADV_COLS] = {
{ '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', LV_KEY_BACKSPACE },
{ '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\' },
{ 0, 0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', LV_KEY_ENTER },
{ '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', CODEPOINT_BACKSPACE },
{ CODEPOINT_TAB, 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\' },
{ 0, 0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', CODEPOINT_ENTER },
{ 0, 0, 0, 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', ' ' },
};
static const uint32_t cardputer_adv_keymap_uc[CARDPUTER_ADV_ROWS][CARDPUTER_ADV_COLS] = {
{ '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', LV_KEY_DEL },
{ '\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|' },
{ 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', LV_KEY_ENTER },
{ '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', CODEPOINT_DELETE },
{ CODEPOINT_FOCUS_PREV, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|' },
{ 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', CODEPOINT_ENTER },
{ 0, 0, 0, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', ' ' },
};
static const uint32_t cardputer_adv_keymap_sym[CARDPUTER_ADV_ROWS][CARDPUTER_ADV_COLS] = {
{ LV_KEY_ESC, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ '\t', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, LV_KEY_PREV, 0, LV_KEY_ENTER },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, LV_KEY_LEFT, LV_KEY_NEXT, LV_KEY_RIGHT, 0 },
{ CODEPOINT_ESCAPE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ CODEPOINT_TAB, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CODEPOINT_ARROW_UP, 0, CODEPOINT_ENTER },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 0 },
};
struct CardputerAdvActiveKey {

View File

@ -14,8 +14,6 @@
#include <tactility/error.h>
#include <tactility/log.h>
#include <lvgl.h>
#include <cstdlib>
static constexpr const char* TAG = "CardputerKeyboard";
@ -74,8 +72,8 @@ struct CardputerKeyboardPendingEvent {
struct CardputerKeyboardInternal {
GpioDescriptor* output_descriptors[CARDPUTER_OUTPUT_COUNT];
GpioDescriptor* input_descriptors[CARDPUTER_INPUT_COUNT];
// 0 when no actionable key is currently held; otherwise the LVGL key code last reported
// via read_key(). Only ever one actionable key at a time (matches original hardware driver:
// 0 when no actionable key is currently held; otherwise the key (Unicode codepoint) last
// reported via read_key(). Only ever one actionable key at a time (matches original hardware driver:
// modifier keys are consumed internally, and only the first non-modifier key found in a
// scan is reported).
uint32_t active_key;
@ -195,10 +193,12 @@ static uint8_t read_input(CardputerKeyboardInternal* internal) {
return mask;
}
// Scans the full matrix and resolves it to a single LVGL key code (0 if none), applying the
// same priority as the original driver: enter > space > backspace > first regular character
// found in scan order, with fn changing the interpretation of backspace/enter/punctuation.
// Modifier keys (fn/shift/ctrl/opt/alt/tab) are never reported themselves.
// Scans the full matrix and resolves it to a single key (Unicode codepoint, 0 if none - never an
// LV_KEY_* constant, see KeyboardKeyData::key's contract), applying priority: enter > space >
// backspace > tab (U+21E5 tab-to-bar, or U+21E4 with shift - the closest standard Unicode symbol
// for focus navigation) > first regular character found in scan order, with fn changing the
// interpretation of backspace/enter/punctuation. Modifier keys (fn/shift/ctrl/opt/alt) are never
// reported themselves.
static uint32_t scan_key(CardputerKeyboardInternal* internal) {
bool fn = false, shift = false, ctrl = false;
bool del_flag = false, enter_flag = false, space_flag = false;
@ -223,7 +223,6 @@ static uint32_t scan_key(CardputerKeyboardInternal* internal) {
const auto& def = cardputer_key_map[row][col];
switch (def.role) {
case CARDPUTER_KEY_TAB:
case CARDPUTER_KEY_OPT:
case CARDPUTER_KEY_ALT:
break; // consumed, never affects output
@ -259,24 +258,25 @@ static uint32_t scan_key(CardputerKeyboardInternal* internal) {
char resolved_char = has_regular ? ((ctrl || shift) ? regular_shifted : regular_normal) : 0;
if (!fn) {
if (enter_flag) return LV_KEY_ENTER;
if (enter_flag) return CODEPOINT_ENTER;
if (space_flag) return (uint32_t)' ';
if (del_flag) return LV_KEY_BACKSPACE;
if (has_regular) return (uint32_t)(uint8_t)resolved_char;
if (del_flag) return CODEPOINT_BACKSPACE;
if (has_regular) return (uint32_t)resolved_char;
return 0;
}
// fn combos: forward-delete, enter, and group navigation (using PREV/NEXT rather than
// UP/DOWN so widgets like lv_switch that toggle on arrow keys aren't affected).
if (del_flag) return LV_KEY_DEL;
if (enter_flag) return LV_KEY_ENTER;
// fn combos: forward-delete, enter, and group navigation (using the tab-to-bar codepoints
// rather than arrow codepoints so widgets like lv_switch that toggle on arrow keys aren't
// affected).
if (del_flag) return CODEPOINT_DELETE;
if (enter_flag) return CODEPOINT_ENTER;
if (has_regular) {
switch (resolved_char) {
case '`': return LV_KEY_ESC;
case ',': return LV_KEY_LEFT;
case '/': return LV_KEY_RIGHT;
case ';': return LV_KEY_PREV;
case '.': return LV_KEY_NEXT;
case '`': return CODEPOINT_ESCAPE;
case ',': return CODEPOINT_ARROW_LEFT;
case '/': return CODEPOINT_ARROW_RIGHT;
case ';': return CODEPOINT_ARROW_UP;
case '.': return CODEPOINT_ARROW_DOWN;
default: return 0;
}
}

View File

@ -34,8 +34,9 @@ properties:
Base (lowercase) layer keymap, rows*columns bytes in row-major order (already in
silkscreen/keymap column order - see reverse-columns). 0 = no key at this position (e.g.
a blank matrix position, or a position handled as a modifier via shift-row/shift-col/
sym-row/sym-col instead). Non-zero bytes are sent as-is via KeyboardKeyData::key (ASCII
or an LVGL LV_KEY_* code).
sym-row/sym-col instead). Non-zero bytes are sent as-is via KeyboardKeyData::key (a Unicode
codepoint - byte range covers Latin-1 - or an LVGL LV_KEY_* code for keys with no character
representation).
keymap-uc:
type: array
element-type: uint8_t

View File

@ -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)

View File

@ -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`):
```
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.

View File

@ -20,19 +20,16 @@ static bool initialized = false;
void lvgl_lock(void) {
if (!initialized) { return; }
if (!lvgl_is_running()) { return; }
lvgl_port_lock(portMAX_DELAY);
}
bool lvgl_try_lock(uint32_t timeoutTicks) {
if (!initialized) { return false; }
if (!lvgl_is_running()) { return false; }
// lvgl_port_lock expects milliseconds
return lvgl_port_lock(timeoutTicks * portTICK_PERIOD_MS);
}
void lvgl_unlock(void) {
if (!lvgl_is_running()) { return; }
if (!initialized) { return; }
lvgl_port_unlock();
}

View File

@ -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_UP;
case CODEPOINT_ARROW_RIGHT: return LV_KEY_RIGHT;
case CODEPOINT_ARROW_DOWN: return LV_KEY_DOWN;
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;
}

View File

@ -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;
}

View File

@ -85,6 +85,12 @@ static const uint8_t keycode2ascii[57][2] = {
{'`', '~'}, {',', '<'}, {'.', '>'}, {'/', '?'},
};
// Every key returns a real Unicode codepoint per KeyboardKeyData::key's contract - never an
// LV_KEY_* (or USB_HID_KEY_*, which mirrors it) constant. Keys with no ordinary character of
// their own use the CodePoint enum's standard Unicode symbol for the concept. lvgl-module's
// keyboard.cpp translates all of these back to LVGL's own sentinels (CODEPOINT_ESCAPE/BACKSPACE/
// DELETE already equal their LV_KEY_* counterpart numerically, so no translation is needed for
// those).
static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code,
bool caps_lock, bool num_lock) {
bool shift = (modifier & HID_LEFT_SHIFT) || (modifier & HID_RIGHT_SHIFT);
@ -92,33 +98,33 @@ static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code,
bool alt = (modifier & HID_LEFT_ALT) || (modifier & HID_RIGHT_ALT);
switch (key_code) {
case HID_KEY_ENTER: return USB_HID_KEY_ENTER;
case HID_KEY_ESC: return USB_HID_KEY_ESC;
case HID_KEY_DEL: return USB_HID_KEY_BACKSPACE;
case HID_KEY_DELETE: return USB_HID_KEY_DEL;
case HID_KEY_TAB: return shift ? USB_HID_KEY_PREV : USB_HID_KEY_NEXT;
case HID_KEY_UP: return USB_HID_KEY_UP;
case HID_KEY_DOWN: return USB_HID_KEY_DOWN;
case HID_KEY_LEFT: return USB_HID_KEY_LEFT;
case HID_KEY_RIGHT: return USB_HID_KEY_RIGHT;
case HID_KEY_HOME: return USB_HID_KEY_HOME;
case HID_KEY_END: return USB_HID_KEY_END;
case HID_KEY_KEYPAD_ENTER: return USB_HID_KEY_ENTER;
case HID_KEY_ENTER: return CODEPOINT_ENTER;
case HID_KEY_ESC: return CODEPOINT_ESCAPE;
case HID_KEY_DEL: return CODEPOINT_BACKSPACE;
case HID_KEY_DELETE: return CODEPOINT_DELETE;
case HID_KEY_TAB: return '\t';
case HID_KEY_UP: return CODEPOINT_ARROW_UP;
case HID_KEY_DOWN: return CODEPOINT_ARROW_DOWN;
case HID_KEY_LEFT: return CODEPOINT_ARROW_LEFT;
case HID_KEY_RIGHT: return CODEPOINT_ARROW_RIGHT;
case HID_KEY_HOME: return CODEPOINT_HOME;
case HID_KEY_END: return CODEPOINT_END;
case HID_KEY_KEYPAD_ENTER: return CODEPOINT_ENTER;
case HID_KEY_KEYPAD_ADD: return '+';
case HID_KEY_KEYPAD_SUB: return '-';
case HID_KEY_KEYPAD_MUL: return '*';
case HID_KEY_KEYPAD_DIV: return '/';
case HID_KEY_KEYPAD_0: return num_lock ? (uint32_t)'0' : 0u;
case HID_KEY_KEYPAD_1: return num_lock ? (uint32_t)'1' : (uint32_t)USB_HID_KEY_END;
case HID_KEY_KEYPAD_2: return num_lock ? (uint32_t)'2' : (uint32_t)USB_HID_KEY_DOWN;
case HID_KEY_KEYPAD_1: return num_lock ? (uint32_t)'1' : (uint32_t)CODEPOINT_END;
case HID_KEY_KEYPAD_2: return num_lock ? (uint32_t)'2' : (uint32_t)CODEPOINT_ARROW_DOWN;
case HID_KEY_KEYPAD_3: return num_lock ? (uint32_t)'3' : 0u;
case HID_KEY_KEYPAD_4: return num_lock ? (uint32_t)'4' : (uint32_t)USB_HID_KEY_LEFT;
case HID_KEY_KEYPAD_4: return num_lock ? (uint32_t)'4' : (uint32_t)CODEPOINT_ARROW_LEFT;
case HID_KEY_KEYPAD_5: return num_lock ? (uint32_t)'5' : 0u;
case HID_KEY_KEYPAD_6: return num_lock ? (uint32_t)'6' : (uint32_t)USB_HID_KEY_RIGHT;
case HID_KEY_KEYPAD_7: return num_lock ? (uint32_t)'7' : (uint32_t)USB_HID_KEY_HOME;
case HID_KEY_KEYPAD_8: return num_lock ? (uint32_t)'8' : (uint32_t)USB_HID_KEY_UP;
case HID_KEY_KEYPAD_6: return num_lock ? (uint32_t)'6' : (uint32_t)CODEPOINT_ARROW_RIGHT;
case HID_KEY_KEYPAD_7: return num_lock ? (uint32_t)'7' : (uint32_t)CODEPOINT_HOME;
case HID_KEY_KEYPAD_8: return num_lock ? (uint32_t)'8' : (uint32_t)CODEPOINT_ARROW_UP;
case HID_KEY_KEYPAD_9: return num_lock ? (uint32_t)'9' : 0u;
case HID_KEY_KEYPAD_DELETE: return num_lock ? (uint32_t)'.' : (uint32_t)USB_HID_KEY_DEL;
case HID_KEY_KEYPAD_DELETE: return num_lock ? (uint32_t)'.' : (uint32_t)CODEPOINT_DELETE;
default: break;
}

View File

@ -74,6 +74,7 @@ constexpr auto* TAG = "Tactility";
static DispatcherHandle_t mainDispatcherHandle = dispatcher_alloc();
void initFileMutexForLvgl();
void deinitFileMutexForLvgl();
namespace {
@ -423,6 +424,8 @@ static void applySavedTouchCalibration() {
#endif // CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED
static void onLvglStarted() {
initFileMutexForLvgl();
window_manager_configure(windowManagerScreenInit);
check(module_ensure_started(&lvgl_window_manager_module) == ERROR_NONE);
@ -453,6 +456,8 @@ static void onLvglStarted() {
}
static void onLvglStopped() {
deinitFileMutexForLvgl();
if (softwareKeyboard.object != nullptr) {
lvgl_software_keyboard_destruct(&softwareKeyboard);
}
@ -510,9 +515,6 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
registerAndStartServices();
// Must start right before LVGL
initFileMutexForLvgl();
lvgl_module_configure((LvglModuleConfig) {
.on_start = onLvglStarted,
.on_stop = onLvglStopped,

View File

@ -273,7 +273,6 @@ void runBootSequence(TickType_t startTime) {
#endif
if (!setupUsbBootMode()) {
LOG_I(TAG, "initFromBootApp");
registerApps();
waitForMinimalSplashDuration(startTime);
startNextApp();

View File

@ -6,12 +6,16 @@
#include <tactility/filesystem/file_system.h>
#include <lvgl/lvgl.h>
#include <vector>
constexpr auto* TAG = "file_mutex_lvgl";
struct Device;
namespace {
std::vector<FileMutexId> registered_ids;
void wrapped_lvgl_lock() {
if (!lvgl_is_running()) return;
lvgl_lock();
@ -43,7 +47,7 @@ namespace tt {
* If the SPI controller has a display on the bus, we create an LVGL lock for the file system path.
*/
void initFileMutexForLvgl() {
file_system_for_each(nullptr, [](FileSystem* fs, void* context) {
file_system_for_each(&registered_ids, [](FileSystem* fs, void* context) {
char mount_path[64];
if (file_system_get_path(fs, mount_path, sizeof(mount_path)) != ERROR_NONE) {
return true;
@ -77,17 +81,15 @@ void initFileMutexForLvgl() {
struct Context {
const char* mountPath;
std::vector<FileMutexId>* registeredIds;
};
Context ctx = { .mountPath = mount_path };
Context ctx = { .mountPath = mount_path, .registeredIds = static_cast<std::vector<FileMutexId>*>(context) };
device_for_each_child(parent, &ctx, [](Device* child, void* context) -> bool {
Context* ctx = static_cast<Context*>(context);
if (device_get_type(child) == &DISPLAY_TYPE) {
LOG_I(TAG, "Adding file mutex for %s as it shares a bus with a display", ctx->mountPath);
file_mutex_register(
&lvgl_mutex,
ctx->mountPath
);
ctx->registeredIds->push_back(file_mutex_add(&lvgl_mutex, ctx->mountPath));
return false;
} else {
LOG_D(TAG, "child of parent, %s: not DISPLAY_TYPE", child->name);
@ -105,7 +107,7 @@ void initFileMutexForLvgl() {
return;
}
file_system_for_each(nullptr, [](FileSystem* fs, void* context) {
file_system_for_each(&registered_ids, [](FileSystem* fs, void* context) {
char mount_path[64];
if (file_system_get_path(fs, mount_path, sizeof(mount_path)) != ERROR_NONE) {
return true;
@ -117,9 +119,17 @@ void initFileMutexForLvgl() {
}
LOG_I(TAG, "Adding file mutex for %s (SD card) - a display is present and may contend for bus/DMA resources", mount_path);
file_mutex_register(&lvgl_mutex, mount_path);
auto* ids = static_cast<std::vector<FileMutexId>*>(context);
ids->push_back(file_mutex_add(&lvgl_mutex, mount_path));
return true;
});
}
void deinitFileMutexForLvgl() {
for (FileMutexId id : registered_ids) {
file_mutex_remove(id);
}
registered_ids.clear();
}
}

View File

@ -11,11 +11,43 @@ extern "C" {
#include <tactility/device.h>
#include <tactility/error.h>
/**
* @brief Named Unicode codepoints for KeyboardKeyData::key.
*
* Keys that produce an ordinary character use that character's own codepoint directly (e.g. 'a',
* ' ') and don't need a name here. These are the standard Unicode symbol codepoints used instead
* of LVGL's LV_KEY_* sentinels for keys with no character of their own (arrows, Home/End,
* Tab/Shift+Tab-as-focus-navigation), plus names for the handful of C0 control keys every driver
* needs (Enter/Escape/Backspace/Delete/Tab). Modules/lvgl-module/source/devices/keyboard.cpp
* translates these back into LV_KEY_* internally for LVGL.
*/
typedef enum {
CODEPOINT_ENTER = '\r',
CODEPOINT_ESCAPE = '\x1B',
CODEPOINT_BACKSPACE = '\b',
CODEPOINT_DELETE = '\x7F',
CODEPOINT_TAB = '\t',
CODEPOINT_ARROW_LEFT = 0x2190,
CODEPOINT_ARROW_UP = 0x2191,
CODEPOINT_ARROW_RIGHT = 0x2192,
CODEPOINT_ARROW_DOWN = 0x2193,
CODEPOINT_HOME = 0x21F1,
CODEPOINT_END = 0x21F2,
} CodePoint;
/**
* @brief A single key event read from a keyboard device.
*/
struct KeyboardKeyData {
/** @brief The key code. Driver-defined (e.g. ASCII/UTF-8 codepoint, scan code, or LVGL key code). */
/**
* @brief The key. Always a Unicode codepoint - never a raw scan code.
*
* For a key that produces a character, this is that character's codepoint (e.g. 'a', ' '). For
* a key with no character representation, or one of the handful of C0 control keys every
* driver needs, this is one of the CodePoint enum values above - never an LVGL LV_KEY_*
* constant directly. See ctrl's doc comment below for the CodePoint values that numerically
* collide with real C0 control codes.
*/
uint32_t key;
/** @brief True if the key was pressed, false if released. */
bool pressed;
@ -29,9 +61,9 @@ struct KeyboardKeyData {
*
* Reported separately rather than folded into `key` because the two encodings collide: the C0
* control codes a terminal expects for Ctrl chords (Ctrl+C is 0x03, Ctrl+K is 0x0B, ...) overlap
* the LVGL key constants drivers emit in the same field (LV_KEY_END is 3, LV_KEY_PREV is 11,
* LV_KEY_UP is 17, ...), so a single uint32_t cannot express both. Consumers that want control
* codes derive them here, e.g.
* the four CodePoint values that remain in true C0 range (CODEPOINT_ENTER is Ctrl+M/13,
* CODEPOINT_BACKSPACE is Ctrl+H/8, CODEPOINT_TAB is Ctrl+I/9, CODEPOINT_ESCAPE is Ctrl+[/27), so
* a single uint32_t cannot express both. Consumers that want control codes derive them here, e.g.
* `((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z')) ? (key & 0x1F) : key`
* when ctrl is set.
*
@ -48,8 +80,8 @@ struct KeyboardKeyData {
* or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers
* need). Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so
* consumers that want to mirror physical key presses as real HID reports (e.g. USB HID output)
* don't have to reverse-engineer one out of `key`'s LVGL/ASCII encoding - which is lossy for
* keys with no ASCII/LVGL representation at all, e.g. F1-F12.
* don't have to reverse-engineer one out of `key`'s codepoint encoding - which is lossy for
* keys with no Unicode/LVGL representation at all, e.g. F1-F12.
*/
uint8_t hid_keycode;
/**
@ -83,7 +115,7 @@ struct KeyboardApi {
/**
* @brief Optional: reports whether the keyboard is physically present right now. Only
* meaningful for hot-pluggable/detachable keyboards (e.g. a removable accessory) whose
* meaningful for hot-pluggable/detachable keyboarcardputer_keyboard.cppds (e.g. a removable accessory) whose
* kernel device is constructed and started once at boot regardless of physical attachment -
* leave NULL for a keyboard that's always physically present whenever its device is active
* (the common case; callers must treat NULL the same as "always present").

View File

@ -19,13 +19,25 @@ struct FileMutex {
void (*unlock)();
};
typedef uint32_t FileMutexId;
#define FILE_MUTEX_ID_INVALID ((FileMutexId)0)
/**
* @brief Registers a mutex for a mount path (e.g. "/sdcard") and its descendants.
* @param[in] mutex callbacks to associate with the path; a copy is stored
* @param[in] path mount path this mutex serializes access to
* @note No-op if a mutex is already registered for this exact path.
* @return the id of the new entry, or the id of the existing entry if path is already registered
* @note If a mutex is already registered for this exact path, no new entry is created and the
* existing entry's id is returned; the existing callbacks are left unchanged.
*/
void file_mutex_register(const struct FileMutex* mutex, const char* path);
FileMutexId file_mutex_add(const struct FileMutex* mutex, const char* path);
/**
* @brief Removes a previously added mutex registration.
* @param[in] id id returned by file_mutex_add(); a stale or unknown id is a no-op
*/
void file_mutex_remove(FileMutexId id);
/**
* @brief Looks up the mutex registered for path or one of its ancestor mount paths.

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/filesystem/file_mutex.h>
#include <tactility/concurrent/mutex.h>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
@ -12,41 +14,89 @@ static const FileMutex no_mutex = {
};
struct FileMutexEntry {
FileMutexId id;
std::string path;
FileMutex mutex;
};
static std::vector<FileMutexEntry> mutex_entries;
// Guards mutex_entries against concurrent add/get/remove; unrelated to whether a FileMutex's own
// lock/unlock is currently held (file_mutex_get() hands out a copy that stays valid regardless of
// later registry changes - see file_mutex_remove()).
struct FileMutexLedger {
std::vector<FileMutexEntry> entries;
FileMutexId next_id = 1;
Mutex mutex {};
FileMutexLedger() { mutex_construct(&mutex); }
~FileMutexLedger() { mutex_destruct(&mutex); }
void lock() { mutex_lock(&mutex); }
void unlock() { mutex_unlock(&mutex); }
};
static FileMutexLedger& get_ledger() {
static FileMutexLedger ledger;
return ledger;
}
extern "C" {
void file_mutex_register(const FileMutex* mutex, const char* path) {
// Skip if entry for path exists
for (auto& entry : mutex_entries) {
FileMutexId file_mutex_add(const FileMutex* mutex, const char* path) {
auto& ledger = get_ledger();
ledger.lock();
for (auto& entry : ledger.entries) {
if (entry.path == path) {
return;
FileMutexId existing_id = entry.id;
ledger.unlock();
return existing_id;
}
}
// Store a copy of the entry
mutex_entries.push_back({
FileMutexId new_id = ledger.next_id++;
ledger.entries.push_back({
.id = new_id,
.path = path,
.mutex = *mutex
});
ledger.unlock();
return new_id;
}
void file_mutex_remove(FileMutexId id) {
auto& ledger = get_ledger();
ledger.lock();
const auto iterator = std::ranges::find_if(ledger.entries, [id](const FileMutexEntry& entry) {
return entry.id == id;
});
if (iterator != ledger.entries.end()) {
// Plain erase, not swap-and-pop: file_mutex_get() matches first-registered-wins, so
// removal must preserve the relative order of the remaining entries.
ledger.entries.erase(iterator);
}
ledger.unlock();
}
void file_mutex_get(FileMutex* mutex, const char* path) {
auto& ledger = get_ledger();
std::string path_string = path;
for (auto& entry : mutex_entries) {
ledger.lock();
for (auto& entry : ledger.entries) {
// Match the mount path itself, or a descendant (e.g. "/sdcard" registered, "/sdcard/config.json" requested).
bool is_match = path_string == entry.path ||
(entry.path == "/" && !path_string.empty() && path_string[0] == '/') ||
(path_string.rfind(entry.path, 0) == 0 && path_string[entry.path.size()] == '/');
if (is_match) {
memcpy(mutex, &entry.mutex, sizeof(FileMutex));
ledger.unlock();
return;
}
}
ledger.unlock();
*mutex = no_mutex;
}

View File

@ -25,18 +25,22 @@ void* memory_alloc_with_policy(size_t size, const struct MemoryPolicy* policy) {
uint32_t required_caps = toHeapCaps(policy->required);
uint32_t desired_caps = toHeapCaps(policy->desired);
// heap_caps matches heaps via (heap->caps[prio] & caps) != 0 - a caps value of 0 (e.g.
// required_caps when policy->required wasn't set) can never match any heap, so the fallback
// must OR in MALLOC_CAP_DEFAULT to actually reach a general-purpose heap, same as ESP-IDF's
// own heap_caps_malloc_default() does.
void* ptr;
if (policy->alignment > 0) {
ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps | desired_caps);
if (ptr == nullptr && desired_caps != 0) {
// Desired caps couldn't be satisfied alongside the required ones - retry with
// required only, since desired is explicitly optional.
ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps);
ptr = heap_caps_aligned_alloc(policy->alignment, size, required_caps | MALLOC_CAP_DEFAULT);
}
} else {
ptr = heap_caps_malloc(size, required_caps | desired_caps);
if (ptr == nullptr && desired_caps != 0) {
ptr = heap_caps_malloc(size, required_caps);
ptr = heap_caps_malloc(size, required_caps | MALLOC_CAP_DEFAULT);
}
}
return ptr;
@ -50,7 +54,7 @@ void* memory_realloc_with_policy(void* ptr, size_t size, const struct MemoryPoli
// on fresh allocations (memory_alloc_with_policy/memory_calloc_with_policy).
void* result = heap_caps_realloc(ptr, size, required_caps | desired_caps);
if (result == nullptr && desired_caps != 0) {
result = heap_caps_realloc(ptr, size, required_caps);
result = heap_caps_realloc(ptr, size, required_caps | MALLOC_CAP_DEFAULT);
}
return result;
}
@ -63,12 +67,12 @@ void* memory_calloc_with_policy(size_t count, size_t size, const struct MemoryPo
if (policy->alignment > 0) {
ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps | desired_caps);
if (ptr == nullptr && desired_caps != 0) {
ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps);
ptr = heap_caps_aligned_calloc(policy->alignment, count, size, required_caps | MALLOC_CAP_DEFAULT);
}
} else {
ptr = heap_caps_calloc(count, size, required_caps | desired_caps);
if (ptr == nullptr && desired_caps != 0) {
ptr = heap_caps_calloc(count, size, required_caps);
ptr = heap_caps_calloc(count, size, required_caps | MALLOC_CAP_DEFAULT);
}
}
return ptr;

View File

@ -169,7 +169,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(display_get_backlight),
DEFINE_MODULE_SYMBOL(DISPLAY_TYPE),
// file_mutex
DEFINE_MODULE_SYMBOL(file_mutex_register),
DEFINE_MODULE_SYMBOL(file_mutex_add),
DEFINE_MODULE_SYMBOL(file_mutex_remove),
DEFINE_MODULE_SYMBOL(file_mutex_get),
DEFINE_MODULE_SYMBOL(file_mutex_lock),
DEFINE_MODULE_SYMBOL(file_mutex_try_lock),

View File

@ -48,10 +48,10 @@ TEST_CASE("file_mutex_get with zero registrations returns a no-op mutex") {
file_mutex_unlock(&mutex);
}
TEST_CASE("file_mutex_register/get with a single registration") {
TEST_CASE("file_mutex_add/get with a single registration") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = mock_try_lock, .unlock = mock_unlock };
file_mutex_register(&registered, "/mock1");
file_mutex_add(&registered, "/mock1");
FileMutex mutex;
@ -87,19 +87,19 @@ TEST_CASE("file_mutex_register/get with a single registration") {
// Re-registering the same path is a no-op: original callbacks remain in place.
FileMutex replacement = { .lock = nullptr, .try_lock = nullptr, .unlock = nullptr };
file_mutex_register(&replacement, "/mock1");
file_mutex_add(&replacement, "/mock1");
file_mutex_get(&mutex, "/mock1");
CHECK_EQ(mutex.lock, mock_lock);
}
TEST_CASE("file_mutex_register/get with two registrations resolves to the matching path") {
TEST_CASE("file_mutex_add/get with two registrations resolves to the matching path") {
reset_mocks();
FileMutex mutex_a = { .lock = mock_lock_a, .try_lock = nullptr, .unlock = nullptr };
FileMutex mutex_b = { .lock = mock_lock_b, .try_lock = nullptr, .unlock = nullptr };
file_mutex_register(&mutex_a, "/mock2a");
file_mutex_register(&mutex_b, "/mock2b");
file_mutex_add(&mutex_a, "/mock2a");
file_mutex_add(&mutex_b, "/mock2b");
FileMutex resolved;
@ -116,7 +116,75 @@ TEST_CASE("file_mutex_register/get with two registrations resolves to the matchi
// Registration order matters: the first matching entry wins, not the longest
// prefix. A mount nested under an earlier one is shadowed by it.
FileMutex mutex_nested = { .lock = nullptr, .try_lock = nullptr, .unlock = nullptr };
file_mutex_register(&mutex_nested, "/mock2a/nested");
file_mutex_add(&mutex_nested, "/mock2a/nested");
file_mutex_get(&resolved, "/mock2a/nested/file.txt");
CHECK_EQ(resolved.lock, mock_lock_a); // still /mock2a, registered first
}
TEST_CASE("file_mutex_add returns a valid id") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id = file_mutex_add(&registered, "/mockA");
CHECK_NE(id, FILE_MUTEX_ID_INVALID);
}
TEST_CASE("file_mutex_add with a duplicate path returns the existing id") {
reset_mocks();
FileMutex mutex_1 = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
FileMutex mutex_2 = { .lock = mock_lock_a, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id_1 = file_mutex_add(&mutex_1, "/mockB");
FileMutexId id_2 = file_mutex_add(&mutex_2, "/mockB");
CHECK_EQ(id_1, id_2);
FileMutex resolved;
file_mutex_get(&resolved, "/mockB");
CHECK_EQ(resolved.lock, mock_lock); // first registration's callbacks win, unchanged
}
TEST_CASE("file_mutex_remove removes a registration") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id = file_mutex_add(&registered, "/mockC");
FileMutex before;
file_mutex_get(&before, "/mockC");
CHECK_EQ(before.lock, mock_lock);
file_mutex_remove(id);
FileMutex after;
file_mutex_get(&after, "/mockC");
CHECK_EQ(after.lock, nullptr);
}
TEST_CASE("file_mutex_remove with an unknown id is a safe no-op") {
reset_mocks();
FileMutex registered = { .lock = mock_lock, .try_lock = nullptr, .unlock = nullptr };
file_mutex_add(&registered, "/mockD");
file_mutex_remove(999999); // never issued
file_mutex_remove(FILE_MUTEX_ID_INVALID);
FileMutex resolved;
file_mutex_get(&resolved, "/mockD");
CHECK_EQ(resolved.lock, mock_lock); // untouched
}
TEST_CASE("file_mutex_remove of one registration leaves others intact") {
reset_mocks();
FileMutex mutex_a = { .lock = mock_lock_a, .try_lock = nullptr, .unlock = nullptr };
FileMutex mutex_b = { .lock = mock_lock_b, .try_lock = nullptr, .unlock = nullptr };
FileMutexId id_a = file_mutex_add(&mutex_a, "/mockE1");
file_mutex_add(&mutex_b, "/mockE2");
file_mutex_remove(id_a);
FileMutex resolved_a;
file_mutex_get(&resolved_a, "/mockE1");
CHECK_EQ(resolved_a.lock, nullptr);
FileMutex resolved_b;
file_mutex_get(&resolved_b, "/mockE2");
CHECK_EQ(resolved_b.lock, mock_lock_b);
}

View File

@ -38,14 +38,14 @@
* - LV_STDLIB_RTTHREAD: RT-Thread implementation
* - LV_STDLIB_CUSTOM: Implement the functions externally
*/
#define LV_USE_STDLIB_MALLOC LV_STDLIB_CLIB
#define LV_USE_STDLIB_MALLOC LV_STDLIB_CUSTOM
#define LV_USE_STDLIB_STRING LV_STDLIB_BUILTIN
#define LV_USE_STDLIB_SPRINTF LV_STDLIB_BUILTIN
#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN
/*Size of the memory available for `lv_malloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (64 * 1024U) /*[bytes]*/
#define LV_MEM_SIZE (128 * 1024U) /*[bytes]*/
/*Size of the memory expand for `lv_malloc()` in bytes*/
#define LV_MEM_POOL_EXPAND_SIZE 0