# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # # SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard # and is covered by GPLv3 license found in COPYING. # # ux.py - UX/UI related helper functions # # NOTE: do not import from main at top. import gc import utime from display import Display, FontSmall, FontTiny from uasyncio import sleep_ms from uasyncio.queues import QueueEmpty from common import system, dis from data_codecs.qr_type import QRType from data_codecs.qr_factory import get_qr_decoder_for_data, make_qr_encoder from utils import is_alphanumeric_qr LEFT_MARGIN = 8 RIGHT_MARGIN = 6 TOP_MARGIN = 12 VERT_SPACING = 10 MAX_WIDTH = Display.WIDTH - LEFT_MARGIN - \ RIGHT_MARGIN - Display.SCROLLBAR_WIDTH TEXTBOX_MARGIN = 6 # This signals the need to switch from current # menu (or whatever) to show something new. The # stack has already been updated, but the old # top-of-stack code was waiting for a key event. class AbortInteraction(Exception): pass class UserInteraction: def __init__(self): self.stack = [] def is_top_level(self): return len(self.stack) == 1 def top_of_stack(self): return self.stack[-1] if self.stack else None def reset_to_root(self): root = self.stack[0] self.reset(root) def reset(self, new_ux): self.stack.clear() gc.collect() self.push(new_ux) async def interact(self): # this is called inside a while(1) all the time # - execute top of stack item try: await self.stack[-1].interact() except AbortInteraction: pass def push(self, new_ux): self.stack.append(new_ux) def replace(self, new_ux): old = self.stack.pop() del old self.stack.append(new_ux) def pop(self): if len(self.stack) < 2: # top of stack, do nothing return True old = self.stack.pop() del old # Singleton. User interacts with this "menu" stack. the_ux = UserInteraction() def time_now_ms(): import utime return utime.ticks_ms() class KeyInputHandler: def __init__(self, down="", up="", long="", repeat_delay=None, repeat_speed=None, long_duration=2000): self.time_pressed = {} self.down = down self.up = up self.long = long self.repeat_delay = repeat_delay # How long until repeat mode starts self.repeat_speed = repeat_speed # How many ms between each repeat self.repeat_active = False self.long_duration = long_duration self.kcode_state = 0 self.kcode_last_time_pressed = 0 # Returns a dictionary of all pressed keys mapped to the elapsed time that each has been pressed. # This can be used for things like showing the progress bar for the Hold to Sign functionality. def get_all_pressed(self): now = time_now_ms() pressed = {} for key, start_time in self.time_pressed.items(): pressed[key] = now - start_time return pressed def __update_kcode_state(self, expected_keys, actual_key): # print('kcode: state={} expected={} actual={}'.format(self.kcode_state, expected_key, actual_key)) if actual_key in expected_keys: self.kcode_state += 1 self.kcode_last_time_pressed = time_now_ms() # print(' state advanced to {}'.format(self.kcode_state)) else: self.kcode_state = 0 # print(' state reset to {}'.format(self.kcode_state)) # If this key could start a new sequence, then call recursively so we don't skip it if actual_key == 'u': # print(' second chance for {}'.format(actual_key)) self.__check_kcode(actual_key) def __check_kcode(self, key): if self.kcode_state == 0: self.__update_kcode_state('u', key) elif self.kcode_state == 1: self.__update_kcode_state('u', key) elif self.kcode_state == 2: self.__update_kcode_state('d', key) elif self.kcode_state == 3: self.__update_kcode_state('d', key) elif self.kcode_state == 4: self.__update_kcode_state('l', key) elif self.kcode_state == 5: self.__update_kcode_state('r', key) elif self.kcode_state == 6: self.__update_kcode_state('l', key) elif self.kcode_state == 7: self.__update_kcode_state('r', key) elif self.kcode_state == 8: self.__update_kcode_state('xy', key) elif self.kcode_state == 9: self.__update_kcode_state('xy', key) # If the user seems to be entering the kcode, then the caller should # probably not perform the normal button processing def kcode_imminent(self): # print('kcode_immiment() = {}'.format(True if self.kcode_state >= 8 else False)) return self.kcode_state >= 8 def kcode_complete(self): # print('kcode_complete game = {}'.format(True if self.kcode_state == 10 else False)) return self.kcode_state == 10 def kcode_reset(self): # print('kcode_reset()') self.kcode_state = 0 def is_pressed(self, key): return key in self.time_pressed def clear(self): from common import keypad keypad.clear_keys() # Reset internal state so that all pending kcodes and repeats are forgotten. def reset(self): self.time_pressed = {} self.kcode_state = 0 self.kcode_last_time_pressed = 0 self.repeat_active = False # New input function to be used in place of PressRelease and ux_press_release, ux_all_up and ux_poll_once. async def get_event(self): from common import keypad # This awaited sleep is necessary to give the simulator key code a chance to insert keys into the queue # Without it, the ux_poll_once() below will never find a key. await sleep_ms(5) # See if we have a character in the queue and if so process it # Poll for an event key, is_down = keypad.get_event() # if key != None: # print('key={} is_down={}'.format(key, is_down)) if key == None: # There was nothing in the queue, so handle the time-dependent events now = time_now_ms() for k in self.time_pressed: # print('k={} self.down={} self.repeat={} self.time_pressed={}'.format(k, self.down, self.repeat, self.time_pressed)) # Handle repeats if self.repeat_delay != None and k in self.down: elapsed = now - self.time_pressed[k] if self.repeat_active == False: if elapsed >= self.repeat_delay: self.repeat_active = True self.time_pressed[k] = now return (k, 'repeat') else: if elapsed >= self.repeat_speed: self.time_pressed[k] = now return (k, 'repeat') # Handle long press expiration if k in self.long: elapsed = now - self.time_pressed[k] if elapsed >= self.long_duration: del self.time_pressed[k] return (k, 'long_press') # Handle kcode timeout - User seemed to give up, so go back to normal key processing if self.kcode_state > 0 and now - self.kcode_last_time_pressed >= 3000: # print('Resetting kcode due to timeout') self.kcode_state = 0 return None now = time_now_ms() # Handle the event if is_down: self.__check_kcode(key) # Check to see if we are interested in this key event if key in self.down: self.time_pressed[key] = now return (key, 'down') if key in self.long: self.time_pressed[key] = now else: # up # Removing this will cancel long presses of the key as well if key in self.time_pressed: self.repeat_active = False del self.time_pressed[key] # Check to see if we are interested in this key event if key in self.up: return (key, 'up') key_to_char_map_lower = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz', '0': ' ', } key_to_char_map_upper = { '2': 'ABC', '3': 'DEF', '4': 'GHI', '5': 'JKL', '6': 'MNO', '7': 'PQRS', '8': 'TUV', '9': 'WXYZ', '0': ' ', } key_to_char_map_numbers = { '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '0': '0 ', } # Class that implements a state machine for editing a text string like a passphrase. # Takes KeyInputHandler events as state change events, along with elapsed time. IDLE_KEY_TIMEOUT = 500 class TextInputHandler: def __init__(self, text="", num_only=False, max_length=None): self.text = [ch for ch in text] self.cursor_pos = len(self.text) # Put cursor at the end if there is any initial text self.last_key_down_time = 0 self.last_key = None self.next_map_index = 0 self.curr_key_map = key_to_char_map_numbers if num_only else key_to_char_map_lower self.num_only = num_only self.max_length = max_length if self.max_length: # Make sure user-passed value doesn't exceed the max, if given self.text = self.text[0:self.max_length] def _next_key_map(self): if self.num_only: return if self.curr_key_map == key_to_char_map_lower: self.curr_key_map = key_to_char_map_upper elif self.curr_key_map == key_to_char_map_upper: self.curr_key_map = key_to_char_map_numbers elif self.curr_key_map == key_to_char_map_numbers: self.curr_key_map = key_to_char_map_lower def get_mode_description(self): if self.curr_key_map == key_to_char_map_lower: return 'a-z' elif self.curr_key_map == key_to_char_map_upper: return 'A-Z' elif self.curr_key_map == key_to_char_map_numbers: return '0-9' async def handle_event(self, event): now = time_now_ms() key, event_type = event if event_type == 'down': # Outer code handles these if key in 'xy': return # print("key={}".format(key)) if key in '*#rl': if key == '#': self._next_key_map() elif key == '*': if self.cursor_pos > 0: # Delete the character under the cursor self.cursor_pos = max(self.cursor_pos-1, 0) if len(self.text) > self.cursor_pos: del self.text[self.cursor_pos] elif key == 'l': self.cursor_pos = max(self.cursor_pos - 1, 0) elif key == 'r': # Allow cursor_pos to go at most one past the end self.cursor_pos = min( self.cursor_pos + 1, len(self.text)) # print('cursor_pos={} next_map_index={} key={} text={}'.format( # self.cursor_pos, self.next_map_index, key, self.text)) self.last_key_down_time = 0 return # Check for symbols pop-up if key == '1' and self.curr_key_map != key_to_char_map_numbers: if self.max_length and len(self.text) >= self.max_length: pass else: # Show the symbols pop-up, otherwise fall through and handle '1' as a normal key press symbol = await ux_show_symbols_popup('!') if symbol == None: return # Insert the symbol self.text.insert(self.cursor_pos, symbol) self.cursor_pos += 1 return if self.last_key == None: # print("first press of {}".format(key)) # A new keypress, so insert the first character mapped to this key, unless max reached if self.max_length and len(self.text) >= self.max_length: pass else: self.text.insert( self.cursor_pos, self.curr_key_map[key][self.next_map_index]) self.cursor_pos += 1 if len(self.curr_key_map[key]) == 1: # Just immediate commit this key since there are no other possible choices to wait for return elif self.last_key == key: # User is pressing the same key within the idle timeout, so cycle to the next key # making sure to wrap around if they keep going. self.next_map_index = ( self.next_map_index + 1) % len(self.curr_key_map[key]) # Overwrite the last key self.text[self.cursor_pos - 1] = self.curr_key_map[key][self.next_map_index] else: if self.max_length and len(self.text) >= self.max_length: pass else: # User pressed a different key, but before the idle timeout, so we finalize the last # character and start the next character as tentative. self.cursor_pos += 1 # Finalize the last character self.next_map_index = 0 # Reset the map index # Append the new key self.text.insert( self.cursor_pos, self.curr_key_map[key][self.next_map_index]) # Insert or overwrite the character # print('cursor_pos={} next_map_index={} key={} text={}'.format( # self.cursor_pos, self.next_map_index, key, self.text)) # Always record the value and time of the key, regardless of which case we were in above self.last_key = key self.last_key_down_time = now # This method should be called periodically (like event 10ms) if there is no key event. # Return True if a timeout occurred, so the caller can render the updated state. def check_timeout(self): now = time_now_ms() if self.last_key_down_time != 0 and now - self.last_key_down_time >= IDLE_KEY_TIMEOUT: # print("timeout!") # Reset for next key self.last_key_down_time = 0 self.last_key = None self.last_index = -1 self.next_map_index = 0 return True return False def get_text(self): return "".join(self.text) def get_num(self): return int("".join(self.text)) async def ux_enter_text(title="Enter Text", label="Text", initial_text='', left_btn='BACK', right_btn='CONTINUE', num_only=False, max_length=None): from common import dis from display import FontSmall font = FontSmall input = KeyInputHandler(down='1234567890*#rlxy', up='xy') text_handler = TextInputHandler(text=initial_text, num_only=num_only, max_length=max_length) while 1: # redraw system.turbo(True) dis.clear() dis.draw_header(title, left_text=text_handler.get_mode_description()) # Draw the title y = Display.HEADER_HEIGHT + TEXTBOX_MARGIN dis.text(None, y+2, label) # Draw a bounding box around the text area y += font.leading + TEXTBOX_MARGIN dis.draw_rect(TEXTBOX_MARGIN, y, Display.WIDTH - (TEXTBOX_MARGIN * 2), Display.HEIGHT - y - TEXTBOX_MARGIN - Display.FOOTER_HEIGHT, 1, fill_color=0, border_color=1) # Draw the text and any other stuff y += 4 dis.text_input(None, y, text_handler.get_text(), cursor_pos=text_handler.cursor_pos, font=font) dis.draw_footer(left_btn, right_btn, input.is_pressed( 'x'), input.is_pressed('y')) dis.show() system.turbo(False) # Wait for key inputs event = None while True: event = await input.get_event() if event != None: break # No event, so handle the idle timing if text_handler.check_timeout(): break if event != None: key, event_type = event # Check for footer button actions first if event_type == 'down': await text_handler.handle_event(event) if event_type == 'up': if key == 'x': return None if key == 'y': return text_handler.get_num() if num_only else text_handler.get_text() symbol_rows = [ '!@#$%^&*', '+/-=\\?|~', '_"`\',.:;', '()[]{}<>', ] async def ux_show_symbols_popup(title="Enter Passphrase"): from common import dis from display import FontSmall # print('ux_show_symbols_popup()') font = FontSmall input = KeyInputHandler(down='rlduxy', up='xy') text_handler = TextInputHandler() cursor_row = 0 cursor_col = 0 num_symbols_per_row = 8 num_rows = len(symbol_rows) h_margin = 12 v_margin = 10 char_spacing = 22 width = num_symbols_per_row * char_spacing + (2 * h_margin) height = num_rows * font.leading + (2 * v_margin) while 1: system.turbo(True) # redraw x = Display.WIDTH // 2 - width // 2 y = Display.HEIGHT - Display.FOOTER_HEIGHT - height - 14 dis.draw_rect(x, y, width, height, 2, 0, 1) x += h_margin y += v_margin # Draw the grid of symbols curr_row = 0 for symbols in symbol_rows: dis.text(x, y, symbols, cursor_pos=(cursor_col if curr_row == cursor_row else None), font=font, fixed_spacing=char_spacing, cursor_shape='block') curr_row += 1 y += font.leading dis.draw_footer('CANCEL', 'SELECT', input.is_pressed( 'x'), input.is_pressed('y')) dis.show() system.turbo(False) # Wait for key inputs event = None while True: event = await input.get_event() if event != None: break # No event, so handle the idle timing if text_handler.check_timeout(): break num_symbols = len(symbol_rows[cursor_row]) if event != None: key, event_type = event # Check for footer button actions first if event_type == 'down': if key == 'u': cursor_row = (cursor_row - 1) % num_rows elif key == 'd': cursor_row = (cursor_row + 1) % num_rows elif key == 'l': cursor_col = (cursor_col - 1) % num_symbols elif key == 'r': cursor_col = (cursor_col + 1) % num_symbols if event_type == 'up': if key == 'x': return None if key == 'y': return symbol_rows[cursor_row][cursor_col] # NOTE: This function factors in the scrollbar width even if the scrollbar might not end up being shown, # because the line breaking code uses it BEFORE knowing if scrolling is required. def chars_per_line(font): return (Display.WIDTH - LEFT_MARGIN - Display.SCROLLBAR_WIDTH) // font.advance def word_wrap(ln, font): from common import dis max_width = Display.WIDTH - LEFT_MARGIN - \ RIGHT_MARGIN - Display.SCROLLBAR_WIDTH while ln: sp = 0 last_space = 0 line_width = 0 first_non_space = 0 # Strip out leading and trailing spaces ln = ln.strip() while sp < len(ln): ch = ln[sp] if ch.isspace(): last_space = sp ch_w = dis.char_width(ch, font) line_width += ch_w if line_width >= max_width: # If we found a space, we can break there, but if we didn't # then just break before we went over. if last_space != 0: sp = last_space break sp += 1 line = ln[first_non_space:sp] ln = ln[sp:] yield line async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, escape='', left_btn='BACK', right_btn='CONTINUE', scroll_label=None, left_btn_enabled=True, right_btn_enabled=True, center_vertically=False, center=False, overlay=None, clear_keys=False): from common import dis, keypad from utils import split_by_char_size system.turbo(True) # Clear the keys before starting if clear_keys: keypad.clear_keys() if hasattr(msg, 'readline'): lines = split_by_char_size(msg.getvalue(), font) else: lines = split_by_char_size(msg, font) # trim blank lines at end while not lines[-1]: lines = lines[:-1] top = 0 H = (Display.HEIGHT - Display.HEADER_HEIGHT - Display.FOOTER_HEIGHT) // font.leading max_visible_lines = (Display.HEIGHT - Display.HEADER_HEIGHT - Display.FOOTER_HEIGHT) // font.leading input = KeyInputHandler(down='rldu0xy', up='xy' + escape, repeat_delay=250, repeat_speed=10) system.turbo(False) allow_right_btn_action = True turbo = None # We rely on this being 3 states: None, False, True while 1: # redraw dis.clear() dis.draw_header(title) y = Display.HEADER_HEIGHT # Only take center_vertically into account if there are more lines than will fit on the page if len(lines) <= max_visible_lines and center_vertically: avail_height = (Display.HEIGHT - Display.HEADER_HEIGHT - Display.FOOTER_HEIGHT) text_height = len(lines) * font.leading - font.descent y += avail_height // 2 - text_height // 2 content_to_height_ratio = H / len(lines) show_scrollbar= True if content_to_height_ratio < 1 else False last_to_show = min(top+H+1, len(lines)) for ln in lines[top:last_to_show]: x = LEFT_MARGIN if not center else None dis.text(x, y, ln, font=font, scrollbar_visible=show_scrollbar) y += font.leading if show_scrollbar: dis.scrollbar(top / len(lines), content_to_height_ratio) # Show the scroll_label if given and if we have not reached the bottom yet scroll_enable_right_btn = True right_btn_label = right_btn if scroll_label != None: if H + top < len(lines): scroll_enable_right_btn = False right_btn_label = scroll_label dis.draw_footer(left_btn, right_btn_label, input.is_pressed('x'), input.is_pressed('y')) # Draw overlay image, if any if overlay: (x, y, image) = overlay dis.icon(x, y, image) dis.show() # We only want to turn it off once rather than whenever it's False, so we # set to None to avoid turning turbo off again. if turbo == False: system.turbo(False) turbo = None # Wait for key inputs event = None while True: while True: event = await input.get_event() if event != None: break key, event_type = event # print('key={} event_type={}'.format(key, event_type)) if event_type == 'down' or event_type == 'repeat': system.turbo(True) turbo = True if key == 'u': top = max(0, top-1) break elif key == 'd': if len(lines) > H: top = min(len(lines) - H, top+1) break elif key == 'y': if event_type == 'repeat': allow_right_btn_action = False elif event_type == 'down': allow_right_btn_action = True if not scroll_enable_right_btn: if len(lines) > H: top = min(len(lines) - H, top+1) else: continue break elif key in 'xy': # allow buttons to redraw for pressed state break else: continue if event_type == 'down': if key == '0': top = 0 break else: continue if event_type == 'up': turbo = False # We set to False here, but actually turn off after rendering if key in escape: return key # No left_btn means don't exit on the 'x' key if left_btn_enabled and (key == 'x'): return key if key == 'y': if scroll_enable_right_btn: if right_btn_enabled and allow_right_btn_action: return key break else: if len(lines) > H: top = min(len(lines) - H, top+1) break else: continue async def ux_confirm(msg, title='Passport', negative_btn='NO', positive_btn='YES', center=True, center_vertically=True, scroll_label=None): resp = await ux_show_story(msg, title=title, center=center, center_vertically=center_vertically, left_btn=negative_btn, right_btn=positive_btn, scroll_label=scroll_label) return resp == 'y' # async def ux_dramatic_pause(msg, seconds): # from common import dis, system # # # show a full-screen msg, with a dramatic pause + progress bar # n = seconds * 8 # dis.fullscreen(msg) # for i in range(n): # system.progress_bar((i*100)//n) # await sleep_ms(125) def blocking_sleep(ms): start = utime.ticks_ms() while (1): now = utime.ticks_ms() if now - start >= ms: return def save_error_log(msg, filename): from files import CardSlot, CardMissingError wrote_to_sd = False try: with CardSlot() as card: # Full path and short filename fname, nice = card.get_file_path(filename) with open(fname, 'wb') as fd: line = 'Saved %s to microSD' % nice fd.write(msg) wrote_to_sd = True except CardMissingError: line = 'Insert microSD to save log' except Exception: line = 'Failed to save %s' % filename return wrote_to_sd, line def ux_show_fatal(msg): from common import dis from display import FontTiny font = FontTiny ch_per_line = chars_per_line(font) lines = [] for ln in msg.split('\n'): if len(ln) > ch_per_line: lines.extend(word_wrap(ln, font)) else: # ok if empty string, just a blank line lines.append(ln) # Draw top = 0 max_visible_lines = ( Display.HEIGHT - Display.HEADER_HEIGHT) // font.leading + 1 num_lines = len(lines) max_top = max(0, num_lines - max_visible_lines) PER_LINE_DELAY = 500 LONG_DELAY = 2000 INITIAL_DELAY = 5000 delay = INITIAL_DELAY direction = 1 # Write filename = 'error.log' wrote_to_sd, lines[0] = save_error_log(msg, filename) while (1): system.turbo(True) # Draw dis.clear() dis.draw_header('Error') # Draw the subset of lines that is visible y = Display.HEADER_HEIGHT for ln in lines[top:top+max_visible_lines]: dis.text(LEFT_MARGIN, y, ln, font=font) y += font.leading dis.show() system.turbo(False) blocking_sleep(delay) # More lines than can be displayed - early exit if no scrolling needed if num_lines <= max_visible_lines: return # Change direction if we reach the top or bottom if direction == 1: top = min(max_top, top + 1) if top == max_top: direction = -1 else: top = max(0, top - 1) if top == 0: direction = 1 delay = PER_LINE_DELAY if top == 0 or top == max_top: if wrote_to_sd is False: wrote_to_sd, lines[0] = save_error_log(msg, filename) delay = LONG_DELAY def show_fatal_error(msg): all_lines = msg.split('\n')[0:] # Remove lines we don't want to shorten lines = all_lines[1:-2] # Insert lines we want to add for readability or keep from the original msg lines.insert(0, "") lines.insert(1, "") lines.append("") lines.append(all_lines[-2]) ux_show_fatal("\n".join(lines)) def restore_menu(): # redraw screen contents after disrupting it w/ non-ux things (usb upload) m = the_ux.top_of_stack() if hasattr(m, 'update_contents'): m.update_contents() if hasattr(m, 'show'): m.show() def abort_and_goto(m): import common common.keypad.clear_keys() the_ux.reset(m) def abort_and_push(m): common.keypad.clear_keys() the_ux.push(m) # async def show_qr_codes(addrs, is_alnum, start_n): # o = QRDisplay(addrs, is_alnum, start_n) # await o.interact_bare() # class QRDisplay(UserInteraction): # # Show a QR code for (typically) a list of addresses. Can only work on Mk3 # # def __init__(self, addrs, is_alnum, start_n=0,path='', account=0, change=0): # self.is_alnum = is_alnum # self.idx = 0 # start with first address # self.invert = False # looks better, but neither mode is ideal # self.addrs = addrs # self.start_n = start_n # self.qr_data = None # self.left_down = False # self.right_down = False # self.input = KeyInputHandler(down='xyudlr', up='xy') # # def render_qr(self, msg): # # Version 2 would be nice, but can't hold what we need, even at min error correction, # # so we are forced into version 3 = 29x29 pixels # # - see # # - to display 29x29 pixels, we have to double them up: 58x58 # # - not really providing enough space around it # # - inverted QR (black/white swap) still readable by scanners, altho wrong # # from utils import imported # # with imported('uQR') as uqr: # if self.is_alnum: # # targeting 'alpha numeric' mode, typical len is 42 # ec = uqr.ERROR_CORRECT_Q # assert len(msg) <= 47 # else: # # has to be 'binary' mode, altho shorter msg, typical 34-36 # ec = uqr.ERROR_CORRECT_M # assert len(msg) <= 42 # # q = uqr.QRCode(version=3, box_size=1, border=0, # mask_pattern=3, error_correction=ec) # if self.is_alnum: # here = uqr.QRData(msg.upper().encode('ascii'), # mode=uqr.MODE_ALPHA_NUM, check_data=False) # else: # here = uqr.QRData(msg.encode('ascii'), # mode=uqr.MODE_8BIT_BYTE, check_data=False) # q.add_data(here) # q.make(fit=False) # # self.qr_data = q.get_matrix() # # def redraw(self): # # Redraw screen. # from common import dis, system # from display import FontTiny # # system.turbo(True) # font = FontTiny # inv = self.invert # # # what we are showing inside the QR # msg, path = self.addrs[self.idx] # # # make the QR, if needed. # if not self.qr_data: # # dis.busy_bar(True) # self.render_qr(msg) # # # Draw display # if inv: # dis.dis.fill_rect(0, 0, Display.WIDTH, # Display.HEIGHT - Display.FOOTER_HEIGHT + 1, 1) # else: # dis.clear() # # y = TOP_MARGIN # # # Draw the derivation path # if len(self.addrs) > 1: # dis.text(None, y, path, font, invert=inv) # y += font.leading + VERT_SPACING # # w = 29 # because version=3 # module_size = 5 # Each "dot" in a QR code is called a module # pixel_width = w * module_size # frame_width = pixel_width + (module_size * 2) # # # QR code offsets # XO = (Display.WIDTH - pixel_width) // 2 # YO = y # dis.dis.fill_rect(XO - module_size, YO - # module_size, frame_width, frame_width, 0 if inv else 1) # # # Draw the actual QR code # data = self.qr_data # for qx in range(w): # for qy in range(w): # px = data[qx][qy] # X = (qx*module_size) + XO # Y = (qy*module_size) + YO # dis.dis.fill_rect(X, Y, module_size, # module_size, px if inv else (not px)) # # # Show the data encoded by the QR code # y += w*module_size + VERT_SPACING + 3 # # MAX_ADDR_CHARS_PER_LINE = 16 # for i in range(0, len(msg), MAX_ADDR_CHARS_PER_LINE): # dis.text(None, y, msg[i:i+MAX_ADDR_CHARS_PER_LINE], font, inv) # y += font.leading # # dis.draw_footer('BACK', 'INVERT', self.input.is_pressed( # 'x'), self.input.is_pressed('y')) # dis.show() # system.turbo(False) # # async def interact_bare(self): # # self.redraw() # while 1: # event = await self.input.get_event() # # if event != None: # key, event_type = event # if event_type == 'down': # if key == 'u' or key == 'l': # if self.idx > 0: # self.idx -= 1 # self.qr_data = None # elif key == 'd' or key == 'r': # if self.idx != len(self.addrs)-1: # self.idx += 1 # self.qr_data = None # elif key in 'xy': # # Allow buttons to redraw in pressed state # pass # else: # continue # elif event_type == 'up': # if key == 'x': # self.redraw() # break # elif key == 'y': # self.invert = not self.invert # else: # continue # # self.redraw() # # async def interact(self): # await self.interact_bare() # the_ux.pop() async def ux_show_text_as_ur(title='QR Code', qr_text='', qr_type=QRType.UR2, qr_args=None, msg=None, left_btn='BACK', right_btn='RESIZE'): # print('ux_show_text_as_ur: qr_type: {}'.format(qr_type)) o = DisplayURCode(title, qr_text, qr_type, qr_args=qr_args, msg=msg, left_btn=left_btn, right_btn=right_btn) result = await o.interact_bare() gc.collect() return result def qr_get_module_size_for_version(version): # 1 -> 21 # 2 -> 25 # etc. return version * 4 + 17 def qr_buffer_size_for_version(version): size = qr_get_module_size_for_version(version) return ((size * size) + 7) // 8 class DisplayURCode(UserInteraction): # Show a QR code or a series of codes in Blockchain Commons' UR format # Purpose is to allow a QR code to be scanned, so we make it as big as possible # given our screen size, but if it's too big, we display a series of images # instead. def __init__(self, title, qr_text, qr_type, qr_args=None, msg=None, left_btn='DONE', right_btn='RESIZE', is_binary=False): self.title = title self.qr_text = qr_text self.input = KeyInputHandler(down='xy', up='xy') self.last_version = 0 self.msg = msg self.left_btn = left_btn self.right_btn = right_btn self.num_supported_sizes = 0 self.qr_version_idx = 0 # "version" for QR codes essentially maps to the size self.render_id = 0 self.last_render_id = -1; self.qr_type = qr_type # print('DisplayURCode: qr_type: {}'.format(qr_type)) self.qr_args = qr_args self.is_binary = is_binary system.turbo(True) self.generate_qr_data() self.qr_data = None system.turbo(False) def generate_qr_data(self): dis.fullscreen('Generating QR') system.show_busy_bar() # Instantiate the right type of QR encoder - always make a new one self.qr_encoder = make_qr_encoder(self.qr_type, self.qr_args) self.num_supported_sizes = self.qr_encoder.get_num_supported_sizes() # We collect before and after to ensure the most available memory gc.collect() max_len = self.qr_encoder.get_max_len(self.qr_version_idx) self.qr_encoder.encode(self.qr_text, is_binary=self.is_binary, max_fragment_len=max_len) gc.collect() system.hide_busy_bar() def set_next_density(self): self.last_version = 0; self.qr_version_idx = (self.qr_version_idx + 1) % self.num_supported_sizes def get_frame_delay(self): if self.qr_version_idx == 0: return 200 else: return 250 def render_qr(self, data): from utils import imported, bytes_to_hex_str # print('data: {}'.format(bytes_to_hex_str(data))) if self.last_render_id != self.render_id: self.last_render_id = self.render_id # Release old buffer and collect so we can reuse that memory self.qr_data = None gc.collect() # Render QR data to buffer # print('qr={}'.format(data.upper())) encoded_data = data.encode('ascii') if self.qr_type != QRType.QR: encoded_data = encoded_data.upper() ll = len(encoded_data) from foundation import QRCode qrcode = QRCode() version = qrcode.fit_to_version(ll, is_alphanumeric_qr(encoded_data)) # Don't go to a smaller QR code, even if it means repeated data since it looks weird # to change the QR code size if self.last_version > version: version = self.last_version else: self.last_version = version buf_size = qr_buffer_size_for_version(version) self.modules_count = qr_get_module_size_for_version(version) # print('fit_to_version({}) = {} buffer size = {}'.format(ll, version,buf_size)) # TODO: Use correct buffer size here or just allocate once outside the loop (largest possible size) out_buf = bytearray(2000) result = qrcode.render(encoded_data, version, 0, out_buf) self.qr_data = out_buf def redraw(self): # Redraw screen from common import dis from display import FontTiny system.turbo(True) TOP_MARGIN = 7 font = FontTiny data = self.qr_encoder.next_part() # print('data={}'.format(data)) self.render_qr(data) # Draw QR display dis.clear() dis.draw_header(self.title) # dis.draw_header(self.title, left_text='{}/{}'.format(self.curr_part + 1, len(self.parts))) y = Display.HEADER_HEIGHT + TOP_MARGIN w = self.modules_count # print('modules_count={}'.format(w)) if self.msg: module_pixel_width = (Display.WIDTH - 60) // w else: module_pixel_width = (Display.WIDTH - 20) // w # print('module_pixel_width={}'.format(module_pixel_width)) total_pixel_width = w * module_pixel_width frame_width = total_pixel_width + (module_pixel_width * 2) # QR code offsets XO = (Display.WIDTH - total_pixel_width) // 2 # Center vertically now that we have no label underneath YO = ((Display.HEIGHT - Display.HEADER_HEIGHT - Display.FOOTER_HEIGHT) - total_pixel_width ) // 2 + Display.HEADER_HEIGHT dis.dis.fill_rect(XO - module_pixel_width, YO - module_pixel_width, frame_width, frame_width, 0) # Draw the actual QR code # print('qr_data = {}'.format(self.qr_data)) if self.qr_data != None: for qy in range(w): for qx in range(w): offset = qy * self.modules_count + qx px = (self.qr_data[offset >> 3]) & (1 << (7 - (offset & 0x07))) X = (qx*module_pixel_width) + XO Y = (qy*module_pixel_width) + YO dis.dis.fill_rect(X, Y, module_pixel_width, module_pixel_width, px) # Draw message if self.msg != None: dis.text(None, Display.HEIGHT - Display.FOOTER_HEIGHT - 20, self.msg, font=FontTiny) dis.draw_footer( self.left_btn, self.right_btn, self.input.is_pressed('x'), self.input.is_pressed('y') ) dis.show() system.turbo(False) async def interact_bare(self): self.redraw() while 1: event = await self.input.get_event() if event != None: # print('event={}'.format(event)) key, event_type = event if event_type == 'up': if key == 'x': self.redraw() return 'x' elif key == 'y': if self.right_btn == 'RESIZE': system.turbo(True) self.set_next_density() self.generate_qr_data() self.render_id += 1 system.turbo(False) else: # User has something else in mind self.redraw() return 'y' else: # Only need to check timer and advance part number if we have more than one part # if len(self.parts) > 1: # Show the next part after a short delay to control speed await sleep_ms(self.get_frame_delay()) self.render_id += 1 self.redraw() continue self.redraw() async def interact(self): await self.interact_bare() the_ux.pop() # async def ux_enter_number(prompt, max_value): # # return the decimal number which the user has entered # # - default/blank value assumed to be zero # # - clamps large values to the max # from common import dis # from display import FontTiny, FontSmall # from math import log # # # allow key repeat on X only # press = PressRelease('1234567890y') # # footer = "X to DELETE, or OK when DONE." # y = 26 # value = '' # max_w = int(log(max_value, 10) + 1) # # while 1: # dis.clear() # dis.text(0, 0, prompt) # # # text centered # if value: # bx = dis.text(None, y, value) # dis.icon(bx+1, y+11, 'space') # else: # dis.icon(64-7, y+11, 'space') # # dis.text(None, -1, footer, FontTiny) # dis.show() # # # ======================================== # # ======================================== # # ======================================== # # ======================================== # # TODO: Replace with KeyInputHandler # # ======================================== # # ======================================== # # ======================================== # # ======================================== # # ch = await press.wait() # if ch == 'y': # # if not value: # return 0 # return min(max_value, int(value)) # # elif ch == 'x': # if value: # value = value[0:-1] # else: # # quit if they press X on empty screen # return 0 # else: # if len(value) == max_w: # value = value[0:-1] + ch # else: # value += ch # # # cleanup leading zeros and such # value = str(int(value)) async def ux_scan_qr_code(title): import common from common import dis, qr_buf, viewfinder_buf from display import FontSmall from utils import save_qr_code_image import utime from constants import VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT, CAMERA_WIDTH, CAMERA_HEIGHT from foundation import Camera, QR font = FontSmall # Create the Camera connection cam = Camera() cam.enable() # Create QR decoder qr = QR(CAMERA_WIDTH, CAMERA_HEIGHT, qr_buf) qr_code = None data = None error = None input = KeyInputHandler(up='xy', down='xy') fps_start = utime.ticks_us() frame_count = 0 qr_decoder = None progress = None while True: frame_start = utime.ticks_us() snapshot_start = frame_start result = cam.snapshot(qr_buf, CAMERA_WIDTH, CAMERA_HEIGHT, viewfinder_buf, VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT) snapshot_end = utime.ticks_us() if not result: # print("ERROR: cam.copy_capture() returned False!") await ux_show_story('Unable to capture image with camera.', title='Error') return None draw_start = utime.ticks_us(); dis.clear() dis.draw_header(title) dis.image(0, Display.HEADER_HEIGHT, VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT, viewfinder_buf) OFFSET = 6 SIZE = 30 THICKNESS = 6 LEFT_X = OFFSET RIGHT_X = Display.WIDTH - OFFSET * 2 Y = Display.HEADER_HEIGHT + OFFSET right_label = progress if progress != None else 'SCANNING...' dis.draw_footer('BACK', right_label, left_down=input.is_pressed('x'), right_down=input.is_pressed('y')) draw_end = utime.ticks_us(); show_start = utime.ticks_us(); dis.show() show_end = utime.ticks_us(); # Look for QR codes in the image decode_start = utime.ticks_us() data = qr.find_qr_codes() # print('find_qr_codes() out') decode_end = utime.ticks_us() # Don't try decoding if we are in Snapshot mode...user is probably using this as a viewfinder if not common.snapshot_mode_enabled and data != None: # print('data={}'.format(data)) # See if this looks like a ur code ur_start = utime.ticks_us() try: if qr_decoder == None: # We need to find out what type of QR this is and have the factory make a decoder for us qr_decoder = get_qr_decoder_for_data(data) # We should be guaranteed to have a qr_decoder here since basic QR accepts any data format qr_decoder.add_data(data) # See if there was any error error = qr_decoder.get_error() if error != None: # print('ERROR: error={}'.format(error)) data = None break if qr_decoder.is_complete(): data = qr_decoder.decode() # print('data: |{}|'.format(data)) # Set the last QRType so that signed transactions know what to encode as common.last_scanned_qr_type = qr_decoder.get_data_format() common.last_scanned_ur_prefix = qr_decoder.get_ur_prefix() # print('common.last_scanned_qr_type={}'.format(common.last_scanned_qr_type)) # print('common.last_scanned_ur_prefix={}'.format(common.last_scanned_ur_prefix)) break progress = '{} OF {}'.format(qr_decoder.received_parts(), qr_decoder.total_parts()) except Exception as e: # print('Failed to parse UR!') import sys # print('Exception: {}'.format(e)) sys.print_exception(e) break ur_end = utime.ticks_us() # print('ur decode: {}us'.format(ur_end - ur_start)) # Check for key input to see if we should back out key_start = utime.ticks_us() event = await input.get_event() if event != None: key, event_type = event if event_type == 'up': if key == 'x': data = None break key_end = utime.ticks_us() frame_count += 1 now = utime.ticks_us() snapshot_ms = (snapshot_end - snapshot_start) / 1000 draw_ms = (draw_end - draw_start) / 1000 show_ms = (show_end - show_start) / 1000 decode_ms = (decode_end - decode_start) / 1000 key_ms = (key_end - key_start) / 1000 total_ms = snapshot_ms + draw_ms + show_ms + decode_ms + key_ms measured_frame_ms = (now - frame_start) / 1000 fps = frame_count / ((now - fps_start) / 1000000) print_stats = False if print_stats and frame_count % 10 == 0: print_start = utime.ticks_us() print('Frame Stats:') print(' {:>3.2f}ms Snapshot'.format(snapshot_ms)) print(' {:>3.2f}ms Draw'.format(draw_ms)) print(' {:>3.2f}ms Update to Screen'.format(show_ms)) print(' {:>3.2f}ms Decode'.format(decode_ms)) print(' {:>3.2f}ms Check keys'.format(key_ms)) print(' --------') print(' {:>3.2f}ms Total of the above\n'.format(total_ms)) print(' {:>3.2f}ms Total measured frame time'.format(measured_frame_ms)) print(' {:>3.2f}ms Missing time\n'.format(measured_frame_ms - total_ms)) print(' {:>3.2f}fps Frame rate'.format(fps)) print_end = utime.ticks_us() print_ms = (print_end - print_start) / 1000 print(' {:03.1f}ms Print time'.format(print_ms)) # Turn off camera after capturing is done! cam.disable() return data async def ux_show_story_sequence(stories): story_idx = 0 while 1: s = stories[story_idx] key = await ux_show_story( s.get('msg'), title=s.get('title', 'Passport'), sensitive=s.get('sensitive', False), left_btn=s.get('left_btn', 'BACK'), right_btn=s.get('right_btn', 'CONTINUE'), center=s.get('center', False), center_vertically=s.get('center_vertically', False), scroll_label=s.get('scroll_label', None), clear_keys=s.get('clear_keys', False)) if key == 'x': if story_idx == 0: return 'x' else: story_idx -= 1 elif key == 'y': if story_idx == len(stories) - 1: return 'y' else: story_idx += 1 async def ux_show_word_list(title, words, heading1='', heading2=None, left_aligned_center=False, left_btn='NO', right_btn='YES'): from common import dis font = FontSmall input = KeyInputHandler(up='xy', down='xy') # Figure out horizonal start - we want to center based on the longest word x = None if left_aligned_center: longest_word_width = 0 for word in words: px_width = dis.width(word, font) if px_width > longest_word_width: longest_word_width = px_width x = Display.HALF_WIDTH - (longest_word_width // 2) while True: system.turbo(True) dis.clear() dis.draw_header(title) y = Display.HEADER_HEIGHT + TOP_MARGIN dis.text(None, y, heading1, font=font) y += font.leading if heading2 != None: dis.text(None, y, heading2, font=font) y += font.leading * 2 else: y += font.leading # Show the word list for word in words: dis.text(x, y, word, font=font) y += font.leading dis.draw_footer(left_btn=left_btn, right_btn=right_btn, left_down=input.is_pressed('x'), right_down=input.is_pressed('y')) dis.show() system.turbo(False) while 1: event = await input.get_event() if event != None: break key, event_type = event if event_type == 'up': if key in 'xy': return key async def ux_enter_pin(title, heading='Enter PIN', left_btn='BACK', right_btn='ENTER', left_btn_function=None, hide_attempt_counter=False, is_new_pin=False): from common import dis, pa import pincodes SHOW_SECURITY_WORDS_AT_LEN = 4 MIN_PIN_LEN = 6 MAX_PIN_LEN = 12 LARGE_BOX_LIMIT = 6 POPUP_WIDTH = Display.WIDTH - 40 font = FontSmall input = KeyInputHandler(up='xy0123456789', down='xy0123456789*') show_security_words = False security_words_confirmed = False show_short_pin_message = False pin = '' pressed = False security_label = 'NEXT' if is_new_pin else 'CONFIRM' def draw_popup(title, lines, y): POPUP_HEIGHT = FontSmall.leading * 4 popup_x = Display.HALF_WIDTH - POPUP_WIDTH // 2 dis.draw_rect(popup_x, y, POPUP_WIDTH, FontSmall.leading + 2, border_w=0, fill_color=1) dis.draw_rect(popup_x, y+FontSmall.leading, POPUP_WIDTH, POPUP_HEIGHT - FontSmall.leading, border_w=2) dis.text(None, y+3, title, font=FontSmall, invert=True) y += int(FontSmall.leading * 1.5) for line in lines: dis.text(None, y, line, font=FontSmall) y += FontSmall.leading return y def draw_security_words_popup(title1, title2, words, y): POPUP_HEIGHT = FontSmall.leading * 2 popup_x = Display.HALF_WIDTH - POPUP_WIDTH // 2 dis.draw_rect(popup_x, y, POPUP_WIDTH, FontSmall.leading * 2 + 2, border_w=0, fill_color=1) dis.draw_rect(popup_x, y+FontSmall.leading*2, POPUP_WIDTH, POPUP_HEIGHT, border_w=2) dis.text(None, y+3, title1, font=FontSmall, invert=True) y += int(FontSmall.leading)-1 dis.text(None, y+3, title2, font=FontSmall, invert=True) y += int(FontSmall.leading * 1.5) + 2 dis.text(None, y, words, font=FontSmall) return y while True: system.turbo(True) dis.clear() dis.draw_header(title) y = dis.HEADER_HEIGHT + 8 dis.text(None, y, heading, font=FontSmall) y += FontSmall.leading - 2 num_filled = len(pin) # print('num_filled={} pressed={}'.format(num_filled, pressed)) if (num_filled < LARGE_BOX_LIMIT) or (num_filled == LARGE_BOX_LIMIT and not pressed): empty_box = 'pw_empty_box_lg' pressed_box = 'pw_pressed_box_lg' filled_box = 'pw_filled_box_lg' MAX_PIN_BOXES_TO_DISPLAY = LARGE_BOX_LIMIT y += 3 else: empty_box = 'pw_empty_box_sm' pressed_box = 'pw_pressed_box_sm' filled_box = 'pw_filled_box_sm' MAX_PIN_BOXES_TO_DISPLAY = MAX_PIN_LEN y += 14 PIN_BOX_W, PIN_BOX_H = dis.icon_size(empty_box) PIN_BOX_SPACING = (dis.WIDTH - PIN_BOX_W * MAX_PIN_BOXES_TO_DISPLAY) // (MAX_PIN_BOXES_TO_DISPLAY + 1) PIN_BOX_ADVANCE = PIN_BOX_W + PIN_BOX_SPACING if num_filled >= 1: total_width = (num_filled * PIN_BOX_W) + ((num_filled - 1) * PIN_BOX_SPACING) else: total_width = 0 # print('total_width = {}'.format(total_width)) if pressed and num_filled > 0: total_width += PIN_BOX_SPACING + PIN_BOX_W # print('total_width increased to = {}'.format(total_width)) # Have a width when PIN is empty and nothing pressed if total_width == 0: total_width = PIN_BOX_W x = Display.HALF_WIDTH - (total_width // 2) for _idx in range(num_filled): dis.icon(x, y, filled_box) x += PIN_BOX_ADVANCE if pressed: # print('pressed case') dis.icon(x, y, pressed_box) elif len(pin) == 0: # print('draw empty box') dis.icon(x, y, empty_box) # Show remaining attempts if not hidden and if there was at least one failure if not hide_attempt_counter and pa.attempts_left < pa.max_attempts: dis.text(None, Display.HEIGHT - Display.HEADER_HEIGHT - FontTiny.leading + 3, '{} attempts remaining'.format(pa.attempts_left), font=FontTiny) # Since box size can vary, we advance a constant here and center the box in that space based on height above y = 128 if show_short_pin_message: y = draw_popup('Info', ['PIN must be at','least 6 digits'], y) # Show the security word list if key is held down elif show_security_words: if not security_words: try: security_words = pincodes.PinAttempt.anti_phishing_words(pin[0:SHOW_SECURITY_WORDS_AT_LEN].encode()) except Exception as e: security_words = ['Unable to','retrieve'] # print("Exception getting anti-phishing words: {}".format(e)) if is_new_pin: y = draw_security_words_popup('Remember these', 'Security Words', ' '.join(security_words), y) else: y = draw_security_words_popup('Recognize these', 'Security Words?', ' '.join(security_words), y) else: # Clear them so we reload next time (numbers may have changed) security_words = None dis.draw_footer(left_btn, security_label if show_security_words else right_btn, input.is_pressed('x'), input.is_pressed('y')) dis.show() system.turbo(False) # Interaction while True: event = await input.get_event() if event != None: break key, event_type = event if event_type == 'down': # Hide the short pin message as soon as any other key is pressed show_short_pin_message = False if key == '*': if pin and len(pin) > 0: pin = pin[:-1] if len(pin) <= SHOW_SECURITY_WORDS_AT_LEN: security_words_confirmed = False elif key in '0123456789': if not show_security_words: if len(pin) < MAX_PIN_LEN: pressed = True elif event_type == 'up': if key == 'x': return None elif key == 'y': if show_security_words: show_security_words = False security_words_confirmed = True continue elif len(pin) < MIN_PIN_LEN: # they haven't given enough yet show_short_pin_message = True continue else: # print('RETURNING PIN = {}'.format(pin)) return pin elif key in '0123456789': if not show_security_words: pressed = False # Add the number to the PIN or replace the last digit if len(pin) == MAX_PIN_LEN: pass else: pin += key # Decide whether to show the security words if not security_words_confirmed: show_security_words = len(pin) == SHOW_SECURITY_WORDS_AT_LEN async def ux_shutdown(): from common import system confirm = await ux_confirm("Are you sure you want to shutdown?", center=True, center_vertically=True) if confirm: # print('SHUTTING DOWN!') system.shutdown() return Keypad = [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['*', '0', '#'], ] KeyState = { '1': {'pressed': False, 'released': False}, '2': {'pressed': False, 'released': False}, '3': {'pressed': False, 'released': False}, '4': {'pressed': False, 'released': False}, '5': {'pressed': False, 'released': False}, '6': {'pressed': False, 'released': False}, '7': {'pressed': False, 'released': False}, '8': {'pressed': False, 'released': False}, '9': {'pressed': False, 'released': False}, '0': {'pressed': False, 'released': False}, '*': {'pressed': False, 'released': False}, '#': {'pressed': False, 'released': False}, 'l': {'pressed': False, 'released': False}, 'r': {'pressed': False, 'released': False}, 'u': {'pressed': False, 'released': False}, 'd': {'pressed': False, 'released': False}, 'x': {'pressed': False, 'released': False}, 'y': {'pressed': False, 'released': False}, } async def ux_keypad_test(*a): from common import dis from display import FontSmall SIDE_MARGIN = 20 NUMKEY_HGAP = 10 NUMKEY_VGAP = 5 KEY_WIDTH = ((Display.WIDTH - (SIDE_MARGIN*2) - (2 * NUMKEY_HGAP)) // 3) KEY_HEIGHT = 24 font = FontSmall # Hold x or y for one second to exit keypad test input = KeyInputHandler(down='1234567890*#rludxy', up='1234567890*#rludxy', long='xy', long_duration=1000) def draw_key(key, key_x, key_y, small=False, vertical=False): state = KeyState.get(key) pressed = state.get('pressed') released = state.get('released') w = KEY_WIDTH if not small else KEY_WIDTH // 2 dis.draw_rect(key_x, key_y, w, KEY_HEIGHT, 4 if pressed else 2, fill_color=1 if released else 0 , border_color=1) if not small: dis.text(key_x + 22, key_y + 1, key, invert=1 if released else 0) else: dis.text(key_x + 9, key_y + 1, key, invert=1 if released else 0) while True: # Redraw system.turbo(True) dis.clear() dis.draw_header('Keypad Test') # Draw the title y = Display.HEADER_HEIGHT + TEXTBOX_MARGIN dis.text(None, y, 'Press Each Key') y += font.leading # Draw the select and back buttons draw_key('u', Display.HALF_WIDTH - KEY_WIDTH // 4, y, small=True) draw_key('d', Display.HALF_WIDTH - KEY_WIDTH // 4, y + NUMKEY_VGAP + KEY_HEIGHT, small=True) draw_key('l', Display.HALF_WIDTH - KEY_WIDTH // 4 - NUMKEY_HGAP - KEY_WIDTH//2, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) draw_key('r', Display.HALF_WIDTH - KEY_WIDTH // 4 + NUMKEY_HGAP + KEY_WIDTH//2, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) draw_key('x', SIDE_MARGIN, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) draw_key('y', Display.WIDTH - SIDE_MARGIN - KEY_WIDTH//2, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) y += 80 # Draw the Numeric keypad grid for row in range(len(Keypad)): for col in range(len(Keypad[row])): key = Keypad[row][col] key_x = SIDE_MARGIN + (col *(KEY_WIDTH + NUMKEY_HGAP)) key_y = y + row * (KEY_HEIGHT + NUMKEY_VGAP) draw_key(key, key_x, key_y) dis.draw_footer('BACK', 'NEXT', input.is_pressed('x'), input.is_pressed('y')) dis.show() system.turbo(False) # Wait for key inputs event = None while True: event = await input.get_event() if event != None: break if event != None: key, event_type = event state = KeyState.get(key) # Check for footer button actions first if event_type == 'down': state['pressed'] = True if event_type == 'up': state['released'] = True if event_type == 'long_press': return key async def ux_draw_alignment_grid(title='Align Screen'): from common import dis from display import FontSmall NUM_VERTICAL_LINES = 7 NUM_HORIZONTAL_LINES = 14 GRID_WIDTH = Display.WIDTH // (NUM_VERTICAL_LINES + 1) GRID_HEIGHT = Display.HEIGHT // (NUM_HORIZONTAL_LINES + 1) # print('GRID_WIDTH={}'.format(GRID_WIDTH)) # print('GRID_HEIGHT={}'.format(GRID_HEIGHT)) font = FontSmall input = KeyInputHandler(down='xy', up='xy') pressed = {} while True: # Redraw system.turbo(True) dis.clear() # dis.draw_header(title) # dis.draw_footer('BACK', 'NEXT', input.is_pressed('x'), input.is_pressed('y')) # Draw an inset rectangle around the outside dis.draw_rect(2, 2, Display.WIDTH-4, Display.HEIGHT-4, 1, fill_color=0, border_color=1) # Draw vertical lines for col in range(NUM_VERTICAL_LINES): x = (col + 1) * GRID_WIDTH dis.vline(x) # Draw Horizontal lines for row in range(NUM_HORIZONTAL_LINES): y = (row + 1) * GRID_HEIGHT dis.hline(y) dis.show() system.turbo(False) # Wait for key inputs event = None while True: event = await input.get_event() if event != None: break if event != None: key, event_type = event state = KeyState.get(key) # Exit if key pressed if event_type == 'down': pressed[key] = True if event_type == 'up': # We may have come to this screen from a longpress on the keypad test screen # in which case an up will come that we DON'T want to act on. The local input # won't have transitioned the key to pressed state yet, so use this to differentiate. if key in pressed: return key