179 lines
6.2 KiB
Python
179 lines
6.2 KiB
Python
# (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 <https://www.qrcode.com/en/about/version.html>
|
|
# - 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
|