diff --git a/Buildscripts/module.cmake b/Buildscripts/module.cmake index 9c148b3a7..a950ccc2a 100644 --- a/Buildscripts/module.cmake +++ b/Buildscripts/module.cmake @@ -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) diff --git a/Buildscripts/sdkconfig/default.properties b/Buildscripts/sdkconfig/default.properties index 268021b17..c20cec826 100644 --- a/Buildscripts/sdkconfig/default.properties +++ b/Buildscripts/sdkconfig/default.properties @@ -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 diff --git a/Devices/cl32/cl32.dts b/Devices/cl32/cl32.dts index 9027c8e99..042b2256a 100644 --- a/Devices/cl32/cl32.dts +++ b/Devices/cl32/cl32.dts @@ -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>; diff --git a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts index 0be730b59..d8ca0f0ab 100644 --- a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts +++ b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts @@ -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>; diff --git a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts index dfa466002..a8db46f48 100644 --- a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts +++ b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts @@ -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>; diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts index 48c571c7c..d1bfc61be 100644 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts +++ b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts @@ -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 ]; diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp index 429b317cd..9cfb27710 100644 --- a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -17,8 +17,6 @@ #include #include -#include - #include #include @@ -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; diff --git a/Devices/simulator/Source/drivers/sdl_input.cpp b/Devices/simulator/Source/drivers/sdl_input.cpp index 86aea8c81..002a2d069 100644 --- a/Devices/simulator/Source/drivers/sdl_input.cpp +++ b/Devices/simulator/Source/drivers/sdl_input.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "sdl_input.h" -#include +#include #include @@ -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. diff --git a/Drivers/button-control-module/source/button_control.cpp b/Drivers/button-control-module/source/button_control.cpp index 9facf5e60..4a32ffdb6 100644 --- a/Drivers/button-control-module/source/button_control.cpp +++ b/Drivers/button-control-module/source/button_control.cpp @@ -12,8 +12,6 @@ #include #include -#include - #include #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); diff --git a/Drivers/m5stack-module/source/cardputer_adv_keyboard.cpp b/Drivers/m5stack-module/source/cardputer_adv_keyboard.cpp index 72db94b07..a7210bd2b 100644 --- a/Drivers/m5stack-module/source/cardputer_adv_keyboard.cpp +++ b/Drivers/m5stack-module/source/cardputer_adv_keyboard.cpp @@ -14,8 +14,6 @@ #include #include -#include - #include 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 { diff --git a/Drivers/m5stack-module/source/cardputer_keyboard.cpp b/Drivers/m5stack-module/source/cardputer_keyboard.cpp index 43722fae4..91d7bc0b5 100644 --- a/Drivers/m5stack-module/source/cardputer_keyboard.cpp +++ b/Drivers/m5stack-module/source/cardputer_keyboard.cpp @@ -14,8 +14,6 @@ #include #include -#include - #include 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; } } diff --git a/Drivers/tca8418-module/bindings/ti,tca8418.yaml b/Drivers/tca8418-module/bindings/ti,tca8418.yaml index f6b2018f6..5a7856043 100644 --- a/Drivers/tca8418-module/bindings/ti,tca8418.yaml +++ b/Drivers/tca8418-module/bindings/ti,tca8418.yaml @@ -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 diff --git a/Modules/lvgl-module/CMakeLists.txt b/Modules/lvgl-module/CMakeLists.txt index b170cad1a..7a5bc78f8 100644 --- a/Modules/lvgl-module/CMakeLists.txt +++ b/Modules/lvgl-module/CMakeLists.txt @@ -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) diff --git a/Modules/lvgl-module/README.md b/Modules/lvgl-module/README.md index 121062d4e..d36133022 100644 --- a/Modules/lvgl-module/README.md +++ b/Modules/lvgl-module/README.md @@ -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. \ No newline at end of file diff --git a/Modules/lvgl-module/source/arch/lvgl_esp32.c b/Modules/lvgl-module/source/arch/lvgl_esp32.c index a7f879844..63d1134ce 100644 --- a/Modules/lvgl-module/source/arch/lvgl_esp32.c +++ b/Modules/lvgl-module/source/arch/lvgl_esp32.c @@ -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(); } diff --git a/Modules/lvgl-module/source/devices/keyboard.cpp b/Modules/lvgl-module/source/devices/keyboard.cpp index a0b725295..b50e88d84 100644 --- a/Modules/lvgl-module/source/devices/keyboard.cpp +++ b/Modules/lvgl-module/source/devices/keyboard.cpp @@ -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(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; } diff --git a/Modules/lvgl-module/source/lv_mem_custom.c b/Modules/lvgl-module/source/lv_mem_custom.c new file mode 100644 index 000000000..64bcab1c6 --- /dev/null +++ b/Modules/lvgl-module/source/lv_mem_custom.c @@ -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 +#include + +// 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; +} diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index 00dbe6524..25b973f12 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -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; } diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index d28d32705..7d8041ac0 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -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, diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 6c6356b78..4b5631494 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -273,7 +273,6 @@ void runBootSequence(TickType_t startTime) { #endif if (!setupUsbBootMode()) { - LOG_I(TAG, "initFromBootApp"); registerApps(); waitForMinimalSplashDuration(startTime); startNextApp(); diff --git a/Tactility/Source/file/FileMutexLvgl.cpp b/Tactility/Source/file/FileMutexLvgl.cpp index 8a4b8094f..3d4eeffcc 100644 --- a/Tactility/Source/file/FileMutexLvgl.cpp +++ b/Tactility/Source/file/FileMutexLvgl.cpp @@ -6,12 +6,16 @@ #include #include +#include + constexpr auto* TAG = "file_mutex_lvgl"; struct Device; namespace { +std::vector 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(®istered_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* registeredIds; }; - Context ctx = { .mountPath = mount_path }; + Context ctx = { .mountPath = mount_path, .registeredIds = static_cast*>(context) }; device_for_each_child(parent, &ctx, [](Device* child, void* context) -> bool { Context* ctx = static_cast(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(®istered_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*>(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(); +} + } diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index 20d4e849d..74978441f 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -11,11 +11,43 @@ extern "C" { #include #include +/** + * @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"). diff --git a/TactilityKernel/include/tactility/filesystem/file_mutex.h b/TactilityKernel/include/tactility/filesystem/file_mutex.h index cec151878..c0a165b93 100644 --- a/TactilityKernel/include/tactility/filesystem/file_mutex.h +++ b/TactilityKernel/include/tactility/filesystem/file_mutex.h @@ -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. diff --git a/TactilityKernel/source/filesystem/file_mutex.cpp b/TactilityKernel/source/filesystem/file_mutex.cpp index a8bbc9f92..72f648d16 100644 --- a/TactilityKernel/source/filesystem/file_mutex.cpp +++ b/TactilityKernel/source/filesystem/file_mutex.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#include #include #include #include @@ -12,41 +14,89 @@ static const FileMutex no_mutex = { }; struct FileMutexEntry { + FileMutexId id; std::string path; FileMutex mutex; }; -static std::vector 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 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; } diff --git a/TactilityKernel/source/memory_esp32.cpp b/TactilityKernel/source/memory_esp32.cpp index f52ab4620..44d467925 100644 --- a/TactilityKernel/source/memory_esp32.cpp +++ b/TactilityKernel/source/memory_esp32.cpp @@ -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; diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 0f1d93030..d55076948 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -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), diff --git a/TactilityKernel/tests/source/file_mutex_test.cpp b/TactilityKernel/tests/source/file_mutex_test.cpp index 8e92e009c..a67f14c55 100644 --- a/TactilityKernel/tests/source/file_mutex_test.cpp +++ b/TactilityKernel/tests/source/file_mutex_test.cpp @@ -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(®istered, "/mock1"); + file_mutex_add(®istered, "/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(®istered, "/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(®istered, "/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(®istered, "/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); +} diff --git a/lv_conf.h b/lv_conf.h index b7f3421f3..d5ba25e75 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -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