From a2a8b8008554f741789f6e636e648a9f8be4542c Mon Sep 17 00:00:00 2001 From: tadeubas Date: Tue, 2 Dec 2025 21:11:11 -0300 Subject: [PATCH] feat: ignore edge touches and bad swipes --- CHANGELOG.md | 3 +- src/krux/input.py | 34 ++++--- src/krux/pages/__init__.py | 137 ++++++++++++---------------- src/krux/pages/keypads.py | 13 ++- src/krux/pages/mnemonic_editor.py | 28 +++--- src/krux/pages/settings_page.py | 68 ++++++++------ src/krux/pages/stack_1248.py | 25 +++-- src/krux/pages/tiny_seed.py | 25 +++-- src/krux/touch.py | 133 +++++++++++++++------------ tests/pages/test_encryption_ui.py | 47 ++++++---- tests/pages/test_keypads.py | 10 ++ tests/pages/test_menu.py | 10 ++ tests/pages/test_mnemonic_editor.py | 1 + tests/pages/test_settings_page.py | 4 + tests/pages/test_stackbit.py | 18 +++- tests/pages/test_tiny_seed.py | 2 + tests/test_input.py | 25 ++++- tests/test_touch.py | 75 ++++++++++++--- 18 files changed, 421 insertions(+), 237 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9295d31f..c27481999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ Exported Uniform Resource (UR) QR codes, a widely adopted standard for exchangin ### Other Bug Fixes and Improvements - Settings: Reduced default _Buttons Debounce_ value (with an even lower default on _M5StickV_) - Settings: Expanded value ranges for _Touch Threshold_ and _Buttons Debounce_ -- Swipe handling: Detection threshold has been slightly reduced +- Swipe handling: Diagonal and long-hold swipes are now discarded, and the swipe detection threshold has been slightly reduced +- Touch handling: Discards touches near edges of adjacent regions - Keypad: Added backtick **`** - Bugfix: Screensaver not activating in menu pages without statusbar - Embit: Improved BIP39 mnemonic validation diff --git a/src/krux/input.py b/src/krux/input.py index 0eb076203..22a41349f 100644 --- a/src/krux/input.py +++ b/src/krux/input.py @@ -19,6 +19,8 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# pylint: disable=unnecessary-lambda + import time import board from .wdt import wdt @@ -35,6 +37,7 @@ SWIPE_LEFT = 5 SWIPE_UP = 6 SWIPE_DOWN = 7 +SWIPE_FAIL = 99 FAST_FORWARD = 8 FAST_BACKWARD = 9 @@ -175,29 +178,30 @@ def touch_event(self, validate_position=True): return self.touch.event(validate_position) return False - def swipe_right_value(self): - """Intermediary method to pull touch gesture, if touch available""" + def _swipe_check_value(self, swipe_fnc): if kboard.has_touchscreen: - return self.touch.swipe_right_value() + return swipe_fnc() return RELEASED + def swipe_none_value(self): + """Intermediary method to pull touch gesture, if touch available""" + return self._swipe_check_value(lambda: self.touch.swipe_none_value()) + + def swipe_right_value(self): + """Intermediary method to pull touch gesture, if touch available""" + return self._swipe_check_value(lambda: self.touch.swipe_right_value()) + def swipe_left_value(self): """Intermediary method to pull touch gesture, if touch available""" - if kboard.has_touchscreen: - return self.touch.swipe_left_value() - return RELEASED + return self._swipe_check_value(lambda: self.touch.swipe_left_value()) def swipe_up_value(self): """Intermediary method to pull touch gesture, if touch available""" - if kboard.has_touchscreen: - return self.touch.swipe_up_value() - return RELEASED + return self._swipe_check_value(lambda: self.touch.swipe_up_value()) def swipe_down_value(self): """Intermediary method to pull touch gesture, if touch available""" - if kboard.has_touchscreen: - return self.touch.swipe_down_value() - return RELEASED + return self._swipe_check_value(lambda: self.touch.swipe_down_value()) def wdt_feed_inc_entropy(self): """Feeds the watchdog and increments the input's entropy""" @@ -310,6 +314,10 @@ def _handle_touch_input(): while self.touch_value() == PRESSED: self.wdt_feed_inc_entropy() self.buttons_active = False + + # Check if was a swipe + if self.swipe_none_value() == PRESSED: + return SWIPE_FAIL if self.swipe_right_value() == PRESSED: return SWIPE_RIGHT if self.swipe_left_value() == PRESSED: @@ -318,6 +326,8 @@ def _handle_touch_input(): return SWIPE_UP if self.swipe_down_value() == PRESSED: return SWIPE_DOWN + + # was a simple touch return BUTTON_TOUCH if btn in [BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV]: diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 5e954c093..68cbf2d06 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -33,6 +33,7 @@ BUTTON_TOUCH, SWIPE_DOWN, SWIPE_UP, + SWIPE_FAIL, FAST_FORWARD, FAST_BACKWARD, SWIPE_LEFT, @@ -157,7 +158,7 @@ def capture_from_keypad( """ buffer = starting_buffer pad = Keypad(self.ctx, keysets, possible_keys_fn) - swipe_has_not_been_used = True + swipe_used = False show_swipe_hint = False while True: self.ctx.display.clear() @@ -171,9 +172,12 @@ def capture_from_keypad( show_swipe_hint = False # unless overridden by a particular key, # don't show the swipe hint after a key press - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - btn = pad.touch_to_physical() + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + btn = pad.touch_to_physical() if btn == BUTTON_ENTER: pad.moving_forward = True changed = False @@ -193,8 +197,7 @@ def capture_from_keypad( elif pad.cur_key_index == pad.go_index: break elif pad.cur_key_index == pad.more_index: - swipeable = kboard.has_touchscreen - if swipeable and swipe_has_not_been_used: + if kboard.has_touchscreen and not swipe_used: show_swipe_hint = True pad.next_keyset() elif pad.cur_key_index < len(pad.keys): @@ -212,7 +215,7 @@ def capture_from_keypad( break else: if btn in (SWIPE_UP, SWIPE_LEFT, SWIPE_DOWN, SWIPE_RIGHT): - swipe_has_not_been_used = False + swipe_used = True pad.navigate(btn) if kboard.has_touchscreen: self.ctx.input.touch.clear_regions() @@ -401,90 +404,58 @@ def print_prompt(self, text, check_printer=True): def prompt(self, text, offset_y=0, highlight_prefix=""): """Prompts user to answer Yes or No""" - lines = self.ctx.display.to_lines(text) - offset_y -= (len(lines) - 1) * FONT_HEIGHT - self.ctx.display.draw_hcentered_text( + lines = self.ctx.display.draw_hcentered_text( text, offset_y, theme.fg_color, theme.bg_color, highlight_prefix=highlight_prefix, ) - self.y_keypad_map = [] - self.x_keypad_map = [] + offset_y = min( + offset_y + lines * FONT_HEIGHT, self.ctx.display.height() - FONT_HEIGHT + ) if kboard.has_minimal_display: return self.ctx.input.wait_for_button() == BUTTON_ENTER - offset_y += (len(lines) + 1) * FONT_HEIGHT - self.x_keypad_map.extend( - [0, self.ctx.display.width() // 2, self.ctx.display.width()] - ) - y_key_map = offset_y - (3 * FONT_HEIGHT // 2) - self.y_keypad_map.append(y_key_map) - y_key_map += 4 * FONT_HEIGHT - self.y_keypad_map.append(min(y_key_map, self.ctx.display.height())) + self.x_keypad_map = [ + DEFAULT_PADDING, + self.ctx.display.width() // 2, + self.ctx.display.width() - DEFAULT_PADDING, + ] + touch_offset_y = self.proceed_menu_text_y_offset(offset_y) - FONT_HEIGHT + self.y_keypad_map = [touch_offset_y, touch_offset_y + 3 * FONT_HEIGHT] if kboard.has_touchscreen: self.ctx.input.touch.set_regions(self.x_keypad_map, self.y_keypad_map) + + go_str = t("Yes") + no_str = t("No") btn = None - answer = True + index = 1 while btn != BUTTON_ENTER: - go_str = t("Yes") - no_str = t("No") - offset_x = (self.ctx.display.width() * 3) // 4 - ( - lcd.string_width_px(go_str) // 2 - ) - self.ctx.display.draw_string( - offset_x, offset_y, go_str, theme.go_color, theme.bg_color - ) - offset_x = self.ctx.display.width() // 4 - ( - lcd.string_width_px(no_str) // 2 - ) - self.ctx.display.draw_string( - offset_x, offset_y, no_str, theme.no_esc_color, theme.bg_color - ) - if self.ctx.input.buttons_active: - if answer: - self.ctx.display.outline( - self.ctx.display.width() // 2, - offset_y - FONT_HEIGHT // 2, - self.ctx.display.usable_width() // 2, - 2 * FONT_HEIGHT - 2, - theme.go_color, - ) - else: - self.ctx.display.outline( - DEFAULT_PADDING, - offset_y - FONT_HEIGHT // 2, - self.ctx.display.usable_width() // 2, - 2 * FONT_HEIGHT - 2, - theme.no_esc_color, - ) - elif kboard.has_touchscreen: - self.ctx.display.draw_vline( - self.ctx.display.width() // 2, - self.y_keypad_map[0] + FONT_HEIGHT, - 2 * FONT_HEIGHT, - theme.frame_color, - ) - btn = self.ctx.input.wait_for_button() + self.draw_proceed_menu(go_str, no_str, offset_y, index) + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_button() + if btn == BUTTON_TOUCH: + self.ctx.input.touch.clear_regions() + # index 0 is No / 1 is Yes / -1 is Edge touch (ignore) + new_index = self.ctx.input.touch.current_index() + if new_index in (0, 1): + if new_index == 1: + return True + return False if btn in (BUTTON_PAGE, BUTTON_PAGE_PREV): - answer = not answer # erase yes/no area for next loop self.ctx.display.fill_rectangle( 0, - offset_y - FONT_HEIGHT, + self.proceed_menu_text_y_offset(offset_y) - FONT_HEIGHT // 2, self.ctx.display.width(), 3 * FONT_HEIGHT, theme.bg_color, ) - elif btn == BUTTON_TOUCH: - self.ctx.input.touch.clear_regions() - # index 0 = No - # index 1 = Yes - if self.ctx.input.touch.current_index(): - return True - return False + index = (index + 1) % 2 # BUTTON_ENTER - return answer + return index == 1 def fit_to_line(self, text, prefix="", fixed_chars=0, crop_middle=True): """Fits text with prefix plus fixed_chars at the beginning into one line, @@ -534,6 +505,12 @@ def run(self, start_from_index=None): _, status = self.menu.run_loop(start_from_index) return status != MENU_SHUTDOWN + def proceed_menu_text_y_offset(self, y_offset): + """Y offset for the text on proceed menu""" + return ( + self.ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) + ) // 2 + y_offset + def draw_proceed_menu( self, go_txt, esc_txt, y_offset=0, menu_index=None, go_enabled=True ): @@ -544,9 +521,7 @@ def draw_proceed_menu( esc_x_offset = ( self.ctx.display.width() // 2 - lcd.string_width_px(esc_txt) ) // 2 - go_esc_y_offset = ( - self.ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) - ) // 2 + y_offset + go_esc_y_offset = self.proceed_menu_text_y_offset(y_offset) if menu_index == 0 and self.ctx.input.buttons_active: self.ctx.display.outline( DEFAULT_PADDING, @@ -753,15 +728,19 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None start_from_submenu = False else: screensaver_time = Settings().appearance.screensaver_time - btn = self.ctx.input.wait_for_fastnav_button( - # Block if screen saver not active - screensaver_time == 0, - screensaver_time * ONE_MINUTE, - ) - if kboard.has_touchscreen: + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button( + # Block if screen saver not active + screensaver_time == 0, + screensaver_time * ONE_MINUTE, + ) if btn == BUTTON_TOUCH: selected_item_index = self.ctx.input.touch.current_index() + if selected_item_index < 0: + continue btn = BUTTON_ENTER + if kboard.has_touchscreen: self.ctx.input.touch.clear_regions() if btn == BUTTON_ENTER: status = self._clicked_item(selected_item_index) diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index f4652583a..cf1c8aa3e 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -28,6 +28,7 @@ BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, + BUTTON_TOUCH, SWIPE_RIGHT, SWIPE_LEFT, SWIPE_UP, @@ -256,11 +257,19 @@ def get_valid_index(self): def touch_to_physical(self): """Convert a touch press in button press""" self.cur_key_index = self.ctx.input.touch.current_index() - actual_button = None + if self.cur_key_index < 0: + self.cur_key_index = 0 + return BUTTON_TOUCH + + special_keys = [self.del_index, self.esc_index, self.go_index] + if self.has_more_key(): + special_keys.append(self.more_index) + + actual_button = BUTTON_TOUCH if self.cur_key_index < len(self.keys): if self.keys[self.cur_key_index] in self.possible_keys: actual_button = BUTTON_ENTER - elif self.cur_key_index < self.layout.max_index: + elif self.cur_key_index in special_keys: actual_button = BUTTON_ENTER else: self.cur_key_index = 0 diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index e78c2e021..a1c045478 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -31,6 +31,7 @@ BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, + SWIPE_FAIL, FAST_FORWARD, FAST_BACKWARD, ) @@ -294,16 +295,19 @@ def edit(self): self.ctx.display.clear() self._draw_header() self._map_words(button_index, page) - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - button_index = self.ctx.input.touch.current_index() - if button_index < ESC_INDEX: - if self.mnemonic_length == 24 and button_index % 2 == 1: - button_index //= 2 - button_index += 12 - else: - button_index //= 2 - btn = BUTTON_ENTER + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + button_index = self.ctx.input.touch.current_index() + if button_index < 0: + continue + if button_index < ESC_INDEX: + if self.mnemonic_length == 24 and button_index % 2 == 1: + button_index = (button_index >> 1) + 12 + else: + button_index >>= 1 + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if button_index == GO_INDEX: if self.mnemonic_length == 24 and kboard.is_m5stickv and page == 0: @@ -316,7 +320,7 @@ def edit(self): if button_index == ESC_INDEX: # Cancel self.ctx.display.clear() - if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): + if self.prompt(t("Are you sure?"), self.ctx.display.height() >> 1): return None continue new_word = self.edit_word(button_index + page * 12) @@ -324,7 +328,7 @@ def edit(self): self.ctx.display.clear() if self.prompt( str(button_index + page * 12 + 1) + ".\n\n" + new_word + "\n\n", - self.ctx.display.height() // 2, + self.ctx.display.height() >> 1, ): self.current_mnemonic[button_index + page * 12] = new_word self.calculate_checksum() diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index 16045a923..a8452e9e2 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -41,7 +41,13 @@ t, locale_control, ) -from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH +from ..input import ( + BUTTON_ENTER, + BUTTON_PAGE, + BUTTON_PAGE_PREV, + BUTTON_TOUCH, + SWIPE_FAIL, +) from ..sd_card import SDHandler from . import ( Page, @@ -83,32 +89,33 @@ def settings(self): def _draw_settings_pad(self): """Draws buttons to change settings with touch""" - if kboard.has_touchscreen: - self.ctx.input.touch.clear_regions() - offset_y = self.ctx.display.height() * 2 // 3 - self.ctx.input.touch.add_y_delimiter(offset_y) - self.ctx.input.touch.add_y_delimiter(offset_y + FONT_HEIGHT * 3) - button_width = (self.ctx.display.width() - 2 * DEFAULT_PADDING) // 3 - for i in range(4): - self.ctx.input.touch.add_x_delimiter(DEFAULT_PADDING + button_width * i) - offset_y += FONT_HEIGHT - keys = ["<", t("Go"), ">"] - for i, x in enumerate(self.ctx.input.touch.x_regions[:-1]): - self.ctx.display.outline( - x, - self.ctx.input.touch.y_regions[0], - button_width - 1, - FONT_HEIGHT * 3, - theme.frame_color, - ) - offset_x = x - offset_x += (button_width - lcd.string_width_px(keys[i])) // 2 - self.ctx.display.draw_string( - offset_x, offset_y, keys[i], theme.fg_color, theme.bg_color - ) + self.ctx.input.touch.clear_regions() + offset_y = self.ctx.display.height() * 2 // 3 + self.ctx.input.touch.add_y_delimiter(offset_y) + self.ctx.input.touch.add_y_delimiter(offset_y + FONT_HEIGHT * 3) + button_width = (self.ctx.display.width() - 2 * DEFAULT_PADDING) // 3 + for i in range(4): + self.ctx.input.touch.add_x_delimiter(DEFAULT_PADDING + button_width * i) + offset_y += FONT_HEIGHT + keys = ["<", t("Go"), ">"] + for i, x in enumerate(self.ctx.input.touch.x_regions[:-1]): + self.ctx.display.outline( + x, + self.ctx.input.touch.y_regions[0], + button_width - 1, + FONT_HEIGHT * 3, + theme.frame_color, + ) + offset_x = x + offset_x += (button_width - lcd.string_width_px(keys[i])) // 2 + self.ctx.display.draw_string( + offset_x, offset_y, keys[i], theme.fg_color, theme.bg_color + ) def _touch_to_physical(self, index): """Mimics touch presses into physical button presses""" + if index < 0: + return BUTTON_TOUCH if index == 0: return BUTTON_PAGE_PREV if index == 1: @@ -373,10 +380,15 @@ def category_setting(self, settings_namespace, setting): color, theme.bg_color, ) - self._draw_settings_pad() - btn = self.ctx.input.wait_for_button() - if btn == BUTTON_TOUCH: - btn = self._touch_to_physical(self.ctx.input.touch.current_index()) + if kboard.has_touchscreen: + self._draw_settings_pad() + + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_button() + if btn == BUTTON_TOUCH: + btn = self._touch_to_physical(self.ctx.input.touch.current_index()) if btn == BUTTON_ENTER: break diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index 0eaa700f2..b5521f4a9 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -32,6 +32,7 @@ BUTTON_TOUCH, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, ) from ..kboard import kboard @@ -392,9 +393,10 @@ def index(self, index, btn): return STACKBIT_GO_INDEX if index <= STACKBIT_MAX_INDEX: return page_prev_move[index] - if index <= STACKBIT_ESC_INDEX: + if index < STACKBIT_GO_INDEX: return STACKBIT_MAX_INDEX - return STACKBIT_ESC_INDEX + return STACKBIT_ESC_INDEX + return index def enter_1248(self): """UI to manually enter a Stackbit 1248""" @@ -414,7 +416,9 @@ def enter_1248(self): words = [] while word_index <= 24: self._map_keys_array() - self.ctx.display.draw_hcentered_text("Stackbit 1248") + self.ctx.display.draw_hcentered_text( + "Stackbit 1248", color=theme.highlight_color + ) y_offset = self.y_offset self._draw_grid(y_offset) self._draw_labels(y_offset, word_index) @@ -423,10 +427,15 @@ def enter_1248(self): self._draw_index(index) self.preview_word(digits) self._draw_punched(digits, y_offset) - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - btn = BUTTON_ENTER - index = self.ctx.input.touch.current_index() + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + index = self.ctx.input.touch.current_index() + if index < 0 or 13 < index < STACKBIT_ESC_INDEX: + continue + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index >= STACKBIT_GO_INDEX: # go word = self.digits_to_word(digits) @@ -455,13 +464,11 @@ def enter_1248(self): self.ctx.display.clear() if self.prompt(t("Done?"), self.ctx.display.height() // 2): break - # self._map_keys_array() #can be removed? word_index += 1 elif index >= STACKBIT_ESC_INDEX: # ESC self.ctx.display.clear() if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): break - # self._map_keys_array() elif index < 14: digits = self._toggle_bit(digits, index) else: diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 8cbf29ee0..2ba5bbfea 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -43,6 +43,7 @@ BUTTON_TOUCH, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, ) from ..bip39 import entropy_checksum from ..kboard import kboard @@ -93,7 +94,7 @@ def _draw_grid(self): def _draw_labels(self, page): """Draws labels for import and export Tinyseed UI""" - self.ctx.display.draw_hcentered_text(self.label) + self.ctx.display.draw_hcentered_text(self.label, color=theme.highlight_color) # For non‑minimal displays, show extra bit numbers (rotate to landscape temporarily) if not kboard.has_minimal_display: self.ctx.display.to_landscape() @@ -248,7 +249,7 @@ def _draw_index(self, index): """Outline index position""" height = self.y_pad - 2 y_pos = (index // 12) * self.y_pad + self.y_offset + 1 - if index < TS_LAST_BIT_NO_CS: + if index < TS_ESC_START_POSITION: x_pos = (index % 12) * self.x_pad + self.x_offset + 1 width = self.x_pad - 2 self.ctx.display.outline(x_pos, y_pos, width, height, theme.fg_color) @@ -375,10 +376,22 @@ def _editable_bit(): if self.ctx.input.buttons_active: self._draw_index(index) - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - btn = BUTTON_ENTER - index = self.ctx.input.touch.current_index() + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + index = self.ctx.input.touch.current_index() + # Ignore clicks on invalid indexes (avoids redraw screen) + disabled_indexes = 4 if not w24 else (8 if page else 0) + if ( + index < 0 + or TS_LAST_BIT_NO_CS - disabled_indexes + < index + < TS_ESC_START_POSITION + ): + continue + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index > TS_ESC_END_POSITION: # "Go" if not w24 or (w24 and (page or scanning_24)): diff --git a/src/krux/touch.py b/src/krux/touch.py index 3bdece846..71506711f 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -30,13 +30,16 @@ PRESSED = 1 RELEASED = 2 +SWIPE_DURATION_MS = 750 SWIPE_THRESHOLD = 35 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 SWIPE_UP = 3 SWIPE_DOWN = 4 +SWIPE_NONE = 5 TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10 +EDGE_PIXELS = 1 # The ammount of pixels to determine the edges of a region class Touch: @@ -47,6 +50,7 @@ def __init__(self, width, height, irq_pin=None, res_pin=None): For Krux width = max_y, height = max_x """ self.sample_time = 0 + self.pressed_time = 0 self.y_regions = [] self.x_regions = [] self.index = 0 @@ -143,30 +147,42 @@ def valid_position(self, data): def _extract_index(self, data): """ Gets an index from touched points, x and y delimiters. - The index is calculated based on the position of the touch within the defined regions. + Return index or -1 if touching an edge. """ - y_index = 0 - x_index = 0 + x, y = data - # Calculate y index - for region in self.y_regions: - if data[1] > region: - y_index += 1 - y_index -= 1 if y_index > 0 else 0 + # Helper to deal with X/Y regions + def _compute_axis_index(pos, regions): + if not regions: + return 0 - # Calculate x index if x regions are defined (2D array) + # Count how many region boundaries pos passed + idx = sum(pos + EDGE_PIXELS >= r for r in regions) + + # # Check boundary at idx-1 (left-side) + if 1 < idx < len(regions) and abs(pos - regions[idx - 1]) <= EDGE_PIXELS: + return -1 + + # Valid is 0<= idx <= len(regions) -2 [valid regions] + return max(min(idx - 1, len(regions) - 2), 0) + + # Y index + y_index = _compute_axis_index(y, self.y_regions) + if y_index < 0: + return -1 + + # X index if self.x_regions: - for x_region in self.x_regions: - if data[0] >= x_region: - x_index += 1 - x_index -= 1 # Adjust index to be zero-based - # Combine y and x indices to get the final index - index = y_index * (len(self.x_regions) - 1) + x_index - else: - index = y_index + x_index = _compute_axis_index(x, self.x_regions) + if x_index < 0: + return -1 + # self.highlight_region( + # y_index * (len(self.x_regions) - 1) + x_index, y_index + # ) + return y_index * (len(self.x_regions) - 1) + x_index - # self.highlight_region(x_index, y_index) - return index + # self.highlight_region(0, y_index) + return y_index def set_regions(self, x_list=None, y_list=None): """Set buttons map regions x and y""" @@ -222,31 +238,37 @@ def current_state(self): data = self.touch_driver.current_point() if isinstance(data, tuple): self._store_points(data) - elif data is None: # gets release then return to idle. + return self.state + + if data is None: # gets release then return to idle. if self.state == RELEASED: # On touch release self.state = IDLE - elif self.state == PRESSED: - if self.release_point is not None: - lateral_lenght = self.release_point[0] - self.press_point[0][0] - if lateral_lenght > SWIPE_THRESHOLD: - self.gesture = SWIPE_RIGHT - elif -lateral_lenght > SWIPE_THRESHOLD: - self.gesture = SWIPE_LEFT - lateral_lenght *= -1 # make it positive value - vertical_lenght = self.release_point[1] - self.press_point[0][1] - if ( - vertical_lenght > SWIPE_THRESHOLD - and vertical_lenght > lateral_lenght - ): - self.gesture = SWIPE_DOWN - elif ( - -vertical_lenght > SWIPE_THRESHOLD - and -vertical_lenght > lateral_lenght - ): - self.gesture = SWIPE_UP + return self.state + + if self.state == PRESSED: self.state = RELEASED - else: - print("Touch error") + + if self.release_point is not None: + dx = self.release_point[0] - self.press_point[0][0] + dy = self.release_point[1] - self.press_point[0][1] + + if abs(dx) > SWIPE_THRESHOLD or abs(dy) > SWIPE_THRESHOLD: + # discard swipes that took more than ~1s + if self.sample_time - self.pressed_time < SWIPE_DURATION_MS: + # discards swipes with angle > 27 degrees + if abs(dx) > abs(dy) * 2: + self.gesture = SWIPE_LEFT if dx < 0 else SWIPE_RIGHT + elif abs(dy) > abs(dx) * 2: + self.gesture = SWIPE_UP if dy < 0 else SWIPE_DOWN + else: + self.gesture = SWIPE_NONE # undetermined diagonal swipe + else: + self.gesture = ( + SWIPE_NONE # hold finger on screen for too long + ) + return self.state + + print("Touch error") return self.state def event(self, validate_position=True): @@ -261,6 +283,7 @@ def event(self, validate_position=True): if isinstance(self.touch_driver.irq_point, tuple): if self.valid_position(self.touch_driver.irq_point): self._store_points(self.touch_driver.irq_point) + self.pressed_time = time.ticks_ms() return True return False @@ -268,33 +291,31 @@ def value(self): """Wraps touch states to behave like a regular button""" return 1 if self.current_state() == IDLE else 0 - def swipe_right_value(self): - """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_RIGHT: + def _swipe_state_check(self, swipe_type): + if self.gesture == swipe_type: self.gesture = None return 0 return 1 + def swipe_none_value(self): + """Returns detected gestures and clean respective variable""" + return self._swipe_state_check(SWIPE_NONE) + + def swipe_right_value(self): + """Returns detected gestures and clean respective variable""" + return self._swipe_state_check(SWIPE_RIGHT) + def swipe_left_value(self): """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_LEFT: - self.gesture = None - return 0 - return 1 + return self._swipe_state_check(SWIPE_LEFT) def swipe_up_value(self): """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_UP: - self.gesture = None - return 0 - return 1 + return self._swipe_state_check(SWIPE_UP) def swipe_down_value(self): """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_DOWN: - self.gesture = None - return 0 - return 1 + return self._swipe_state_check(SWIPE_DOWN) def current_index(self): """Returns current index of last touched point""" diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index 17f5cca7a..bb4607ff7 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -714,6 +714,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): from krux.baseconv import base_encode from krux.pages.encryption_ui import decrypt_kef, KEFEnvelope from krux.input import BUTTON_PAGE_PREV + from krux.themes import theme # setup data: a fake kef envelope, non-kef data, decrypt-evidence, and responding "No" to "Decrypt?" fake_kef = kef.wrap(b"", 0, 10000, bytes([i * 8 for i in range(32)])) @@ -728,7 +729,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test w/ non-kef bytes") ctx = create_ctx(mocker, []) @@ -737,7 +740,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test w/ kef hex") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -746,7 +749,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef hex") ctx = create_ctx(mocker, []) @@ -755,7 +760,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid hex-ish str") ctx = create_ctx(mocker, []) @@ -764,7 +769,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef HEX") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -773,7 +778,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef HEX") ctx = create_ctx(mocker, []) @@ -782,7 +789,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid HEX-ish str") ctx = create_ctx(mocker, []) @@ -791,7 +798,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef base32") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -800,7 +807,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef base32") ctx = create_ctx(mocker, []) @@ -809,7 +818,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid base32-ish str") ctx = create_ctx(mocker, []) @@ -818,7 +827,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef base43") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -827,7 +836,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef base43") ctx = create_ctx(mocker, []) @@ -836,7 +847,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid base43-ish str") ctx = create_ctx(mocker, []) @@ -845,7 +856,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef base64") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -854,7 +865,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef base64") ctx = create_ctx(mocker, []) @@ -863,7 +876,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid base64-ish str") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -872,7 +885,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() def test_prompt_for_text_update_dflt_via_yes(m5stickv, mocker): diff --git a/tests/pages/test_keypads.py b/tests/pages/test_keypads.py index c544f5ec8..356f48391 100644 --- a/tests/pages/test_keypads.py +++ b/tests/pages/test_keypads.py @@ -19,3 +19,13 @@ def test_button_turbo(mocker, m5stickv): ctx.input.page_prev_value = mocker.MagicMock(side_effect=[PRESSED, None]) keypad.navigate(FAST_BACKWARD) keypad._previous_key.assert_called() + + +def test_invalid_touch_index(mocker, amigo): + from krux.pages.keypads import Keypad + from krux.input import BUTTON_TOUCH + + ctx = create_ctx(mocker, [BUTTON_TOUCH], touch_seq=[-1]) + keypad = Keypad(ctx, "abc") + btn = keypad.touch_to_physical() + assert keypad.cur_key_index == 0 diff --git a/tests/pages/test_menu.py b/tests/pages/test_menu.py index cf6d821b9..f50092469 100644 --- a/tests/pages/test_menu.py +++ b/tests/pages/test_menu.py @@ -122,6 +122,16 @@ def exception_raiser(): assert status == MENU_SHUTDOWN assert ctx.input.wait_for_fastnav_button.call_count == call_count + # Check invalid touch index don't change result + BTN_SEQUENCE.insert(1, BUTTON_TOUCH) + call_count += len(BTN_SEQUENCE) + # invalid touch index + mocker.patch.object(ctx.input.touch, "current_index", new=lambda: -1) + ctx.input.wait_for_fastnav_button.side_effect = BTN_SEQUENCE + index, status = menu.run_loop() + assert status == MENU_SHUTDOWN + assert ctx.input.wait_for_fastnav_button.call_count == call_count + mocker.patch.object(ctx.input.touch, "current_index", new=lambda: 1) mocker.patch.object(ctx.input, "buttons_active", False) diff --git a/tests/pages/test_mnemonic_editor.py b/tests/pages/test_mnemonic_editor.py index 19c0a292f..f7d71675e 100644 --- a/tests/pages/test_mnemonic_editor.py +++ b/tests/pages/test_mnemonic_editor.py @@ -269,6 +269,7 @@ def test_edit_existing_mnemonic_using_touch(mocker, amigo): 1, 1, 1, # Confirm cabbage + -1, # Try a swipe return invalid index 25, # Try to "Go" with invalid checksum word 23, # index 23 = word 24 22, # Type w, i, t -> witness diff --git a/tests/pages/test_settings_page.py b/tests/pages/test_settings_page.py index 1222640ce..5aff50538 100644 --- a/tests/pages/test_settings_page.py +++ b/tests/pages/test_settings_page.py @@ -266,6 +266,7 @@ def test_settings_on_amigo_tft(amigo, mocker, mocker_printer): PREV_INDEX = 0 GO_INDEX = 1 NEXT_INDEX = 2 + INVALID_INDEX = -1 # SWIPE HARDWARE_INDEX = 2 LOCALE_INDEX = 3 @@ -331,6 +332,9 @@ def test_settings_on_amigo_tft(amigo, mocker, mocker_printer): LOCALE_INDEX, # Change Locale NEXT_INDEX, + NEXT_INDEX, + INVALID_INDEX, + PREV_INDEX, GO_INDEX, ), [ diff --git a/tests/pages/test_stackbit.py b/tests/pages/test_stackbit.py index e695db784..19a661f75 100644 --- a/tests/pages/test_stackbit.py +++ b/tests/pages/test_stackbit.py @@ -122,8 +122,10 @@ def test_enter_stackbit_touch(amigo, mocker): from krux.input import BUTTON_TOUCH YES = 1 - BTN_SEQUENCE = [BUTTON_TOUCH] * 3 * 12 + [BUTTON_TOUCH] - TOUCH_SEQUENCE = [0, STACKBIT_GO_INDEX + 1, YES] * 12 + [YES] + BTN_SEQUENCE = [BUTTON_TOUCH] * 4 * 12 + [BUTTON_TOUCH] + TOUCH_SEQUENCE = [0, -1, STACKBIT_GO_INDEX + 1, YES] * 12 + [ + YES + ] # negative values (invalid touches) should not change the result TEST_12_WORDS = "language language language language language language language language language language language language" ctx = create_ctx(mocker, BTN_SEQUENCE, touch_seq=TOUCH_SEQUENCE) @@ -181,3 +183,15 @@ def test_entering_stackbit_buttons_turbo(mocker, m5stickv): stackbit.enter_1248() stackbit.index.assert_called_with(0, FAST_BACKWARD) + + +def test_stackbit_index_ignore_swipe(mocker, amigo): + from krux.pages.stack_1248 import Stackbit + from krux.input import SWIPE_LEFT, SWIPE_RIGHT, SWIPE_DOWN, SWIPE_UP, SWIPE_FAIL + + ctx = create_ctx(mocker, []) + stackbit = Stackbit(ctx) + tmp = 10 + for swipe in (SWIPE_LEFT, SWIPE_RIGHT, SWIPE_DOWN, SWIPE_UP, SWIPE_FAIL): + new_index = stackbit.index(tmp, swipe) + assert new_index == tmp diff --git a/tests/pages/test_tiny_seed.py b/tests/pages/test_tiny_seed.py index 42fe57273..ab57d2efb 100644 --- a/tests/pages/test_tiny_seed.py +++ b/tests/pages/test_tiny_seed.py @@ -176,6 +176,8 @@ def test_enter_tiny_seed_24w_amigo(amigo, mocker): + [3] # Toggle to last editable bit + [135] + # An invalid index don't change result + + [-1] # Press ESC + [TS_ESC_START_POSITION] # Give up from ESC diff --git a/tests/test_input.py b/tests/test_input.py index bd64a3310..3c1ab6a43 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -373,6 +373,14 @@ def test_swipe_down_value_released_when_none(mocker, m5stickv): assert input.swipe_down_value() == RELEASED +def test_swipe_fail_value_released_when_none(mocker, m5stickv): + from krux.input import Input, RELEASED + + input = Input() + input.touch = None + assert input.swipe_none_value() == RELEASED + + def test_wait_for_release(mocker, m5stickv): import krux from krux.input import Input, RELEASED, PRESSED, BUTTON_ENTER @@ -676,7 +684,14 @@ def mock_points(point1, point2): def test_touch_gestures(mocker, amigo): import krux - from krux.input import Input, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN + from krux.input import ( + Input, + SWIPE_LEFT, + SWIPE_RIGHT, + SWIPE_UP, + SWIPE_DOWN, + SWIPE_FAIL, + ) input = Input() input = reset_input_states(mocker, input) @@ -691,6 +706,7 @@ def mock_points(point1, point2): "current_point", side_effect=[None, point1, point2, None, None], ) + input.touch.pressed_time = time.ticks_ms() # Swipe Right input.touch.clear_regions() @@ -720,6 +736,13 @@ def mock_points(point1, point2): assert btn == SWIPE_DOWN krux.input.wdt.feed.assert_called() + # Swipe Fail + input.touch.clear_regions() + mock_points((75, 50), (150, 100)) + btn = input.wait_for_button(True) + assert btn == SWIPE_FAIL + krux.input.wdt.feed.assert_called() + def test_invalid_touch_delimiter(mocker, amigo): # Tries to add a delimiter outside screen area diff --git a/tests/test_touch.py b/tests/test_touch.py index 3f03ff734..92c2f544b 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -14,16 +14,6 @@ def mock_settings(mocker): return mock_settings_obj -@pytest.fixture -def mock_touch_driver(mocker): - """Mock a generic touch driver""" - driver = mocker.MagicMock() - driver.current_point.return_value = None - driver.event.return_value = False - driver.irq_point = None - return driver - - def test_touch_init_ft6x36(mocker, amigo, mock_settings): """Test Touch initialization with FT6X36 driver (default case)""" from krux.touch import Touch @@ -201,11 +191,47 @@ def test_valid_position( [ ([60, 120], [], (100, 50), 0), # y=50 < 60: index 0 ([60, 120], [], (100, 80), 0), # y=80 between 60 and 120: index 0 - ([60, 120], [], (100, 130), 1), # y=130 > 120: index 1 + ([60, 120], [], (100, 130), 0), # y=130 > 120: index 0 (max==len(regions)-2) ([40, 80, 120], [], (50, 30), 0), # y=30 < 40: index 0 ([40, 80, 120], [], (50, 50), 0), # y=50 between 40-80: index 0 ([40, 80, 120], [], (50, 90), 1), # y=90 between 80-120: index 1 - ([40, 80, 120], [], (50, 130), 2), # y=130 > 120: index 2 + ( + [40, 80, 120], + [], + (50, 130), + 1, + ), # y=130 > 120: index 1 (max==len(regions)-2) + ([0, 100, 200], [], (100, 100), -1), # boundary y region: index -1 + ([0, 100, 200], [], (100, 200), 1), # last boundary y region ignore: index 1 + ([], [10, 100, 200], (0, 100), 0), # x=0 < 10: index 0 first region + ([], [10, 100, 200], (50, 100), 0), # x=50 < 100: index 0 still first region + ([], [10, 100, 200], (100, 100), -1), # boundary x region: index -1 + ([], [10, 100, 200], (150, 100), 1), # x=150 > 100: index 1 + ( + [], + [0, 100, 200], + (201, 100), + 1, + ), # x=201 > 200: index 1 (max==len(regions)-2) + ( + [0, 100, 200], + [0, 100, 200], + (100, 100), + -1, + ), # boundary x and y region: index -1 + ([], [], (50, 50), 0), # no regions + ([60, 120], [], (10, 30), 0), # before first y region + ([60, 120], [], (10, 80), 0), # normal y region + ([60, 120], [], (10, 60), 0), # edge of the first y region + ([60, 120], [], (10, 120), 0), # edge of last y region + ( + [60, 120], + [50, 100, 150], + (70, 130), + 0, + ), # y=130 > last region, x=70 > 50 first region + ([60, 120], [50, 100, 150], (100, 130), -1), # boundary x region: index -1 + ([60, 80, 90, 100, 110, 120], [], (0, 91), -1), # boundary with more regions ], ) def test_extract_index( @@ -291,6 +317,7 @@ def test_current_state_released_to_idle(mocker, amigo, mock_settings): ((100, 100), (100, 160), 4), # SWIPE_DOWN (vertical = 60 > lateral) ((100, 160), (100, 100), 3), # SWIPE_UP (vertical = -60 > lateral) ((100, 100), (105, 105), None), # No gesture (< threshold) + ((10, 10), (60, 60), 5), # SWIPE_FAIL diagonal swipe ], ) def test_gesture_detection( @@ -308,12 +335,36 @@ def test_gesture_detection( touch.state = PRESSED touch.press_point = [press_point] touch.release_point = release_point + touch.pressed_time = time.ticks_ms() touch.current_state() assert touch.gesture == expected_gesture +def test_gesture_long_duration_fail( + mocker, + amigo, + mock_settings, +): + """Test swipe gesture fail detection""" + from krux.touch import Touch, PRESSED, SWIPE_NONE + + mock_driver = mocker.MagicMock() + mock_driver.current_point.return_value = None + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", side_effect=[1000, 3000]) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.state = PRESSED + touch.press_point = [(10, 10)] + touch.release_point = (10, 60) + + touch.current_state() + + assert touch.gesture == SWIPE_NONE + + def test_event_with_validation(mocker, amigo, mock_settings): """Test event detection with position validation""" from krux.touch import Touch