firmware/shared/lcd_display.py
2026-06-19 10:56:45 -04:00

926 lines
30 KiB
Python

# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# lcd_display.py - LCD rendering for Q1's 320x240 pixel *colour* display!
#
import machine, uzlib, utime, array
from graphics_q1 import Graphics
from st7788 import ST7788
from utils import xfp2str, word_wrap, chunk_address
from ucollections import namedtuple
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS
# the one font: fixed-width (except for a few double-width chars)
from font_iosevka import CELL_W, CELL_H, TEXT_PALETTES, COL_TEXT, COL_DARK_TEXT, COL_SCROLL_DARK
from font_iosevka import FontIosevka
#WIDTH = const(320)
#HEIGHT = const(240)
LEFT_MARGIN = const(7) # equal on right side, but used for scroll bar
TOP_MARGIN = const(15)
PROGRESS_BAR_H = const(5)
ACTIVE_H = const(240 - TOP_MARGIN - PROGRESS_BAR_H)
CHARS_W = const(34)
CHARS_H = const(10)
# colouuurs: RGB565
COL_WHITE = 0xffff
COL_BLACK = 0x0000
COL_PROGRESS = COL_TEXT
# Attribute stored per-char; really just an index into TEXT_PALETTES
FLAG_INVERT = 0x10000
FLAG_DARK = 0x20000
ATTR_MASK = 0x30000
# use this to describe cursor you need.
# - outline leaves most of the cell unaffected (just 1px inside border)
# - solid/outline available in double-width as well
CURSOR_SOLID = 0x01
CURSOR_OUTLINE = 0x02
CURSOR_MENU = 0x03
CURSOR_DW_OUTLINE = 0x11
CURSOR_DW_SOLID = 0x12
CursorSpec = namedtuple('CursorSpec', 'x y cur_type')
CURSOR_DW_Mask = 0x10
def grey_level(amt):
# give percent 0..1.0
r = int(amt * 0x1f)
g = int(amt * 0x3f)
#b = int(amt * 0x1f) # same as Red
return (r<<11) | (g << 5) | r
def rgb(r,g,b):
# as if 24-bit, but we're 16
r = int(r/255 * 0x1f)
g = int(g/255 * 0x3f)
b = int(b/255 * 0x1f)
return (r<<11) | (g << 5) | b
def get_sys_status():
# Read current values for all status-bar items
# - normally we update as we go along.
# - return a dict
from battery import get_batt_threshold
rv = dict(shift=0, caps=0, symbol=0)
rv['bat'] = get_batt_threshold()
from stash import bip39_passphrase
rv['bip39'] = int(bool(bip39_passphrase))
from pincodes import pa
rv['tmp'] = int(bool(pa.tmp_value))
from glob import settings
if settings:
rv['xfp'] = settings.get('xfp')
from version import is_edge, is_devmode
if is_edge:
rv['edge'] = 1
elif is_devmode:
rv['devmode'] = 1
return rv
class Display:
# TODO: move to global, but rest of system looks at these member vars
WIDTH = 320
HEIGHT = 240
# use these negative X values for auto layout features
CENTER = -2
RJUST = -1
# use this to know if on Q1 or earlier
has_lcd = True
# icon names and their values (0 / 1)
status_icons = {}
def __init__(self):
self.dis = ST7788()
from gpu import GPUAccess
self.gpu = GPUAccess()
try:
self.gpu.upgrade_if_needed()
except:
print("GPU upgrade failed")
self.last_buf = self.make_buf(0)
self.next_buf = self.make_buf(32)
# state of progress bar (bottom edge)
self.last_prog = (-1, -1)
self.next_prog = (0, -1)
# state of scroll bar (right side)
self.last_scroll = 0.0
self.next_scroll = None
self.last_bar_update = 0
#self.dis.fill_screen() # defer a bit
self.draw_status(full=True)
def make_buf(self, ch=32):
# make a screen-state storage buffer. One spot per character, but needs to
# store attributes as well as support 16-bit unicode
return [array.array('I', (ch for i in range(CHARS_W))) for y in range(CHARS_H)]
def redraw_metakeys(self, new_state):
# called when metakeys have changed state
self.draw_status(**new_state)
async def async_draw_status(self, **kws):
self.draw_status(**kws)
def set_lcd_brightness(self, on_battery=None, tmp_override=None):
# Call when battery changes state, or if you want max for a bit (QR display)
# - call w/o args to get back to state we're supposed to be in.
from glob import settings
from battery import get_batt_threshold, DEFAULT_BATT_BRIGHTNESS
if tmp_override is not None:
self.dis.backlight.intensity(tmp_override)
return
# otherwise: respect setting
if on_battery is None:
on_battery = (get_batt_threshold() is not None)
if on_battery:
# user-defined brightness when running on batteries.
lvl = DEFAULT_BATT_BRIGHTNESS
if settings:
lvl = settings.get('bright', DEFAULT_BATT_BRIGHTNESS)
self.dis.backlight.intensity(lvl)
else:
# full brightness when on VBUS and when showing QR's
self.dis.backlight.intensity(255)
def draw_status(self, full=False, **kws):
animating = self.gpu.take_spi()
if full:
self.dis.fill_rect(0, 0, WIDTH, TOP_MARGIN-1, 0x0)
self.dis.fill_rect(0, TOP_MARGIN-1, WIDTH, 1, grey_level(0.25))
kws = get_sys_status()
b_x = 292
if 'bat' in kws:
if kws['bat'] is None:
self.image(b_x, 0, 'plugged')
self.set_lcd_brightness(False)
else:
self.image(b_x, 0, 'bat_%d' % kws['bat'])
self.set_lcd_brightness(True)
if 'bip39' in kws:
self.image(102, 0, 'bip39_%d' % kws['bip39'])
if 'tmp' in kws:
self.image(165, 0, 'tmp_%d' % kws['tmp'])
xfp = kws.get('xfp', None) # expects an integer
if xfp is not None:
x = 217
for ch in xfp2str(xfp).lower():
self.image(x, 0, 'ch_'+ch)
x += 6
x = 265
if 'edge' in kws:
self.image(x, 0, 'edge')
elif 'devmode' in kws:
self.image(x+5, 0, 'devmode')
x = 8
for dx, meta in [(5, 'shift'), (39, 'symbol'), (61, 'caps')]:
if meta in kws:
self.image(x+dx, 0, '%s_%d' % (meta, kws[meta]))
if animating:
self.gpu.give_spi()
def image(self, x, y, name):
# display a graphics image, immediately
w,h, data = getattr(Graphics, name)
if x is None:
x = max(0, (WIDTH - w) // 2)
self.gpu.take_spi()
self.dis.show_zpixels(x, y, w, h, data)
self.mark_correct(x, y, w, h)
self.show()
def mark_correct(self, px, py, w, h):
# mark a subset of the screen as already drawn correctly
# - because we drew an image in that spot already (immediate)
# - hard: need to convert from pixel coord space to chars
if py < TOP_MARGIN:
# status icons not a concern
return
cy = (py - TOP_MARGIN) // CELL_H
cx = (px - LEFT_MARGIN) // CELL_W
cw = (w+CELL_W) // CELL_W
ch = (h+CELL_H) // CELL_H
#print('pixel %dx%d @ (%d,%d) => %dx%d @ (%d,%d)' % (w, h, px,py, cw, ch, cx,cy))
for y in range(cy, cy+ch):
for x in range(cx, cx+cw+1):
try:
self.last_buf[y][x] = self.next_buf[y][x] = 0xfffe
except IndexError:
pass
def icon(self, x, y, name, invert=0):
# plan is these are chars or images
raise NotImplementedError
def width(self, msg):
# length of text msg in char cells
# - typically 1:1 but we have a few double-width chars
rv = len(msg)
rv += sum(1 for ch in msg if ch in FontIosevka.DOUBLE_WIDE)
return rv
def text(self, x,y, msg, font=None, invert=False, dark=False):
# Draw at x,y (in cell positions, not pixels)
# - use invert=1 to get reverse video
# - returns ending X position, where you might want a cursor after
end_x = None
# encode text attribute for this part
attr = 0
if invert:
attr = FLAG_INVERT
if dark:
attr = FLAG_DARK
if x is None or x < 0:
w = self.width(msg)
if x is None:
# center: also blanks rest of line
x = max(0, (CHARS_W - w) // 2)
end_x = x + w
msg = ((' '*x) + msg + (' ' * CHARS_W))[0:CHARS_W]
x = 0
else:
# measure from right edge (right justify)
x = max(0, CHARS_W - w + 1 + x)
end_x = x + w
if y < 0:
# measure up from bottom edge
y = CHARS_H + y
if y >= CHARS_H:
#print("BAD Draw '%s' at y=%d" % (msg, y))
return # past bottom
for ch in msg:
if x >= CHARS_W: break
self.next_buf[y][x] = ord(ch) + attr
x += 1
if ch in FontIosevka.DOUBLE_WIDE:
if x >= CHARS_W-1:
# trying to draw double-wide in last (half) of right edge: avoid
#self.next_buf[y][x-1] = 32
continue
self.next_buf[y][x] = 0
x += 1
return end_x if end_x is not None else x
def real_clear(self, _internal=False):
# fill to black, but only text area, not status bar
if not _internal:
self.gpu.take_spi()
self.dis.fill_rect(0, TOP_MARGIN, WIDTH, HEIGHT-TOP_MARGIN, 0x0)
self.last_buf = self.make_buf(32)
self.next_buf = self.make_buf(32)
self.next_prog = (0, -1)
self.next_scroll = None
def clear(self):
# clear text
self.next_buf = self.make_buf(32)
# clear progress bar & scroll bar
self.next_prog = (0, -1)
self.next_scroll = None
def show(self, just_lines=None, cursor=None, max_bright=False):
# Push internal screen representation to device, effeciently
self.gpu.take_spi()
if hasattr(self, '_full_redraw'):
# redraw every pixel, assume nothing about display contents
del self._full_redraw
self.dis.fill_rect(0, TOP_MARGIN, WIDTH, HEIGHT-TOP_MARGIN, 0x0)
self.draw_status(full=True)
self.last_buf = self.make_buf(0xfff0)
lines = just_lines or range(CHARS_H)
for y in lines:
py = TOP_MARGIN + (y * CELL_H)
x = 0
while x < CHARS_W:
if self.next_buf[y][x] == self.last_buf[y][x]:
# already correct
x += 1
continue
px = LEFT_MARGIN + (x * CELL_W)
ch = chr(self.next_buf[y][x] & ~ATTR_MASK)
attr = (self.next_buf[y][x] & ATTR_MASK)
if ch == ' ':
# space - look for horz runs & fill w/ blank
run = 1
here = self.next_buf[y][x]
for x2 in range(x+1, CHARS_W):
if self.next_buf[y][x2] != here:
break
run += 1
self.dis.fill_rect(px, py, run*CELL_W, CELL_H,
COL_TEXT if attr == FLAG_INVERT else 0)
x += run
continue
fn = FontIosevka.lookup(ch)
if not fn:
# unknown char
x += 1
continue
self.dis.show_pal_pixels(px, py, fn.w, fn.h, TEXT_PALETTES[attr >> 16], fn.bits)
x += fn.w // CELL_W
self.last_buf[y][:] = self.next_buf[y]
# maybe update progress bar
if self.next_prog != self.last_prog:
self._draw_progress_bar()
# maybe update right hand scroll bar
if self.next_scroll != self.last_scroll:
self._draw_scroll_bar(self.next_scroll)
self.last_scroll = self.next_scroll
# implement cursor based on CursorSpec values
if cursor:
assert 0 <= cursor.x < CHARS_W, 'cur x'
assert 0 <= cursor.y < CHARS_H, 'cur y'
self.gpu.cursor_at(cursor.x, cursor.y, cursor.cur_type)
self.last_buf[cursor.y][cursor.x] = 0xfffd
if (cursor.cur_type & CURSOR_DW_Mask) and (cursor.x < CHARS_W-1):
self.last_buf[cursor.y][cursor.x+1] = 0xfffd
# modulate the LCD brightness if we're showing QR or something
if max_bright:
self.set_lcd_brightness(tmp_override=255)
self._max_bright = True
elif hasattr(self, '_max_bright'):
self.set_lcd_brightness() # back to normal
del self._max_bright
# When drawing another screen for a bit, then coming back, use these
def save_state(self):
# should really be a dataclass w/ all our state details
return ([array.array('I', ln) for ln in self.last_buf],
self.last_prog, self.last_scroll)
def restore_state(self, old_state):
rows, self.next_prog, self.next_scroll = old_state
for y in range(CHARS_H):
self.next_buf[y][:] = rows[y]
self.show()
# obsolete OLED approach
def save(self):
raise NotImplementedError
def restore(self):
raise NotImplementedError
def hline(self, y):
# used only in hsm_ux.py
#self.dis.fill_rect(0,y, WIDTH, 1, 0xffff)
pass
def vline(self, x):
# used only in hsm_ux.py
#self.dis.fill_rect(x,TOP_MARGIN, 1, ACTIVE_H, 0xffff)
pass
def clear_rect(self, x,y, w,h):
# but see clear_box() instead
raise NotImplementedError
def scroll_bar(self, offset, count, per_page=CHARS_H):
# next show(), we will draw a scroll bar on right edge
assert count >= 1
self.next_scroll = (offset, count, per_page)
def _draw_progress_bar(self):
# show the "part 1 of 10" style bar used for BBQr
h = PROGRESS_BAR_H # NOTE: misc/gpu/lcd.c will need update if H changes
w, x = self.next_prog
if x == -1:
# normal old-style progress bar
lw, lx = self.last_prog
if lw <= w and lx == -1:
# no need to erase, we can just draw on top
pass
else:
# erase under / clear it
self.dis.fill_rect(0, HEIGHT-h, WIDTH, h, COL_BLACK)
if w:
self.dis.fill_rect(0, HEIGHT-h, w, h, COL_PROGRESS)
else:
# draw new bar
self.dis.fill_rect(0, HEIGHT-h, WIDTH, h, COL_DARK_TEXT)
self.dis.fill_rect(x, HEIGHT-h, w, h, COL_PROGRESS)
self.last_prog = self.next_prog
def _draw_scroll_bar(self, values):
# Immediately draw bar along right edge.
bw = 5 # bar width
if values is None:
# clear old display
self.dis.fill_rect(WIDTH-bw, TOP_MARGIN, bw, ACTIVE_H, COL_BLACK)
return
offset, count, per_page = values
assert 0 <= offset <= count, (offset, count, per_page)
num_pages = max(count / per_page, 2)
bh = max(int(ACTIVE_H / num_pages), 4)
pos = int((ACTIVE_H - bh) * (offset / count))
# "round up" the final page so touches bottom always
is_last = offset and (offset + per_page >= count)
if is_last:
pos = ACTIVE_H - bh
self.dis.fill_rect(WIDTH-bw, TOP_MARGIN, bw, ACTIVE_H, COL_SCROLL_DARK)
self.dis.fill_rect(WIDTH-bw, TOP_MARGIN+pos, bw, bh, COL_TEXT)
def fullscreen(self, msg, percent=None, line2=None):
# show a simple message "fullscreen".
self.clear()
y = CHARS_H // 3
self.text(None, y, msg)
if line2:
y += 2
for ln in word_wrap(line2, CHARS_W):
self.text(None, y, ln, dark=True)
y += 1
if percent is not None:
self.progress_bar(percent)
self.show()
def splash(self):
# display a splash screen with some version numbers
self.real_clear()
y = 6
self.image(None, 90, 'splash')
self.text(None, y, "Don't Trust. Verify.")
from version import get_mpy_version
timestamp, label, *_ = get_mpy_version()
self.text(0, -1, 'Version '+label)
self.text(-1, -1, timestamp)
self.show([y, CHARS_H-1])
def progress_bar(self, percent):
# Horizontal progress bar
# takes 0.0 .. 1.0 as fraction of doneness
percent = max(0, min(1.0, percent))
self.next_prog = (int(WIDTH * percent), -1)
def progress_part_bar(self, n_of_m):
# for BBQr: a part of a bar (segment N of M parts)
n, m = n_of_m
assert n <= m
if m <= 1:
# turn off bar segment display if one or none of them
self.next_prog = (0, -1)
else:
w = WIDTH // m
if n == m-1:
# be sure last bar touchs right edge
self.next_prog = (w, WIDTH-w)
else:
self.next_prog = (w, (n * w))
def progress_sofar(self, done, total):
# Update progress bar, but only if it's been a while since last update
if utime.ticks_diff(utime.ticks_ms(), self.last_bar_update) < 100:
return
self.last_bar_update = utime.ticks_ms()
self.progress_bar_show(done / total)
def progress_bar_show(self, percent):
# useful as a callback
self.progress_bar(percent)
self.show()
def busy_bar(self, enable):
# activate the GPU to render/animate this.
# - show() in this funct is relied-upon by callers
if enable:
self.last_prog = (-1, -1)
self.show()
self.gpu.busy_bar(True)
else:
# - self.show will stop animation
# - and redraw w/ no bar visible
self.last_prog = (WIDTH, -1)
self.next_prog = (0, -1)
self.show()
def set_brightness(self, val):
# - was only used by HSM ux code
# - QR code display brightness is done in show_qr_data() now
# - see self.set_lcd_brightness()
return
def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
# draw a menu item, perhaps selected, checked.
assert CHARS_W == 34
if ry >= CHARS_H:
# higher layer tries to draw partial line past bottom, and that's
# ok because the mk4 had a 5th, half-line as a hint
return
if msg[0] == ' ' and space_indicators:
# unused, but might need?
msg = '' + msg[1:]
x = 0
self.text(x, ry, ' '+msg+' ', invert=is_sel)
if is_checked:
self.text(len(msg)+2, ry, '')
def menu_show(self, cursor_y):
cs = CursorSpec(0, cursor_y or 0, CURSOR_MENU)
self.show(cursor=cs)
def show_yikes(self, lines):
# dump a stack trace
# - intended for photos, sent to support!
self.clear()
self.text(None, 0, '>>>> Yikes!! <<<<')
y = 1
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 ', ' ')
if ln[0] == '/':
ln = ln.split('/')[-1]
for second, l in enumerate(word_wrap(ln, CHARS_W)):
self.text(1 if second else 0, y, l)
y += 1
self.show()
def draw_story(self, lines, top, num_lines, is_sensitive, hint_icons=''):
self.clear()
y=0
prev_x = None
for ln in lines:
if ln == 'EOT':
self.text(0, y, ''*CHARS_W, dark=True)
continue
elif ln and ln[0] == OUT_CTRL_TITLE:
# title ... but we have no special font? Inverse!
self.text(0, y, ' '+ln[1:]+' ', invert=True)
if hint_icons:
# hint_icons not shown if is story without title
# maybe show that [QR,NFC] can do something
self.text(-1, y, hint_icons, dark=True)
elif ln and ln[0] == OUT_CTRL_ADDRESS:
# we can assume this will be a single line for our display
# thanks to code in utils.word_wrap
prev_x = self._draw_addr(y, ln[1:], prev_x=prev_x)
else:
self.text(0, y, ln)
prev_x = None
y += 1
self.scroll_bar(top, num_lines, CHARS_H)
self.show()
def _draw_addr(self, y, addr, prev_x=None):
# Draw a single-line of an address
# - use prev_x=0 to start centered
if prev_x is None:
# left justify (for stories)
prev_x = x = 1
elif prev_x == 0:
# center first line, following line(s) will be left-justified to match that
prev_x = x = max(((CHARS_W - (len(addr) * 5) // 4) // 2), 0)
else:
x = prev_x
self.text(x, y, ' '+' '.join(chunk_address(addr))+' ', invert=True)
return prev_x
@staticmethod
def handle_qr_msg(msg, max_lines=False):
if len(msg) <= CHARS_W:
parts = [msg]
elif ' ' not in msg and (len(msg) <= (CHARS_W * 2)):
# fits in two lines, but has no spaces
hh = len(msg) // 2
parts = [msg[0:hh], msg[hh:]]
else:
if not max_lines:
# do word wrap
parts = list(word_wrap(msg, CHARS_W))
else:
# 2 lines max
parts = [msg[:30] + "", "" + msg[-30:]]
return parts
def draw_qr_lines(self, lines, is_addr):
y = CHARS_H - len(lines)
prev_x = 0
for line in lines:
if not is_addr:
self.text(None, y, line)
else:
prev_x = self._draw_addr(y, line, prev_x=prev_x)
y += 1
def draw_qr_idx_hint(self, str_idx):
lh = len(str_idx)
assert lh <= 10
if lh > 5:
# needs 2 lines
self.text(-1, 0, str_idx[:5])
self.text(-1, 1, str_idx[5:])
else:
self.text(-1, 0, str_idx)
def draw_side_msg(self, msg, has_idx):
right_sub = 2 if has_idx else 0
start_right = right_msg = None
if len(msg) <= CHARS_H:
# we only need left side
start_left = CHARS_H - len(msg)
left_msg = msg
else:
split_msg = msg.split()
if len(split_msg) == 1 or len(split_msg) > 2:
return # not possible
left_msg, right_msg = split_msg
if len(left_msg) > CHARS_H:
return
if len(right_msg) > (CHARS_H - right_sub):
return
start_left = CHARS_H - len(left_msg)
start_right = CHARS_H - len(right_msg)
for i, c in enumerate(left_msg, start=start_left):
self.text(1, i, c)
if start_right:
for i, c in enumerate(right_msg, start=start_right):
self.text(-1, i, c)
def draw_qr_error(self, idx_hint, msg=None):
x = 85
y = 30
w = 150
self.clear()
self.dis.fill_rect(x, y, w, w, COL_TEXT)
self.dis.fill_rect(x + 1, y + 1, w - 2, w - 2) # Black
self.text(12, 3, "QR too big")
if msg:
lines = self.handle_qr_msg(msg, max_lines=True)
self.draw_qr_lines(lines, False)
if idx_hint:
self.draw_qr_idx_hint(idx_hint)
self.show()
def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert, partial_bar=None,
is_addr=False, force_msg=False, side_msg=None):
# Show a QR code on screen w/ some text under it
# - invert not supported on Q1
# - sidebar not supported here (see users.py)
assert not sidebar
# maybe show something other than QR contents under it
if is_addr:
# With fancy display, no address, even classic can fit in single line,
# so always split nicely in middle and at mod4
hh = len(msg) // 2
if hh <= 20:
hh = (hh + 3) & ~0x3
parts = [msg[0:hh], msg[hh:]]
num_lines = 2
else:
# p2wsh address would need 3 lines to show, so we won't
num_lines = 0
elif msg:
parts = self.handle_qr_msg(msg)
num_lines = len(parts)
else:
num_lines = 0
if num_lines > 2:
# show no text if it would be too big (example: 18, 24 seed words)
num_lines = 0
del parts
# send packed pixel data to C level to decode and expand onto LCD
# - 8-bit aligned rows of data
scan_w, w, data = qr_data.packed() if hasattr(qr_data, 'packed') else qr_data
# always draw as large as possible (vertical is limit)
# - even if that's a bit more than 240 and might even cover status bar
# - see bbqr.CHARS_PER_VERSION for specific versions to support
expand = max(1, (ACTIVE_H - (num_lines * CELL_H)) // (w+2))
fullscreen = False
trim_lines = 0
# always try to show the biggest possible QR code if not force_msg
if not force_msg:
if num_lines:
# better with text dropped?
e2 = max(1, ACTIVE_H // (w + 2))
if e2 > expand:
num_lines = 0
expand = e2
# fullscreen ?
e3 = (ACTIVE_H + 20) // (w + 2)
if expand < e3:
expand = e3
fullscreen = True
num_lines = 0
# vert center in available space
qw = (w+2) * expand
if fullscreen:
usable = HEIGHT - PROGRESS_BAR_H
if qw > usable:
# we will skip displaying some interior lines; slightly squishing it
y = 0
trim_lines = qw - usable
else:
y = max(0, (usable - (num_lines * CELL_H) - qw) // 2)
if not hasattr(self, '_full_redraw'):
# blank status bar area, but only first time thru
self.dis.fill_rect(0, 0, WIDTH, TOP_MARGIN, 0x0)
else:
y = TOP_MARGIN + max(0, (ACTIVE_H - (num_lines * CELL_H) - qw) // 2)
# horz center - easy
x = (WIDTH - qw) // 2
self.clear()
self.dis.show_qr_data(x, y, w, expand, scan_w, data, trim_lines)
if partial_bar is not None:
self.progress_part_bar(partial_bar)
if not fullscreen:
self.mark_correct(x, y, qw, qw)
if num_lines:
# centered text under that
self.draw_qr_lines(parts, is_addr)
if idx_hint:
self.draw_qr_idx_hint(idx_hint)
if side_msg:
self.draw_side_msg(side_msg, idx_hint)
# pass a max brightness flag here, which will be cleared after next show
self.show(max_bright=True)
else:
# not much left to draw: just the progress bar
self._draw_progress_bar()
self.set_lcd_brightness(tmp_override=255)
self._max_bright = True
self._full_redraw = True
def draw_bbqr_progress(self, hdr, got_parts, corrupt=False):
# we've seen at least one BBQr QR, so update display w/ progress bar
# - lots of data so we can show nice animation
# - hdr:BBQrHeader instance
count = len(got_parts)
if 2 <= hdr.num_parts < (CHARS_W // 2):
# if not too many parts, show - or 3 as they arrive
pat = []
for i in range(hdr.num_parts):
if i in got_parts:
pat.append(str(i+1))
else:
wl = 1 if i < 9 else 2
if corrupt and i == hdr.which:
pat.append('X'*wl)
else:
pat.append('-'*wl)
pat = (' ' if hdr.num_parts <= 8 else ' ').join(pat)
if len(pat) > CHARS_W:
pat = ''
else:
pat = '' # clear line
if count == hdr.num_parts and count == 1:
# skip the BS, it's a simple one
self.progress_bar_show(1)
return
self.text(None, -3, pat)
self.text(None, -2, 'Keep scanning more...' if count < hdr.num_parts else 'Got all parts!')
self.text(None, -1, '%s: %d of %d parts' % (hdr.file_label(), count, hdr.num_parts),
dark=True)
percent = count / hdr.num_parts
self.progress_bar(percent)
self.show()
def draw_box(self, x, y, w, h, **kw):
# using line-drawing chars, draw a box
# returns X pos of first inside char
assert 0 <= h <= CHARS_H-2 # 8 max
assert 0 <= w <= CHARS_W-2 # 32 max
if x is None:
x = (CHARS_W - w - 2) // 2
ln = '' + (''*w) + ''
self.text(x, y, ln, **kw)
for yy in range(y+1, y+h+1):
self.text(x, yy, '', **kw)
self.text(x+w+1, yy, '', **kw)
ln = '' + (''*w) + ''
self.text(x, y+h+1, ln, **kw)
return x+1
def clear_box(self, x, y, w, h, fill=32):
# clear (w/ spaces) a box on screen
for Y in range(y, y+h):
for X in range(x, x+w):
assert 0 <= X < CHARS_W, X
assert 0 <= Y < CHARS_H, Y
self.next_buf[Y][X] = fill
def bootrom_takeover(self):
# we are going to go into the bootrom and have it do stuff on the
# screen... we need to redraw completely on return
self.gpu.take_spi() # blocks until xfer complete
self.last_buf = self.make_buf(0)
self.last_prog = (-1, -1)
# here for mpy reasons
WIDTH = const(320)
HEIGHT = const(240)
# EOF