def show_pin_randomized(self, force_draw): # screen redraw, when we are "randomized" if force_draw: dis.clear() # prompt dis.text(5+3, 2, "ENTER PIN") dis.text(5+6, 17, ('1st part' if not self.pin_prefix else '2nd part')) # remapped keypad y = 2 x = 89 h = 16 for i in range(0, 10, 3): if i == 9: dis.text(x, y, ' %s' % self.randomize[0]) else: dis.text(x, y, ' '.join(self.randomize[1+i:1+i+3])) y += h else: # just clear what we need to: the PIN area dis.clear_rect(0, 40, 88, 20) # placeholder text msg = '[' + ('*'*len(self.pin)) + ']' x = 40 - ((10*len(msg))//2) dis.text(x, 40, msg, FontLarge) dis.show()
def draw_busy(self, msg, percent): from display import FontTiny from main import dis self.last_percent = 0.5 # centered in bottom part of screen. y = 48 if percent is not None: self.percent = percent # reset display once we're at 100% if percent >= 0.995: # ~ last pixel self.percent = None self.busy_text = msg = None if msg is not None: self.busy_text = msg if self.busy_text is not None: # clear under it dis.clear_rect(0,y, 128, 64-y) dis.text(None, y, self.busy_text) if self.percent is not None: x = int(128 * self.percent) dis.dis.hline(0, 63, x, 1) dis.dis.hline(x+1, 63, 127, 0) dis.show()
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")
async def test_oled(): # all on/off tests for ph in (1, 0): dis.clear() dis.dis.fill(ph) dis.text(None,2, "Selftest", invert=ph) dis.text(None,30, "All on?" if ph else 'All off?', invert=ph, font=FontLarge) dis.show() ch = await ux_wait_keyup('yx') if ch != 'y': raise RuntimeError("OLED test aborted")
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 main import dis from display import FontTiny from math import log # allow key repeat on X only press = PressRelease('1234567890y') y = 26 value = '' max_w = int(log(max_value, 10) + 1) dis.clear() dis.text(0, 0, prompt) dis.text(None, -1, "X to DELETE, or OK when DONE.", FontTiny) dis.save() while 1: dis.restore() # 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.show() 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))
def show_pin(self, show_hint=False): filled = len(self.pin) if show_hint: filled -= 1 hint = self.pin[-1] if not version.has_membrane else None dis.clear() if not self.pin_prefix: prompt="Enter PIN Prefix" else: prompt="Enter rest of PIN" if self.subtitle: dis.text(None, 0, self.subtitle) dis.text(None, 16, prompt, FontTiny) else: dis.text(None, 4, prompt) y = 27 w = 18 x = 12 for idx in range(filled): dis.icon(x, y, 'xbox') x += w if show_hint: if not version.has_membrane: dis.text(x+1, y+1, hint, FontLarge) dis.icon(x, y, 'box') else: dis.icon(x, y, 'tbox') else: if len(self.pin) != MAX_PIN_PART_LEN: dis.icon(x, y, 'box') # BTW: √ also works here, but looks like square root, not a checkmark if self.footer: footer = self.footer elif self.is_repeat: footer = "CONFIRM PIN VALUE" elif not self.pin_prefix: footer = "X to CANCEL, or OK when DONE" else: footer = "X to CANCEL, or OK to CONTINUE" dis.text(None, -1, footer, FontTiny) dis.show()
def redraw(): dis.clear() w = 29 # because version=3 XO, YO = 7, 3 # offsets if not invert: dis.dis.fill_rect(XO - YO, 0, 64, 64, 1) for x in range(w): for y in range(w): px = data[x][y] X = (x * 2) + XO Y = (y * 2) + YO dis.dis.fill_rect(X, Y, 2, 2, px if invert else (not px)) x, y = 73, 0 if is_segwit else 2 ll = 7 # per line for i in range(0, len(addr), ll): dis.text(x, y, addr[i:i + ll], FontSmall) y += 10 if is_segwit else 12 if not invert: # show path number, very tiny ai = str(start_n + idx) if len(ai) == 1: dis.text(0, 30, ai[0], FontTiny) else: dis.text(0, 27, ai[0], FontTiny) dis.text(0, 27 + 7, ai[1], FontTiny) dis.busy_bar(False) # includes show
async def test_numpad(): # do an interactive self test keys = list('123456789x0y') for ch in keys: dis.clear() dis.text(0,0, "Numpad Test. Press:") dis.text(None,24, ch if ch != 'y' else 'OK', FontLarge) dis.show() k = await ux_wait_keyup(ch + 'x') if k == 'x' and ch != 'x': raise RuntimeError("numpad test aborted") assert k == ch
def show_pin(self, show_hint=False): filled = len(self.pin) if show_hint: filled -= 1 hint = self.pin[-1] else: hint = '' if len(self.pin) == MAX_PIN_PART_LEN else ' ' dis.clear() if not self.pin_prefix: prompt="Enter PIN Prefix" else: prompt="Enter rest of PIN" if self.subtitle: dis.text(None, 0, self.subtitle) dis.text(None, 16, prompt, FontTiny) else: dis.text(None, 4, prompt) y = 26 w = 18 if 0: x = 64 - ((w * (filled+(1 if hint else 0))) // 2) x += w//2 else: x = 12 for idx in range(filled): dis.icon(x, y, 'xbox') x += w if hint: dis.text(x+1, y+1, hint, FontLarge) dis.icon(x, y, 'box') # BTW: √ also works here, but looks like square root, not a checkmark if self.is_repeat: footer = "CONFIRM PIN VALUE" elif not self.pin_prefix: footer = "X to CANCEL, or OK when DONE." else: footer = "X to CANCEL, or OK to CONTINUE." dis.text(None, -1, footer, FontTiny) dis.show()
async def show_nickname(nick): # Show a nickname for this coldcard (as a personalization) # - no keys here, just show it until they press anything from main import dis from display import FontLarge, FontTiny, FontSmall from ux import ux_wait_keyup dis.clear() if dis.width(nick, FontLarge) <= dis.WIDTH: dis.text(None, 21, nick, font=FontLarge) else: dis.text(None, 27, nick, font=FontSmall) dis.show() await ux_wait_keyup()
def show(self): from main import dis, hsm_active # Plan: show "time til period reset", and some stats, # but never show amounts or private info. dis.dis.buffer[:] = self.screen_buf[:] left = hsm_active.get_time_left() if left is None: left = ' n/a' elif left == -1: left = ' --' else: left = period_display(left) # 3 statistics; see draw_background for X positions y = 28+1 for x, val in [ (14, str(hsm_active.approvals)), (51, str(hsm_active.refusals)), (98, left)]: tw = 7*len(val) # = dis.width(val, FontSmall) dis.text(x - tw//2, y, val) # heartbeat display if 1: #self.phase = (utime.ticks_ms() // 50) % len(cylon) self.phase = (self.phase + 1) % len(cylon) x = cylon[self.phase] w = 12 dis.dis.line(x, 63, x+w-1, 63, True) if self.digits: # UX "feedback" for digits if len(self.digits) < 6: msg = self.digits + ('#' * (6-len(self.digits))) elif self.digits: msg = self.digits # dis.width('######', FontSmall) == 42 x, y, w, h = 80, 0, 42, 14 dis.clear_rect(x,y, x+w, y+h) dis.text(x, y, msg) # contains a dis.show() self.draw_busy(None, None)
async def test_sflash(): dis.clear() dis.text(None, 18, 'Serial Flash') dis.show() #if ckcc.is_simulator(): return from main import sf from ustruct import pack import tcc msize = 1024 * 1024 sf.chip_erase() for phase in [0, 1]: steps = 7 * 4 for i in range(steps): dis.progress_bar(i / steps) dis.show() await sleep_ms(250) if not sf.is_busy(): break assert not sf.is_busy(), "sflash erase didn't finish" # leave chip blank if phase == 1: break buf = bytearray(32) for addr in range(0, msize, 1024): sf.read(addr, buf) assert set(buf) == {255}, "sflash not blank:" + repr(buf) rnd = tcc.sha256(pack('I', addr)).digest() sf.write(addr, rnd) sf.read(addr, buf) assert buf == rnd, "sflash write failed" dis.progress_bar_show(addr / msize) # check no aliasing, also right size part for addr in range(0, msize, 1024): expect = tcc.sha256(pack('I', addr)).digest() sf.read(addr, buf) assert buf == expect, "sflash readback failed" dis.progress_bar_show(addr / msize)
async def test_sd_active(): # Mark 2: SD Card active light. from machine import Pin led = Pin('SD_ACTIVE', Pin.OUT) for ph in range(2): gg = not ph led.value(gg) dis.clear() if gg: dis.text(0,16, "<-- Green ON?") else: dis.text(0,16, "<-- Green off?") dis.show() k = await ux_wait_keyup('xy') assert k == 'y' # "SD Active LED bust"
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 _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()
async def login_countdown(minutes): # show a countdown, which may need to # run for multiple **days** from main import dis from display import FontSmall, FontLarge sec = minutes * 60 while sec: dis.clear() y = 0 dis.text(None, y, 'Login countdown in', font=FontSmall); y += 14 dis.text(None, y, 'effect. Must wait:', font=FontSmall); y += 14 y += 5 dis.text(None, y, pretty_short_delay(sec), font=FontLarge) dis.show() dis.busy_bar(1) # XXX this should be more accurate, errors are accumulating await sleep_ms(1000) sec -= 1 dis.busy_bar(0)
def show_pin(self, force_draw=False): if self.randomize: return self.show_pin_randomized(force_draw) filled = len(self.pin) y = 27 if force_draw: dis.clear() if not self.pin_prefix: prompt="Enter PIN Prefix" else: prompt="Enter rest of PIN" if self.subtitle: dis.text(None, 0, self.subtitle) dis.text(None, 16, prompt, FontTiny) else: dis.text(None, 4, prompt) if self.footer: footer = self.footer elif self.is_repeat: footer = "CONFIRM PIN VALUE" elif not self.pin_prefix: footer = "X to CANCEL, or OK when DONE" else: footer = "X to CANCEL, or OK to CONTINUE" dis.text(None, -1, footer, FontTiny) else: # just clear what we need to: the PIN area dis.clear_rect(0, y, 128, 21) w = 18 # extra (empty) box after if not filled: dis.icon(64-(w//2), y, 'box') else: x = 64 - ((w*filled)//2) # filled boxes for idx in range(filled): dis.icon(x, y, 'xbox') x += w dis.show()
async def test_ae508a(): assert not get_is_bricked(), "AE508a is bricked" for ph in range(5): gg = get_genuine() dis.clear() if gg: dis.text(-1, 8, "Green ON? -->") else: dis.text(-1, 50, "Red ON? -->") dis.show() k = await ux_wait_keyup('xy') assert k == 'y', "LED bust" if ph and gg: # stop once it's on and we've tested both states return # attempt to switch to other state if gg: clear_genuine() else: # very slow! dis.text(0, 0, "Wait") dis.show() set_genuine() ux_clear_keys() ng = get_genuine() assert ng != gg, "Could not invert LED"
async def login_countdown(minutes): # show a countdown, which may need to # run for multiple **days** from main import dis from display import FontSmall, FontLarge sec = minutes * 60 while sec: dis.clear() y = 0 dis.text(None, y, 'Login countdown in', font=FontSmall) y += 14 dis.text(None, y, 'effect. Must wait:', font=FontSmall) y += 14 y += 5 if sec >= 3600: msg = '%2dh %2dm %2ds' % (sec // 3600, (sec // 60) % 60, sec % 60) else: msg = '%2dm %2ds' % ((sec // 60) % 60, sec % 60) dis.text(None, y, msg, font=FontLarge) dis.show() dis.busy_bar(1) await sleep_ms(1000) sec -= 1 dis.busy_bar(0)
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()
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_pin(self, show_hint=False): if self.randomize: return self.show_pin_randomized() filled = len(self.pin) if show_hint: filled -= 1 hint = None # used to be: self.pin[-1] (for Mk1) dis.clear() if not self.pin_prefix: prompt="Enter PIN Prefix" else: prompt="Enter rest of PIN" if self.subtitle: dis.text(None, 0, self.subtitle) dis.text(None, 16, prompt, FontTiny) else: dis.text(None, 4, prompt) y = 27 w = 18 x = 12 for idx in range(filled): dis.icon(x, y, 'xbox') x += w if show_hint: dis.icon(x, y, 'tbox') else: if len(self.pin) != MAX_PIN_PART_LEN: dis.icon(x, y, 'box') if self.footer: footer = self.footer elif self.is_repeat: footer = "CONFIRM PIN VALUE" elif not self.pin_prefix: footer = "X to CANCEL, or OK when DONE" else: footer = "X to CANCEL, or OK to CONTINUE" dis.text(None, -1, footer, FontTiny) dis.show()
def show_words(self, words, has_secondary=False): dis.clear() dis.text(None, 0, "Recognize these?" if (not self.is_setting) or self.is_repeat \ else "Write these down:") y = 15 x = 18 dis.text(x, y, words[0], FontLarge) dis.text(x, y+18, words[1], FontLarge) if not self.is_setting: dis.text(None, -1, "Press (2) for secondary wallet", FontTiny) dis.show()
def redraw(self): # Redraw screen. from main import dis from display import FontSmall, FontTiny # what we are showing inside the QR msg = self.addrs[self.idx] # make the QR, if needed. if not self.qr_data: dis.busy_bar(True) self.render_qr(msg) # draw display dis.clear() w = 29 # because version=3 XO, YO = 7, 3 # offsets if not self.invert: dis.dis.fill_rect(XO - YO, 0, 64, 64, 1) data = self.qr_data inv = self.invert for x in range(w): for y in range(w): px = data[x][y] X = (x * 2) + XO Y = (y * 2) + YO dis.dis.fill_rect(X, Y, 2, 2, px if inv else (not px)) x, y = 73, 0 if self.is_alnum else 2 sidebar, ll = self.sidebar or (msg, 7) for i in range(0, len(sidebar), ll): dis.text(x, y, sidebar[i:i + ll], FontSmall) y += 10 if self.is_alnum else 12 if not inv and len(self.addrs) > 1: # show path number, very tiny ai = str(self.start_n + self.idx) if len(ai) == 1: dis.text(0, 30, ai[0], FontTiny) else: dis.text(0, 27, ai[0], FontTiny) dis.text(0, 27 + 7, ai[1], FontTiny) dis.busy_bar(False) # includes 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:") if not self.is_setting: dis.text(None, -1, "Press (2) for secondary wallet", FontTiny) dis.show() 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) dis.show()
async def test_multipress(): dis.clear() dis.text(None, 10, 'Welcome', font=FontLarge) dis.show() while 1: pr = await numpad.get() dis.clear() dis.text(None, 20, 'Pressed', font=FontSmall) dis.text(None, 35, pr, font=FontLarge) dis.show()
async def add_numbers(self, *a): # collect a series of digits from main import dis from display import FontTiny, FontSmall global pp_sofar # allow key repeat on X only press = PressRelease('1234567890y') footer = "X to DELETE, or OK when DONE." lx = 6 y = 16 here = '' dis.clear() dis.text(None, -1, footer, FontTiny) dis.save() while 1: dis.restore() # text centered msg = here by = y bx = dis.text(lx, y, msg[0:16]) dis.text(lx, y - 9, str(pp_sofar, 'ascii').replace(' ', '_'), FontTiny) if len(msg) > 16: # second line when needed (left just) by += 15 bx = dis.text(lx, by, msg[16:]) if len(here) < 32: dis.icon(bx, by - 2, 'sm_box') dis.show() ch = await press.wait() if ch == 'y': pp_sofar += here self.check_length() return elif ch == 'x': if here: here = here[0:-1] else: # quit if they press X on empty screen return else: if len(here) < 32: here += ch
async def do_delay(self, pa): # show # of failures and implement the delay, which could be # very long. 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) while pa.is_delay_needed(): dis.progress_bar(pa.delay_achieved / pa.delay_required) dis.show() pa.delay()
async def test_secure_element(): assert not get_is_bricked() # bricked already # test right chips installed is_fat = ckcc.is_stm32l496() if is_fat: assert version.has_608 # expect 608a assert version.hw_label == 'mk3' else: assert not version.has_608 # expect 508a assert version.hw_label != 'mk3' if ckcc.is_simulator(): return for ph in range(5): gg = get_genuine() dis.clear() if gg: dis.text(-1, 8, "Green ON? -->") else: dis.text(-1,50, "Red ON? -->") dis.show() k = await ux_wait_keyup('xy') assert k == 'y' # "LED bust" if ph and gg: # stop once it's on and we've tested both states return # attempt to switch to other state if gg: clear_genuine() else: # very slow! dis.text(0,0, "Wait") dis.show() set_genuine() ux_clear_keys() ng = get_genuine() assert ng != gg # "Could not invert LED"
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()