def draw_busy(self, msg, percent): from display import FontTiny from glob 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 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 glob 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))
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_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
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")
def show(self): from glob 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_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"
async def test_sflash(): dis.clear() dis.text(None, 18, 'Serial Flash') dis.show() from sflash import SF from ustruct import pack import ngu 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() # "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} # "not blank" rnd = ngu.hash.sha256s(pack('I', addr)) SF.write(addr, rnd) SF.read(addr, buf) assert buf == rnd # "write failed" dis.progress_bar_show(addr / msize) # check no aliasing, also right size part for addr in range(0, msize, 1024): expect = ngu.hash.sha256s(pack('I', addr)) SF.read(addr, buf) assert buf == expect # "readback failed" dis.progress_bar_show(addr / msize)
def show(self): # # Redraw the menu. # from glob import dis 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 show_fatal_error(msg): # show a multi-line error message, over some kinda "fatal" banner from glob 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 glob 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 redraw(self): # Redraw screen. from glob 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) inv = self.invert for x in range(w): for y in range(w): px = self.qr_data.get(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
async def add_numbers(self, *a): # collect a series of digits from glob 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 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 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 glob import dis, numpad from display import FontLarge 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)
async def spinner_edit(pw, confirm_exit=True): # Allow them to pick each digit using "D-pad" from glob 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 # pre-render the fixed stuff dis.clear() dis.text(None, -10, footer1, FontTiny) dis.text(None, -1, footer2, FontTiny) dis.save() # no key-repeat on certain keys press = PressRelease('4xy') while 1: dis.restore() 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) 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: if confirm_exit: 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 help_msg = '''\ 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 Add more characters by moving past end (right side).''' if confirm_exit: help_msg += '\nTo quit without changes, delete everything.' await ux_show_story(help_msg)
async def add_dice_rolls(count, seed, judge_them): from glob import dis from display import FontTiny, FontLarge md = sha256(seed) pr = PressRelease() # fixed parts of screen dis.clear() y = 38 dis.text(0, y, "Press 1-6 for each dice"); y += 13 dis.text(0, y, "roll to mix in.") dis.save() while 1: # Note: cannot scroll this msg because 5=up arrow dis.restore() dis.text(None, 0, '%d rolls' % count, FontLarge) hx = str(b2a_hex(md.digest()), 'ascii') dis.text(0, 20, hx[0:32], FontTiny) dis.text(0, 20+7, hx[32:], FontTiny) dis.show() ch = await pr.wait() if ch in '123456': count += 1 dis.restore() dis.text(None, 0, '%d rolls' % count, FontLarge) dis.show() # this is slow enough to see md.update(ch) elif ch == 'x': # Because the change (roll) has already been applied, # only let them abort if it's early still if count < 10 and judge_them: return 0, seed elif ch == 'y': if count < 99 and judge_them: if not count: return 0, seed ok = await ux_confirm('''\ You only provided %d dice rolls, and each roll adds only 2.585 bits of entropy. \ For 128-bit security, which is considered the minimum, you need 50 rolls, and \ for 256-bits of security, 99 rolls.''' % count) if not ok: continue break if count: seed = md.digest() return count, seed
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)