Trying to finish keyboard

This commit is contained in:
Thomas Farstrike
2025-11-17 09:31:40 +01:00
parent b8a61d13b8
commit 47fda6e69f
5 changed files with 532 additions and 20 deletions
+38 -20
View File
@@ -20,6 +20,7 @@ Usage:
import lvgl as lv
import mpos.ui.theme
import time
class MposKeyboard:
@@ -60,6 +61,14 @@ class MposKeyboard:
# Store textarea reference (we DON'T pass it to LVGL to avoid double-typing)
self._textarea = None
# Track last mode switch time to prevent race conditions
# When user rapidly clicks mode buttons, button indices can get confused
# because index 29 is "abc" in numbers mode but "," in lowercase mode
self._last_mode_switch_time = 0
# Re-entrancy guard to prevent recursive event processing during mode switches
self._in_mode_switch = False
# Configure layouts
self._setup_layouts()
@@ -148,11 +157,20 @@ class MposKeyboard:
# This prevents LVGL's default handler from interfering
event.stop_processing()
# Re-entrancy guard: Skip processing if we're currently switching modes
# This prevents set_mode() from triggering recursive event processing
if self._in_mode_switch:
return
# Get the pressed button and its text
button = self._keyboard.get_selected_button()
current_mode = self._keyboard.get_mode()
text = self._keyboard.get_button_text(button)
# Ignore if no valid button text (can happen during initialization)
# DEBUG
print(f"[KBD] btn={button}, mode={current_mode}, text='{text}'")
# Ignore if no valid button text (can happen during mode switching)
if text is None:
return
@@ -245,29 +263,29 @@ class MposKeyboard:
mode: One of MODE_LOWERCASE, MODE_UPPERCASE, MODE_NUMBERS, MODE_SPECIALS
(can also accept standard LVGL modes)
"""
# 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
# Map modes to their layouts
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]),
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),
}
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)
# Set re-entrancy guard to block any events triggered during mode switch
self._in_mode_switch = True
self._keyboard.set_mode(mode)
try:
# Set the map for the new mode BEFORE calling set_mode()
# This prevents crashes from set_mode() being called with no map set
if mode in mode_info:
key_map, ctrl_map = mode_info[mode]
self._keyboard.set_map(mode, key_map, ctrl_map)
# Now switch to the new mode
self._keyboard.set_mode(mode)
finally:
# Always clear the guard, even if an exception occurs
self._in_mode_switch = False
# ========================================================================
# Python magic method for automatic method forwarding
+58
View File
@@ -0,0 +1,58 @@
"""
Manual test for the "abc" button bug with DEBUG OUTPUT.
Run with: ./scripts/run_desktop.sh tests/manual_test_abc_button_debug.py
This will show debug output when you click the "abc" button.
Watch the terminal to see what's happening!
"""
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
# Get active screen
screen = lv.screen_active()
screen.clean()
# Create title
title = lv.label(screen)
title.set_text("ABC Button Debug Test")
title.align(lv.ALIGN.TOP_MID, 0, 5)
# Create instructions
instructions = lv.label(screen)
instructions.set_text(
"Watch the TERMINAL output!\n"
"\n"
"1. Click '?123' to go to numbers mode\n"
"2. Click 'abc' to go back to lowercase\n"
"3. Check terminal for debug output\n"
"4. Check if comma appears in textarea"
)
instructions.set_style_text_align(lv.TEXT_ALIGN.LEFT, 0)
instructions.align(lv.ALIGN.TOP_LEFT, 10, 30)
# Create textarea
textarea = lv.textarea(screen)
textarea.set_size(280, 30)
textarea.set_one_line(True)
textarea.align(lv.ALIGN.TOP_MID, 0, 120)
textarea.set_placeholder_text("Type here...")
# Create keyboard
keyboard = MposKeyboard(screen)
keyboard.set_textarea(textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
print("\n" + "="*70)
print("ABC BUTTON DEBUG TEST")
print("="*70)
print("Instructions:")
print("1. The keyboard starts in LOWERCASE mode")
print("2. Click the '?123' button (bottom left) to switch to NUMBERS mode")
print("3. Click the 'abc' button (bottom left) to switch back to LOWERCASE")
print("4. Watch this terminal for [KEYBOARD DEBUG] messages")
print("5. Check if a comma appears in the textarea")
print("="*70)
print("\nWaiting for button clicks...")
print()
+107
View File
@@ -0,0 +1,107 @@
"""
Automated test that simulates clicking the abc button and shows debug output.
This will show us exactly what's happening when the abc button is clicked.
Usage:
Desktop: ./tests/unittest.sh tests/test_graphical_abc_button_debug.py
"""
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
class TestAbcButtonDebug(unittest.TestCase):
"""Test that shows debug output when clicking abc button."""
def setUp(self):
"""Set up test fixtures."""
self.screen = lv.obj()
self.screen.set_size(320, 240)
# Create textarea
self.textarea = lv.textarea(self.screen)
self.textarea.set_size(280, 40)
self.textarea.align(lv.ALIGN.TOP_MID, 0, 10)
self.textarea.set_one_line(True)
# Load screen
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_simulate_abc_button_click(self):
"""
Simulate clicking the abc button and show what happens.
"""
print("\n" + "="*70)
print("SIMULATING ABC BUTTON CLICK - WATCH FOR DEBUG OUTPUT")
print("="*70)
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
# Start in lowercase, switch to numbers
print("\n>>> Switching to NUMBERS mode...")
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(10)
# Wait for debounce period to expire (150ms + margin)
import time
print(">>> Waiting 200ms for debounce period to expire...")
time.sleep(0.2)
# Clear textarea
self.textarea.set_text("")
print(f">>> Textarea cleared: '{self.textarea.get_text()}'")
# Find the "abc" button
abc_button_index = None
for i in range(100):
try:
text = keyboard.get_button_text(i)
if text == "abc":
abc_button_index = i
print(f">>> Found 'abc' button at index {abc_button_index}")
break
except:
pass
# Now simulate what happens when user TOUCHES the button
# When user touches a button, LVGL's button matrix:
# 1. Sets the button as selected
# 2. Triggers VALUE_CHANGED event
print(f"\n>>> Simulating user clicking button {abc_button_index}...")
print(f">>> Before click: textarea = '{self.textarea.get_text()}'")
print("\n--- DEBUG OUTPUT SHOULD APPEAR BELOW ---\n")
# Trigger the VALUE_CHANGED event which our handler catches
# This simulates a real button press
keyboard._keyboard.send_event(lv.EVENT.VALUE_CHANGED, None)
wait_for_render(5)
print("\n--- END DEBUG OUTPUT ---\n")
textarea_after = self.textarea.get_text()
print(f">>> After click: textarea = '{textarea_after}'")
if textarea_after != "":
print(f"\n❌ BUG CONFIRMED!")
print(f" Expected: '' (empty)")
print(f" Got: '{textarea_after}'")
else:
print(f"\n✓ No text added (but check debug output above)")
print("="*70)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,162 @@
"""
Test for the abc button click bug - comma being added.
This test actually CLICKS the abc button to reproduce the comma bug.
Usage:
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_abc_click_bug.py
"""
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
class TestAbcButtonClickBug(unittest.TestCase):
"""Test that clicking abc button doesn't add comma."""
def setUp(self):
"""Set up test fixtures."""
self.screen = lv.obj()
self.screen.set_size(320, 240)
# Create textarea
self.textarea = lv.textarea(self.screen)
self.textarea.set_size(280, 40)
self.textarea.align(lv.ALIGN.TOP_MID, 0, 10)
self.textarea.set_one_line(True)
# Load screen
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_clicking_abc_button_should_not_add_comma(self):
"""
Test that actually CLICKING the abc button doesn't add comma.
This is the REAL test - simulating actual user clicks.
"""
print("\n=== Testing ACTUAL CLICKING of abc button ===")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
# Start in lowercase, switch to numbers
print("Step 1: Switch to numbers mode")
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(10)
# Clear textarea
self.textarea.set_text("")
print(f" Textarea cleared: '{self.textarea.get_text()}'")
# Find the "abc" button
abc_button_index = None
for i in range(100):
try:
text = keyboard.get_button_text(i)
if text == "abc":
abc_button_index = i
print(f" Found 'abc' button at index {i}")
break
except:
pass
self.assertIsNotNone(abc_button_index, "Should find 'abc' button in numbers mode")
# ACTUALLY CLICK THE BUTTON
print(f"\nStep 2: ACTUALLY CLICK button index {abc_button_index}")
print(f" Before click: textarea='{self.textarea.get_text()}'")
# Simulate button click by sending CLICKED event to the button matrix
# Get the underlying button matrix object
btnm = keyboard._keyboard
# Method 1: Try to programmatically click the button
# This simulates what happens when user actually touches the button
btnm.set_selected_button(abc_button_index)
wait_for_render(2)
# Send the VALUE_CHANGED event
btnm.send_event(lv.EVENT.VALUE_CHANGED, None)
wait_for_render(5)
textarea_after = self.textarea.get_text()
print(f" After click: textarea='{textarea_after}'")
# Check if comma was added
if "," in textarea_after:
print(f"\n ❌ BUG CONFIRMED: Comma was added!")
print(f" Textarea contains: '{textarea_after}'")
self.fail(f"BUG: Clicking 'abc' button added comma! Textarea: '{textarea_after}'")
# Also check if anything else was added
if textarea_after != "":
print(f"\n ❌ BUG CONFIRMED: Something was added!")
print(f" Expected: ''")
print(f" Got: '{textarea_after}'")
self.fail(f"BUG: Clicking 'abc' button added text! Textarea: '{textarea_after}'")
print(f"\n ✓ SUCCESS: No text added, textarea is still empty")
def test_clicking_abc_multiple_times(self):
"""
Test clicking abc button multiple times in a row.
"""
print("\n=== Testing MULTIPLE clicks of abc button ===")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
for attempt in range(5):
print(f"\n--- Attempt {attempt + 1} ---")
# Go to numbers mode
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(10)
# Clear textarea
self.textarea.set_text("")
# Find abc button
abc_button_index = None
for i in range(100):
try:
text = keyboard.get_button_text(i)
if text == "abc":
abc_button_index = i
break
except:
pass
# Click it
print(f"Clicking 'abc' at index {abc_button_index}")
keyboard._keyboard.set_selected_button(abc_button_index)
wait_for_render(2)
keyboard._keyboard.send_event(lv.EVENT.VALUE_CHANGED, None)
wait_for_render(5)
textarea_text = self.textarea.get_text()
print(f" Result: textarea='{textarea_text}'")
if textarea_text != "":
print(f" ❌ FAIL on attempt {attempt + 1}: Got '{textarea_text}'")
self.fail(f"Attempt {attempt + 1}: Clicking 'abc' added '{textarea_text}'")
else:
print(f" ✓ OK")
print("\n✓ SUCCESS: All 5 attempts worked correctly")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,167 @@
"""
Test for rapid mode switching bug (clicking ?123/abc rapidly).
This test reproduces:
1. Comma being added when clicking "abc" button
2. Intermittent crashes when rapidly clicking mode switch buttons
Usage:
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_rapid_mode_switch.py
"""
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
class TestRapidModeSwitching(unittest.TestCase):
"""Test rapid mode switching between lowercase and numbers."""
def setUp(self):
"""Set up test fixtures."""
self.screen = lv.obj()
self.screen.set_size(320, 240)
# Create textarea
self.textarea = lv.textarea(self.screen)
self.textarea.set_size(280, 40)
self.textarea.align(lv.ALIGN.TOP_MID, 0, 10)
self.textarea.set_one_line(True)
# Load screen
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_clicking_abc_button(self):
"""
Rapidly click the "abc" button to reproduce the comma bug and crash.
Expected: Clicking "abc" should NOT add comma to textarea
Bug: Comma is being added, suggesting button index confusion
"""
print("\n=== Testing rapid clicking of abc button ===")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
# Start in lowercase, switch to numbers
print("Step 1: Switch to numbers mode")
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(10) # Give time to settle
# Clear textarea
self.textarea.set_text("")
# Now find the "abc" button
abc_button_index = None
for i in range(100):
try:
text = keyboard.get_button_text(i)
if text == "abc":
abc_button_index = i
print(f" Found 'abc' button at index {i}")
break
except:
pass
self.assertIsNotNone(abc_button_index, "Should find 'abc' button in numbers mode")
# Simulate rapid clicking by alternating modes
print("\nStep 2: Rapidly switch modes by simulating abc/?123 clicks")
for i in range(10):
# Get current mode
current_mode = keyboard._keyboard.get_mode()
# Clear text before click
textarea_before = self.textarea.get_text()
print(f" Click {i+1}: mode={current_mode}, textarea='{textarea_before}'")
if current_mode == MposKeyboard.MODE_NUMBERS or current_mode == lv.keyboard.MODE.NUMBER:
# Click "abc" to go to lowercase
keyboard.set_mode(MposKeyboard.MODE_LOWERCASE)
else:
# Click "?123" to go to numbers
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(2)
# Check if text changed (BUG: should not change!)
textarea_after = self.textarea.get_text()
if textarea_after != textarea_before:
print(f" ERROR: Text changed from '{textarea_before}' to '{textarea_after}'")
self.fail(f"BUG: Clicking mode switch button added '{textarea_after}' to textarea")
# Verify textarea is still empty
final_text = self.textarea.get_text()
print(f"\nFinal textarea text: '{final_text}'")
self.assertEqual(final_text, "",
f"Textarea should be empty after mode switches, but contains: '{final_text}'")
print("SUCCESS: No spurious characters added during rapid mode switching")
def test_button_indices_after_mode_switch(self):
"""
Test that button indices remain consistent after mode switches.
This helps identify if the comma bug is due to button index confusion.
"""
print("\n=== Testing button indices after mode switch ===")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
wait_for_render(10)
# Map button indices in lowercase mode
print("\nButton indices in LOWERCASE mode:")
keyboard.set_mode(MposKeyboard.MODE_LOWERCASE)
wait_for_render(10)
lowercase_buttons = {}
for i in range(40):
try:
text = keyboard.get_button_text(i)
if text in ["?123", ",", "abc", lv.SYMBOL.UP]:
lowercase_buttons[text] = i
print(f" '{text}' at index {i}")
except:
pass
# Map button indices in numbers mode
print("\nButton indices in NUMBERS mode:")
keyboard.set_mode(MposKeyboard.MODE_NUMBERS)
wait_for_render(10)
numbers_buttons = {}
for i in range(40):
try:
text = keyboard.get_button_text(i)
if text in ["?123", ",", "abc", "=\\<"]:
numbers_buttons[text] = i
print(f" '{text}' at index {i}")
except:
pass
# Check if comma and abc are at same index
if "," in lowercase_buttons and "abc" in numbers_buttons:
comma_idx = lowercase_buttons[","]
abc_idx = numbers_buttons["abc"]
print(f"\nComparison:")
print(f" Comma in lowercase: index {comma_idx}")
print(f" 'abc' in numbers: index {abc_idx}")
if comma_idx == abc_idx:
print(" WARNING: Comma and 'abc' share the same index!")
print(" This could explain why comma appears when clicking 'abc'")
if __name__ == "__main__":
unittest.main()