async def do_delay(self, pa): # show # of failures and implement the delay, which could be # very long. from main import numpad dis.clear() dis.text(None, 0, "Checking...", FontLarge) dis.text(None, 24, 'Wait '+pretty_delay(pa.delay_required * pa.seconds_per_tick)) dis.text(None, 40, "(%d failures)" % pa.num_fails) # save a little bit of interrupt load/overhead numpad.stop() while pa.is_delay_needed(): dis.progress_bar(pa.delay_achieved / pa.delay_required) dis.show() pa.delay() numpad.start()
def set_genuine(): # PIN must be blank for this to work # - or logged in already as main from main import pa if pa.is_secondary: return if not pa.is_successful(): # assume blank pin during factory selftest pa.setup(b'') assert not pa.is_delay_needed() # "PIN failures?" if not pa.is_successful(): pa.login() assert pa.is_successful() # "PIN not blank?" # do verify step pa.greenlight_firmware() dis.show()
def _show_words(self, has_secondary=False): dis.clear() dis.text(None, 0, "Recognize these?" if (not self.is_setting) or self.is_repeat \ else "Write these down:") dis.show() dis.busy_bar(True) words = pincodes.PinAttempt.prefix_words(self.pin.encode()) y = 15 x = 18 dis.text(x, y, words[0], FontLarge) dis.text(x, y+18, words[1], FontLarge) if self.offer_second: dis.text(None, -1, "Press (2) for secondary wallet", FontTiny) else: dis.text(None, -1, "X to CANCEL, or OK to CONTINUE", FontTiny) dis.busy_bar(False) # includes a dis.show()
def show_fatal_error(msg): # show a multi-line error message, over some kinda "fatal" banner from main import dis from display import FontTiny dis.clear() lines = msg.split('\n')[-6:] dis.text(None, 1, '>>>> Yikes!! <<<<') y = 13 + 2 for num, ln in enumerate(lines): ln = ln.strip() if ln[0:6] == 'File "': # convert: File "main.py", line 63, in interact # into: main.py:63 interact ln = ln[6:].replace('", line ', ':').replace(', in ', ' ') dis.text(0, y + (num * 8), ln, FontTiny) dis.show()
def draw_background(self): # Render and capture static parts of screen one-time. from main import dis from display import FontTiny dis.clear() dis.text(6, 0, "HSM MODE") dis.show() # cover the 300ms or so it takes to draw the rest below dis.hline(15) x, y = 0, 28 for lab, xoff, val in [ ('APPROVED', 0, '0'), ('REFUSED', 0, '0'), ('PERIOD LEFT', 5, 'xx'), ]: nx = dis.text(x+xoff, y-7, lab, FontTiny) hw = nx - x if lab == 'REFUSED': dis.dis.line(nx+2, 0, nx+2, y+16, 1) else: if not xoff: dis.dis.line(nx+2, y-12, nx+2, y+16, 1) # keep this: #print('%s @ x=%d' % (lab, x+(hw//2)-2)) # was: #tw = 7*len(val) # = dis.width(val, FontSmall) #dis.text(x+((hw-tw)//2)-1, y+1, val) x = nx + 7 dis.hline(y+17) # no local confirmation code entered, typically dis.text(80, 0, '######') # save this static background self.screen_buf = dis.dis.buffer[:]
def show(self): # # Redraw the menu. # dis.clear() #print('cur=%d ypos=%d' % (self.cursor, self.ypos)) # subclass hook self.early_draw(dis) x, y = (10, 2) h = 14 for n in range(self.ypos + PER_M + 1): if n + self.ypos >= self.count: break msg = self.items[n + self.ypos].label is_sel = (self.cursor == n + self.ypos) if is_sel: dis.dis.fill_rect(0, y, 128, h - 1, 1) dis.icon(2, y, 'wedge', invert=1) dis.text(x, y, msg, invert=1) else: dis.text(x, y, msg) if msg[0] == ' ' and self.space_indicators: dis.icon(x - 2, y + 11, 'space', invert=is_sel) if self.chosen is not None and (n + self.ypos) == self.chosen: dis.icon(108, y, 'selected', invert=is_sel) y += h if y > 128: break # subclass hook self.late_draw(dis) if self.count > PER_M: dis.scroll_bar(self.ypos / (self.count - PER_M)) dis.show()
async def test_microsd(): if ckcc.is_simulator(): return async def wait_til_state(want): dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Remove' if sd.present() else 'Insert', font=FontLarge) dis.show() while 1: if want == sd.present(): return await sleep_ms(100) if ux_poll_once(): raise RuntimeError("MicroSD test aborted") try: import pyb sd = pyb.SDCard() sd.power(0) # test presence switch for ph in range(7): await wait_til_state(not sd.present()) if ph >= 2 and sd.present(): # debounce await sleep_ms(100) if sd.present(): break if ux_poll_once(): raise RuntimeError("MicroSD test aborted") dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Testing', font=FontLarge) dis.show() # card inserted assert sd.present() #, "SD not present?" # power up? sd.power(1) await sleep_ms(100) try: blks, bsize, *unused = sd.info() assert bsize == 512 except: assert 0 # , "card info" # just read it a bit, writing would prove little buf = bytearray(512) msize = 256*1024 for addr in range(0, msize, 1024): sd.readblocks(addr, buf) dis.progress_bar_show(addr/msize) if addr == 0: assert buf[-2:] == b'\x55\xaa' # "Bad read" # force removal, so cards don't get stuck in finished units await wait_til_state(False) finally: # CRTICAL: power it back down sd.power(0)
async def spinner_edit(pw): # Allow them to pick each digit using "D-pad" from main import dis from display import FontTiny, FontSmall # Should allow full unicode, NKDN # - but limited to what we can show in FontSmall # - so really just ascii; not even latin-1 # - 8-bit codepoints only my_rng = range(32, 127) # FontSmall.code_range symbols = b' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' letters = b'abcdefghijklmnopqrstuvwxyz' Letters = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' numbers = b'1234567890' #assert len(set(symbols+letters+Letters+numbers)) == len(my_rng) footer1 = "1=Letters 2=Numbers 3=Symbols" footer2 = "4=SwapCase 0=HELP" y = 20 pw = bytearray(pw or 'A') pos = len(pw)-1 # which part being changed n_visible = const(9) scroll_x = max(pos - n_visible, 0) def cycle_set(which, direction=1): # pick next item in set of choices for n, s in enumerate(which): if pw[pos] == s: try: pw[pos] = which[n+direction] except IndexError: pw[pos] = which[0 if direction==1 else -1] return pw[pos] = which[0] def change(dx): # next/prev within the same subset of related chars ch = pw[pos] for subset in [symbols, letters, Letters, numbers]: if ch in subset: return cycle_set(subset, dx) # probably unreachable code: numeric up/down ch = pw[pos] + dx if ch not in my_rng: ch = (my_rng.stop-1) if dx < 0 else my_rng.start assert ch in my_rng pw[pos] = ch # no key-repeat on certain keys press = PressRelease('4xy') while 1: dis.clear() lr = pos - scroll_x # left/right distance of cursor if lr < 4 and scroll_x: scroll_x -= 1 elif lr < 0: scroll_x = pos elif lr >= (n_visible-1): # past right edge scroll_x += 1 for i in range(n_visible): # calc abs position in string ax = scroll_x + i x = 4 + (13*i) try: ch = pw[ax] except IndexError: continue if ax == pos: # draw cursor if len(pw) < 2*n_visible: dis.text(x-4, y-19, '0x%02X' % ch, FontTiny) dis.icon(x-2, y-10, 'spin') if ch == 0x20: dis.icon(x, y+11, 'space') else: dis.text(x, y, chr(ch) if ch in my_rng else chr(215), FontSmall) if scroll_x > 0: dis.text(2, y-14, str(pw, 'ascii')[0:scroll_x].replace(' ', '_'), FontTiny) if scroll_x + n_visible < len(pw): dis.text(-1, 1, "MORE>", FontTiny) if 0: wy = 6 count = len(pw) dis.text(-8, wy-4, "%d" % count) dis.text(None, -10, footer1, FontTiny) dis.text(None, -1, footer2, FontTiny) dis.show() ch = await press.wait() if ch == 'y': return str(pw, 'ascii') elif ch == 'x': if len(pw) > 1: # delete current char pw = pw[0:pos] + pw[pos+1:] if pos >= len(pw): pos = len(pw)-1 else: pp = await ux_show_story("OK to leave without any changes? Or X to cancel leaving.") if pp == 'x': continue return None elif ch == '7': # left pos -= 1 if pos < 0: pos = 0 elif ch == '9': # right pos += 1 if pos >= len(pw): if len(pw) < 100 and pw[-3:] != b' ': pw += ' ' # expand with spaces else: pos -= 1 # abort addition elif ch == '5': # up change(1) elif ch == '8': # down change(-1) elif ch == '1': # alpha cycle_set(b'Aa') elif ch == '4': # toggle case if (pw[pos] & ~0x20) in range(65, 91): pw[pos] ^= 0x20 elif ch == '2': # numbers cycle_set(numbers) elif ch == '3': # symbols (all of them) cycle_set(symbols) elif ch == '0': # help await ux_show_story('''\ Use arrow keys (5789) to select letter and move around. 1=Letters (Aa..) 2=Numbers (12..) 3=Symbols (!@#&*) 4=Swap Case (q/Q) X=Delete char To quit without changes, delete everything. \ Add more characters by moving past end (right side). ''')
async def ux_show_story(msg, title=None, escape=None, sensitive=False, strict_escape=False): # show a big long string, and wait for XY to continue # - returns character used to get out (X or Y) # - can accept other chars to 'escape' as well. # - accepts a stream or string from main import dis, numpad from display import FontLarge assert not numpad.disabled # probably inside a CardSlot context lines = [] if title: # kinda weak rendering but it works. lines.append('\x01' + title) if hasattr(msg, 'readline'): msg.seek(0) for ln in msg: if ln[-1] == '\n': ln = ln[:-1] if len(ln) > CH_PER_W: lines.extend(word_wrap(ln, CH_PER_W)) else: # ok if empty string, just a blank line lines.append(ln) # no longer needed & rude to our caller, but let's save the memory msg.close() del msg gc.collect() else: for ln in msg.split('\n'): if len(ln) > CH_PER_W: lines.extend(word_wrap(ln, CH_PER_W)) else: # ok if empty string, just a blank line lines.append(ln) # trim blank lines at end, add our own marker while not lines[-1]: lines = lines[:-1] lines.append('EOT') #print("story:\n\n\"" + '"\n"'.join(lines)) #lines[0] = '111111111121234567893' top = 0 H = 5 ch = None pr = PressRelease() while 1: # redraw dis.clear() y = 0 for ln in lines[top:top + H]: if ln == 'EOT': dis.hline(y + 3) elif ln and ln[0] == '\x01': dis.text(0, y, ln[1:], FontLarge) y += 21 else: dis.text(0, y, ln) if sensitive and len(ln) > 3 and ln[2] == ':': dis.mark_sensitive(y, y + 13) y += 13 dis.scroll_bar(top / len(lines)) dis.show() # wait to do something ch = await pr.wait() if escape and (ch == escape or ch in escape): # allow another way out for some usages return ch elif ch in 'xy': if not strict_escape: return ch elif ch == '0': top = 0 elif ch == '7': # page up top = max(0, top - H) elif ch == '9': # page dn top = min(len(lines) - 2, top + H) elif ch == '5': # scroll up top = max(0, top - 1) elif ch == '8': # scroll dn top = min(len(lines) - 2, top + 1)
def calc(self): # average history, apply threshold to know which are "down" if self.debug == 1: print('\x1b[H\x1b[2J\n') LABELS = [('col%d' % n) for n in range(3)] + [('row%d' % n) for n in range(4)] if self.debug == 2: from main import dis dis.clear() pressed = set() now = [] diffs = [] for idx in range(NUM_PINS): avg = self.levels[idx] # not an average anymore now.append(avg) if self.baseline: diff = self.baseline[idx] - avg # the critical "threshold" .. remember, values below this are # might be "light" touches or proximity. if diff > THRESHOLD: pressed.add(idx) if self.debug == 1: print('%s: %5d %4d %d' % (LABELS[idx], avg, diff, idx in pressed)) diffs.append(diff) if self.debug == 2: from main import dis y = (idx * 6) + 3 if 0: x = int((avg * 128) / 16384.) bx = int((self.baseline[idx] * 128) / 16384.) for j in range(4): dis.dis.line(0, y + j, 128, y + j, 0) dis.dis.pixel(x, y, 1) dis.dis.pixel(bx, y + 1, 1) dx = 64 + int(diff / 8) dx = min(max(0, dx), 127) dis.dis.pixel(dx, y + 2, 1) dis.dis.pixel(dx, y + 3, 1) if idx == 0: dx = 64 + int(THRESHOLD / 8) dis.dis.vline(dx, 60, 64, 1) dis.show() if self.debug == 1: print('\n') if diffs: print('min_diff = %d' % min(diffs)) print('avg_diff = %d' % (sum(diffs) / len(diffs))) # should we remember this as a reference point (of no keys pressed) if self.trigger_baseline: self.baseline = now.copy() self.trigger_baseline = False pressed.clear() if self.debug == 2: return # Consider only single-pressed here; we can detect # many 2-key combo's but no plan to support that so they # are probably noise from that PoV. col_down = [i for i in range(3) if i in pressed] row_down = [i - 3 for i in range(3, 7) if i in pressed] if len(col_down) == 1 and len(row_down) == 1: # determine what key key = self.DECODER[(row_down[0], col_down[0])] else: # not sure, or all up key = '' if key != self.key_pressed: # annouce change self.key_pressed = key if self._changes.full(): # no space, but do a "all up" and the new event print('numpad Q overflow') self._changes.get_nowait() self._changes.get_nowait() if key != '': self._changes.put_nowait('') self._changes.put_nowait(key) self.last_event_time = utime.ticks_ms()
async def start_login_sequence(): # Boot up login sequence here. # from main import pa, settings, dis, loop, numpad import version if pa.is_blank(): # Blank devices, with no PIN set all, can continue w/o login # Do green-light set immediately after firmware upgrade if version.is_fresh_version(): pa.greenlight_firmware() dis.show() goto_top_menu() return # Allow impatient devs and crazy people to skip the PIN guess = settings.get('_skip_pin', None) if guess is not None: try: dis.fullscreen("(Skip PIN)") pa.setup(guess) pa.login() except: pass # if that didn't work, or no skip defined, force # them to login succefully. while not pa.is_successful(): # always get a PIN and login first await block_until_login() # Must read settings after login settings.set_key() settings.load() # Restore a login preference or two numpad.sensitivity = settings.get('sens', numpad.sensitivity) # Do green-light set immediately after firmware upgrade if not pa.is_secondary: if version.is_fresh_version(): pa.greenlight_firmware() dis.show() # Populate xfp/xpub values, if missing. # - can happen for first-time login of duress wallet # - may indicate lost settings, which we can easily recover from # - these values are important to USB protocol if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank(): try: import stash # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues() as sv: sv.capture_xpub() except Exception as exc: # just in case, keep going; we're not useless and this # is early in boot process print("XFP save failed: %s" % exc) # Allow USB protocol, now that we are auth'ed from usb import enable_usb enable_usb(loop, False) goto_top_menu()
async def ux_show_story(msg, title=None, escape=None): # show a big long string, and wait for XY to continue # - returns character used to get out (X or Y) # - can accept other chars to 'escape' as well. from main import dis, numpad from display import FontLarge assert not numpad.disabled # probably inside a CardSlot context lines = [] if title: # kinda weak rendering but it works. lines.append('\x01' + title) #lines.append('') for ln in msg.split('\n'): if len(ln) > CH_PER_W: lines.extend(word_wrap(ln, CH_PER_W)) else: # ok if empty string, just a blank line lines.append(ln) # trim blank lines at end, add our own marker while not lines[-1]: lines = lines[:-1] lines.append('EOT') #print("story:\n\n\"" + '"\n"'.join(lines)) #lines[0] = '111111111121234567893' top = 0 H = 5 ch = None pr = PressRelease() while 1: # redraw dis.clear() y = 0 for ln in lines[top:top + H]: if ln == 'EOT': dis.hline(y + 3) elif ln and ln[0] == '\x01': dis.text(0, y, ln[1:], FontLarge) y += 21 else: dis.text(0, y, ln) y += 13 dis.scroll_bar(top / len(lines)) dis.show() # wait to do something ch = await pr.wait() if escape and (ch == escape or ch in escape): # allow another way out for some usages return ch elif ch in 'xy': return ch elif ch == '0': top = 0 elif ch == '5': top = max(0, top - 1) elif ch == '8': top = min(len(lines) - 2, top + 1)
async def start_login_sequence(): # Boot up login sequence here. # from main import pa, settings, dis, loop, numpad from ux import idle_logout if pa.is_blank(): # Blank devices, with no PIN set all, can continue w/o login # Do green-light set immediately after firmware upgrade if version.is_fresh_version(): pa.greenlight_firmware() dis.show() goto_top_menu() return # maybe show a nickname before we do anything nickname = settings.get('nick', None) if nickname: try: await show_nickname(nickname) except: pass # Allow impatient devs and crazy people to skip the PIN guess = settings.get('_skip_pin', None) if guess is not None: try: dis.fullscreen("(Skip PIN)") pa.setup(guess) pa.login() except: pass # if that didn't work, or no skip defined, force # them to login succefully. while not pa.is_successful(): # always get a PIN and login first await block_until_login() # Must re-read settings after login settings.set_key() settings.load() # implement "login countdown" feature delay = settings.get('lgto', 0) if delay: pa.reset() await login_countdown(delay) await block_until_login() # implement idle timeout now that we are logged-in loop.create_task(idle_logout()) # Do green-light set immediately after firmware upgrade if not pa.is_secondary: if version.is_fresh_version(): pa.greenlight_firmware() dis.show() # Populate xfp/xpub values, if missing. # - can happen for first-time login of duress wallet # - may indicate lost settings, which we can easily recover from # - these values are important to USB protocol if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank(): try: import stash # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues() as sv: sv.capture_xpub() except Exception as exc: # just in case, keep going; we're not useless and this # is early in boot process print("XFP save failed: %s" % exc) # If HSM policy file is available, offer to start that, # **before** the USB is even enabled. if version.has_fatram: try: import hsm, hsm_ux if hsm.hsm_policy_available(): ar = await hsm_ux.start_hsm_approval(usb_mode=False, startup_mode=True) if ar: await ar.interact() except: pass # Allow USB protocol, now that we are auth'ed from usb import enable_usb enable_usb(loop, False) goto_top_menu()
def calc(self): # average history, apply threshold to know which are "down" if self.debug == 1: print('\x1b[H\x1b[2J\n') LABELS = [('col%d' % n) for n in range(3)] + [('row%d' % n) for n in range(4)] if self.debug == 2: from main import dis dis.clear() # should we remember this as a reference point (of no keys pressed) if self.trigger_baseline: self.baseline = array.array('I', self.prev_levels) self.trigger_baseline = False if 0: LABELS = [('col%d' % n) for n in range(3)] + [('row%d' % n) for n in range(4)] print("Baselines:") for idx in range(NUM_PINS): print('%s: %5d' % (LABELS[idx], self.baseline[idx])) return pressed = set() diffs = array.array('I') for idx in range(NUM_PINS): # track a running average, using different weights depending on sensitivity mode if self.sensitivity == 0: avg = self.levels[idx] elif self.sensitivity == 1: avg = (self.prev_levels[idx] + self.levels[idx]) // 2 else: avg = ((self.prev_levels[idx] * 3) + self.levels[idx]) // 4 self.prev_levels[idx] = avg if self.baseline: diff = self.baseline[idx] - avg diffs.append(diff) # the critical "threshold" .. remember, values below this are # might be "light" touches or proximity. if diff > THRESHOLD: pressed.add(idx) # handle baseline drift, in one direction at least if diff < 0: self.baseline[idx] = avg if self.debug == 1: print('%s: %5d %4d %d' % (LABELS[idx], avg, diff, idx in pressed)) if self.debug == 2: from main import dis y = (idx * 6) + 3 dx = 64 + int(diff / 8) dx = min(max(0, dx), 127) dis.dis.pixel(dx, y + 1, 1) dis.dis.pixel(dx, y + 2, 1) dx = 64 + int(THRESHOLD / 8) dis.dis.pixel(dx, y, 1) dis.dis.pixel(dx, y + 3, 1) dis.show() if max(diffs, default=0) < -10 or (len(pressed) > 4): print("auto recal") self.baseline = array.array('I', self.prev_levels) if self.debug == 1: print('\n') if diffs: print('min_diff = %5d / %5d / %5d' % (min(diffs), (sum(diffs) / len(diffs)), max(diffs))) if self.debug == 2: return # Consider only single-pressed here; we can detect # many 2-key combo's but no plan to support that so they # are probably noise from that PoV. col_down = [i for i in range(3) if i in pressed] row_down = [i - 3 for i in range(3, 7) if i in pressed] if len(col_down) == 1 and len(row_down) == 1: # determine what key key = self.DECODER[(row_down[0], col_down[0])] else: # not sure, or all up key = '' if key != self.key_pressed: # annouce change self.key_pressed = key if self._changes.full(): # no space, but do a "all up" and the new event print('numpad Q overflow') self._changes.get_nowait() self._changes.get_nowait() if key != '': self._changes.put_nowait('') self._changes.put_nowait(key) self.last_event_time = utime.ticks_ms()
async def test_microsd(): if ckcc.is_simulator(): return from main import numpad numpad.stop() try: import pyb sd = pyb.SDCard() sd.power(0) # test presence switch for ph in range(7): want = not sd.present() dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Remove' if sd.present() else 'Insert', font=FontLarge) dis.show() while 1: if want == sd.present(): break await sleep_ms(100) if ux_poll_once(): raise RuntimeError("MicroSD test aborted") if ph >= 2 and sd.present(): # debounce await sleep_ms(100) if sd.present(): break if ux_poll_once(): raise RuntimeError("MicroSD test aborted") dis.clear() dis.text(None, 10, 'MicroSD Card:') dis.text(None, 34, 'Testing', font=FontLarge) dis.show() # card inserted assert sd.present(), "SD not present?" # power up? sd.power(1) await sleep_ms(100) try: blks, bsize, ctype = sd.info() assert bsize == 512, "wrong block size" except: assert 0, "unable to get card info" # just read it a bit, writing would prove little buf = bytearray(512) msize = 1024 * 1024 for addr in range(0, msize, 1024): sd.readblocks(addr, buf) dis.progress_bar_show(addr / msize) if addr == 0: assert buf[-2:] == b'\x55\xaa', "Bad read" finally: # CRTICAL: power it back down sd.power(0) numpad.start()