From b8a61d13b8e16be5c9d10f2b0a965183ab499c45 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 17 Nov 2025 07:20:47 +0100 Subject: [PATCH] update keyboard --- internal_filesystem/lib/mpos/ui/keyboard.py | 59 +++---- ...t_graphical_keyboard_crash_reproduction.py | 149 ++++++++++++++++++ 2 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 tests/test_graphical_keyboard_crash_reproduction.py diff --git a/internal_filesystem/lib/mpos/ui/keyboard.py b/internal_filesystem/lib/mpos/ui/keyboard.py index d81b0abc..b3d559d0 100644 --- a/internal_filesystem/lib/mpos/ui/keyboard.py +++ b/internal_filesystem/lib/mpos/ui/keyboard.py @@ -63,25 +63,12 @@ class MposKeyboard: # Configure layouts self._setup_layouts() - # Initialize ALL keyboard mode maps - # Register to BOTH our USER modes AND standard LVGL modes - # This prevents LVGL from using default maps when it internally switches modes - - # Our USER modes (what we use in our API) - self._keyboard.set_map(self.MODE_LOWERCASE, self._lowercase_map, self._lowercase_ctrl) - self._keyboard.set_map(self.MODE_UPPERCASE, self._uppercase_map, self._uppercase_ctrl) - self._keyboard.set_map(self.MODE_NUMBERS, self._numbers_map, self._numbers_ctrl) - self._keyboard.set_map(self.MODE_SPECIALS, self._specials_map, self._specials_ctrl) - - # ALSO register to standard LVGL modes (what LVGL uses internally) - # This catches cases where LVGL internally calls set_mode(TEXT_LOWER) - self._keyboard.set_map(lv.keyboard.MODE.TEXT_LOWER, self._lowercase_map, self._lowercase_ctrl) - self._keyboard.set_map(lv.keyboard.MODE.TEXT_UPPER, self._uppercase_map, self._uppercase_ctrl) - self._keyboard.set_map(lv.keyboard.MODE.NUMBER, self._numbers_map, self._numbers_ctrl) - self._keyboard.set_map(lv.keyboard.MODE.SPECIAL, self._specials_map, self._specials_ctrl) - # Set default mode to lowercase - self._keyboard.set_mode(self.MODE_LOWERCASE) + # IMPORTANT: We do NOT call set_map() here in __init__. + # Instead, set_mode() will call set_map() immediately before set_mode(). + # This matches the proof-of-concept pattern and prevents crashes from + # calling set_map() multiple times which can corrupt button matrix state. + self.set_mode(self.MODE_LOWERCASE) # Add event handler for custom behavior # We need to handle ALL events to catch mode changes that LVGL might trigger @@ -258,25 +245,27 @@ class MposKeyboard: mode: One of MODE_LOWERCASE, MODE_UPPERCASE, MODE_NUMBERS, MODE_SPECIALS (can also accept standard LVGL modes) """ - # Map mode constants to their corresponding map arrays - # Support both our USER modes and standard LVGL modes - mode_maps = { - self.MODE_LOWERCASE: (self._lowercase_map, self._lowercase_ctrl), - self.MODE_UPPERCASE: (self._uppercase_map, self._uppercase_ctrl), - self.MODE_NUMBERS: (self._numbers_map, self._numbers_ctrl), - self.MODE_SPECIALS: (self._specials_map, self._specials_ctrl), - # Also map standard LVGL modes - lv.keyboard.MODE.TEXT_LOWER: (self._lowercase_map, self._lowercase_ctrl), - lv.keyboard.MODE.TEXT_UPPER: (self._uppercase_map, self._uppercase_ctrl), - lv.keyboard.MODE.NUMBER: (self._numbers_map, self._numbers_ctrl), - lv.keyboard.MODE.SPECIAL: (self._specials_map, self._specials_ctrl), + # Determine which layout we're switching to + # We need to set the map for BOTH the USER mode and the corresponding standard mode + # to prevent crashes if LVGL internally switches between them + mode_info = { + self.MODE_LOWERCASE: (self._lowercase_map, self._lowercase_ctrl, [self.MODE_LOWERCASE, lv.keyboard.MODE.TEXT_LOWER]), + self.MODE_UPPERCASE: (self._uppercase_map, self._uppercase_ctrl, [self.MODE_UPPERCASE, lv.keyboard.MODE.TEXT_UPPER]), + self.MODE_NUMBERS: (self._numbers_map, self._numbers_ctrl, [self.MODE_NUMBERS, lv.keyboard.MODE.NUMBER]), + self.MODE_SPECIALS: (self._specials_map, self._specials_ctrl, [self.MODE_SPECIALS, lv.keyboard.MODE.SPECIAL]), + # Also support standard LVGL modes + lv.keyboard.MODE.TEXT_LOWER: (self._lowercase_map, self._lowercase_ctrl, [self.MODE_LOWERCASE, lv.keyboard.MODE.TEXT_LOWER]), + lv.keyboard.MODE.TEXT_UPPER: (self._uppercase_map, self._uppercase_ctrl, [self.MODE_UPPERCASE, lv.keyboard.MODE.TEXT_UPPER]), + lv.keyboard.MODE.NUMBER: (self._numbers_map, self._numbers_ctrl, [self.MODE_NUMBERS, lv.keyboard.MODE.NUMBER]), + lv.keyboard.MODE.SPECIAL: (self._specials_map, self._specials_ctrl, [self.MODE_SPECIALS, lv.keyboard.MODE.SPECIAL]), } - if mode in mode_maps: - key_map, ctrl_map = mode_maps[mode] - # CRITICAL: Always call set_map() BEFORE set_mode() - # This prevents lv_keyboard_update_map() crashes - self._keyboard.set_map(mode, key_map, ctrl_map) + if mode in mode_info: + key_map, ctrl_map, mode_list = mode_info[mode] + # CRITICAL: Set the map for BOTH modes to prevent NULL pointer crashes + # This ensures the map is set regardless of which mode LVGL uses internally + for m in mode_list: + self._keyboard.set_map(m, key_map, ctrl_map) self._keyboard.set_mode(mode) diff --git a/tests/test_graphical_keyboard_crash_reproduction.py b/tests/test_graphical_keyboard_crash_reproduction.py new file mode 100644 index 00000000..c1399cf0 --- /dev/null +++ b/tests/test_graphical_keyboard_crash_reproduction.py @@ -0,0 +1,149 @@ +""" +Test to reproduce the lv_strcmp crash during keyboard mode switching. + +The crash happens in buttonmatrix drawing code when map_p[txt_i] is NULL. + +Usage: + Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_crash_reproduction.py +""" + +import unittest +import lvgl as lv +from mpos.ui.keyboard import MposKeyboard +from graphical_test_helper import wait_for_render + + +class TestKeyboardCrash(unittest.TestCase): + """Test to reproduce keyboard crashes.""" + + def setUp(self): + """Set up test fixtures.""" + self.screen = lv.obj() + self.screen.set_size(320, 240) + lv.screen_load(self.screen) + wait_for_render(5) + + def tearDown(self): + """Clean up.""" + lv.screen_load(lv.obj()) + wait_for_render(5) + + def test_rapid_mode_switching(self): + """ + Rapidly switch between modes to trigger the crash. + + The crash occurs when btnm->map_p[txt_i] is NULL during drawing. + """ + print("\n=== Testing rapid mode switching ===") + + textarea = lv.textarea(self.screen) + textarea.set_size(280, 40) + textarea.align(lv.ALIGN.TOP_MID, 0, 10) + textarea.set_one_line(True) + wait_for_render(5) + + keyboard = MposKeyboard(self.screen) + keyboard.set_textarea(textarea) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + print("Rapidly switching modes...") + modes = [ + MposKeyboard.MODE_LOWERCASE, + MposKeyboard.MODE_NUMBERS, + MposKeyboard.MODE_LOWERCASE, + MposKeyboard.MODE_UPPERCASE, + MposKeyboard.MODE_LOWERCASE, + MposKeyboard.MODE_NUMBERS, + MposKeyboard.MODE_SPECIALS, + MposKeyboard.MODE_NUMBERS, + MposKeyboard.MODE_LOWERCASE, + ] + + for i, mode in enumerate(modes): + print(f" Switch {i+1}: mode {mode}") + keyboard.set_mode(mode) + # Force rendering - this is where the crash happens + wait_for_render(2) + + print("SUCCESS: No crash during rapid switching") + + def test_mode_switching_with_standard_modes(self): + """ + Test switching using standard LVGL modes (TEXT_LOWER, etc). + + This tests if LVGL internally switching modes causes the crash. + """ + print("\n=== Testing with standard LVGL modes ===") + + textarea = lv.textarea(self.screen) + textarea.set_size(280, 40) + textarea.align(lv.ALIGN.TOP_MID, 0, 10) + textarea.set_one_line(True) + wait_for_render(5) + + keyboard = MposKeyboard(self.screen) + keyboard.set_textarea(textarea) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + print("Switching using standard LVGL modes...") + + # Try standard modes + print(" Switching to TEXT_LOWER") + keyboard._keyboard.set_mode(lv.keyboard.MODE.TEXT_LOWER) + wait_for_render(5) + + print(" Switching to NUMBER") + keyboard._keyboard.set_mode(lv.keyboard.MODE.NUMBER) + wait_for_render(5) + + print(" Switching back to TEXT_LOWER") + keyboard._keyboard.set_mode(lv.keyboard.MODE.TEXT_LOWER) + wait_for_render(5) + + print("SUCCESS: No crash with standard modes") + + def test_multiple_keyboards(self): + """ + Test creating multiple keyboards to see if that causes issues. + """ + print("\n=== Testing multiple keyboard creation ===") + + textarea = lv.textarea(self.screen) + textarea.set_size(280, 40) + textarea.align(lv.ALIGN.TOP_MID, 0, 10) + textarea.set_one_line(True) + wait_for_render(5) + + # Create first keyboard + print("Creating keyboard 1...") + keyboard1 = MposKeyboard(self.screen) + keyboard1.set_textarea(textarea) + keyboard1.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + print("Switching modes on keyboard 1...") + keyboard1.set_mode(MposKeyboard.MODE_NUMBERS) + wait_for_render(5) + + print("Deleting keyboard 1...") + keyboard1._keyboard.delete() + wait_for_render(5) + + # Create second keyboard + print("Creating keyboard 2...") + keyboard2 = MposKeyboard(self.screen) + keyboard2.set_textarea(textarea) + keyboard2.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + print("Switching modes on keyboard 2...") + keyboard2.set_mode(MposKeyboard.MODE_UPPERCASE) + wait_for_render(5) + + print("SUCCESS: Multiple keyboards work") + + +if __name__ == "__main__": + unittest.main()