# (c) Copyright 2021 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # qrs.py - QR Display related UX # import framebuf, uqr from ux import UserInteraction, ux_wait_keyup, the_ux from version import has_qwerty from exceptions import QRTooBigError from charcodes import (KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_NFC, KEY_END, KEY_ENTER, KEY_CANCEL) # TODO: This class has a terrible API! # Max in a V11 as bytes (not alnum) ... the limit on Mk4 screen MAX_V11_CHAR_LIMIT = const(321) class QRDisplaySingle(UserInteraction): # Show a single QR code for (typically) a list of addresses, or a single value. def __init__(self, addrs, is_alnum, start_n=0, sidebar=None, msg=None, is_addrs=False, force_msg=False, allow_nfc=True, is_secret=False, change_idxs=None, can_raise=True, qr_msgs=None, no_index=None): 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.sidebar = sidebar self.start_n = start_n self.is_addrs = is_addrs self.msg = msg self.qr_data = None self.force_msg = force_msg self.allow_nfc = allow_nfc # only used for NFC sharing secret material - full chip wipe if is_secret=True self.is_secret = is_secret self.change_idxs = change_idxs or [] self.can_raise = can_raise self.qr_msgs = qr_msgs self.no_index = no_index def calc_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 # - version=3 => to display 29x29 pixels, we have to double them up: 58x58 # - version=4..11 => single pixel per module # - not really providing enough space around these, shrug # - inverted QR (black/white swap) still readable by scanners, altho wrong # - on Q: ver 25 => 117x117 is largest that can be pixel-doubled # - on Q: v40 is possible at at 1:1, but most find that unreadable, so avoid 1:1 if self.is_alnum: # targeting 'alpha numeric' mode, nice and dense; caps only tho enc = uqr.Mode_ALPHANUMERIC if not msg.isdigit() else uqr.Mode_NUMERIC msg = msg.upper() else: # has to be 'binary' mode, altho shorter msg, typical 34-36 enc = uqr.Mode_BYTE # can fail if not enough space in QR self.qr_data = uqr.make(msg, min_version=2, max_version=11 if not has_qwerty else 25, encoding=enc) def idx_hint(self): # draw_qr_display takes this and renders hint in the top right corner # this member function decides what type of hint will be shown # numbers, letters, etc. if self.no_index: return None return str(self.start_n + self.idx) if len(self.addrs) > 1 else None def side_msg(self): if self.idx in self.change_idxs: return "CHANGE BACK" elif self.qr_msgs: try: return self.qr_msgs[self.idx] except IndexError: pass return None def redraw(self): # Redraw screen. from glob import dis dis.clear() # what we are showing inside the QR body = self.addrs[self.idx] idx_hint = self.idx_hint() msg = None if self.msg: msg = self.msg else: if isinstance(body, str): # sanity check msg = body # make the QR, if needed. if not self.qr_data: dis.busy_bar(True) try: self.calc_qr(body) except Exception: dis.busy_bar(False) if not self.can_raise: dis.draw_qr_error(idx_hint, msg) return # other code paths require raise to switch to BBQr raise QRTooBigError # draw display dis.busy_bar(False) dis.draw_qr_display(self.qr_data, msg, self.is_alnum, self.sidebar, idx_hint, self.invert, is_addr=self.is_addrs, force_msg=self.force_msg, side_msg=self.side_msg()) async def interact_bare(self): from glob import NFC, dis self.redraw() while 1: ch = await ux_wait_keyup(flush=True) was = self.idx if ch == '1' or ch == 'i': self.invert = not self.invert self.redraw() continue elif NFC and (ch == '3' or ch == KEY_NFC): if not self.allow_nfc: # not a valid as text over NFC sometimes; treat as cancel break else: # Share any QR over NFC! await NFC.share_text(self.addrs[self.idx], is_secret=self.is_secret) self.redraw() continue elif ch in 'xy'+KEY_ENTER+KEY_CANCEL: break elif len(self.addrs) == 1: continue elif ch in '57' + KEY_UP + KEY_LEFT: if self.idx > 0: self.idx -= 1 elif ch in '89' + KEY_DOWN + KEY_RIGHT: if self.idx != len(self.addrs)-1: self.idx += 1 elif ch == KEY_HOME: self.idx = 0 elif ch == KEY_END: self.idx = len(self.addrs)-1 else: continue if self.idx != was: # self.idx has changed, so need full re-render self.qr_data = None self.redraw() # bugfix if dis.has_lcd: dis.real_clear() async def interact(self): await self.interact_bare() the_ux.pop() class XORQRDisplaySingle(QRDisplaySingle): def idx_hint(self): if len(self.addrs) > 1: return chr(65+int(self.start_n + self.idx)) # EOF