diff --git a/shared/actions.py b/shared/actions.py index 141a8e9e..66675041 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -2098,9 +2098,11 @@ Secure Elements: msg = msg.format(rel=rel, built=built, bl=bl, chk=chk, se=se, ser=serial, hw=hw) if version.has_qr: - from glob import SCAN + from glob import SCAN, dis msg += '\nQR Scanner:\n %s\n' % (SCAN.version or 'missing') + msg += '\nGPU:\n %s\n' % dis.gpu.get_version() + await ux_show_story(msg) async def ship_wo_bag(*a): diff --git a/shared/gpu.py b/shared/gpu.py index f7c90ad9..c7d41bb1 100644 --- a/shared/gpu.py +++ b/shared/gpu.py @@ -4,12 +4,13 @@ # # - see notes in misc/gpu/README.md # - bl = Bootloader, provided by ST Micro in ROM of chip -# - useful: import gpu; g=gpu.GPUAccess(); g.enter_bl() +# - errors are suppressed so we can boot w/o GPU loaded (factory) # import utime, struct import uasyncio as asyncio from utils import B2A from machine import Pin +from ustruct import pack # boot loader ROM response to this I2C address BL_ADDR = const(0x64) @@ -42,7 +43,8 @@ class GPUAccess: from machine import I2C self.i2c = I2C(1, freq=400000) # same bus & speed as nfc.py - # let the GPU run + # let the GPU run, but we have SPI for now + self.g_ctrl(1) self.g_reset(1) def bl_cmd_read(self, cmd, expect_len, addr=None, arg2=None, no_final=False): @@ -192,8 +194,9 @@ class GPUAccess: # "Go command" - starts code, but wants a reset vector really (stack+PC values) self.bl_cmd_read(0x21, 0, addr=addr) - def cmd_resp(self, cmd_args, expect_len): + def cmd_resp(self, cmd_args, expect_len=0): # send a command and read response back from our code running on GPU + # - will fail w/ OSError: ENODEV if i2c device (GPU) doesn't respond self.i2c.writeto(GPU_ADDR, cmd_args) return self.i2c.readfrom(GPU_ADDR, expect_len) @@ -217,41 +220,76 @@ class GPUAccess: return 'BL' # ready to load via BL return resp[0:resp.index(b'\0')].decode() - def spi(self, ours=False): + def take_spi(self): # change the MOSI/SCLK lines to be input so we don't interfere # with the GPU.. other lines are OD # - signal by G_CTRL that GPU can take over - if ours: - self.g_ctrl(1) - self.mosi_pin.init(mode=Pin.ALT, pull=Pin.PULL_DOWN, af=Pin.AF5_SPI1) - self.sclk_pin.init(mode=Pin.ALT, pull=Pin.PULL_DOWN, af=Pin.AF5_SPI1) + if self.g_ctrl() == 1: return + self.g_ctrl(1) + self.mosi_pin.init(mode=Pin.ALT, pull=Pin.PULL_DOWN, af=Pin.AF5_SPI1) + self.sclk_pin.init(mode=Pin.ALT, pull=Pin.PULL_DOWN, af=Pin.AF5_SPI1) + + def give_spi(self): + self.mosi_pin.init(mode=Pin.IN) + self.sclk_pin.init(mode=Pin.IN) + self.g_ctrl(0) + + def have_spi(self): + # do we control the display? + return self.g_ctrl() == 1 + + def busy_bar(self, enable): + if enable: + # start the bar + try: + self.cmd_resp(b'a') + except: pass + self.give_spi() else: - self.mosi_pin.init(mode=Pin.IN) - self.sclk_pin.init(mode=Pin.IN) - self.g_ctrl(0) + # stop showing it + self.take_spi() + + def cursor_off(self): + # stop showing the cursor + self.take_spi() + try: + self.cmd_resp(b'a') + except: pass + + def cursor_at(self, x, y, dbl_wide=False, outline=False): + # use outline to leave most of the cell unaffects (just 1px inside border) + cmd = b'c' + bytes([x, y, int(not outline), int(dbl_wide)]) + try: + self.cmd_resp(cmd) + except: pass + self.give_spi() def upgrade(self): # do in-circuit programming of GPU chip import gpu_binary + # get into bootloader if self.get_version() != 'BL': self.goto_bootloader() assert self.get_version() == 'BL' + # wipe old program ok = self.bulk_erase() assert ok, 'bulk erase fail' + # write block by block, but skip first part, so we can handle powerfail w/o brick for pos in range(256, gpu_binary.LENGTH, 256): - self.write_at(FLASH_START+pos, gpu_binary.BINARY[pos:pos+256]) + ok = self.write_at(FLASH_START+pos, gpu_binary.BINARY[pos:pos+256]) + assert ok - # last! the first part + # finally, the first part, which commits us to running this code on reset self.write_at(FLASH_START, gpu_binary.BINARY[0:256]) self.run_at(FLASH_START) utime.sleep_ms(50) v = self.get_version() - assert v== gpu_binary.VERSION + assert v == gpu_binary.VERSION return v diff --git a/shared/lcd_display.py b/shared/lcd_display.py index 76a002bf..7152ab97 100644 --- a/shared/lcd_display.py +++ b/shared/lcd_display.py @@ -11,6 +11,7 @@ from graphics import Graphics as obsoleteGraphics import sram2 from st7788 import ST7788 from utils import xfp2str +from ucollections import namedtuple # the one font: fixed-width (except for a few double-width chars) from font_iosevka import CELL_W, CELL_H, TEXT_PALETTE, TEXT_PALETTE_INV, COL_TEXT @@ -46,6 +47,9 @@ AT_GREY50 = 0x1 AT_RED = 0x1 AT_GREEN = 0x1 +# use this to describe cursor you need. +CursorSpec = namedtuple('CursorSpec', 'x y dbl_wide outline') + def grey_level(amt): # give percent 0..1.0 r = int(amt * 0x1f) @@ -93,6 +97,7 @@ def get_sys_status(): return rv + class Display: # XXX move to global, but rest of system looks at these member vars @@ -112,6 +117,9 @@ class Display: def __init__(self): self.dis = ST7788() + from gpu import GPUAccess + self.gpu = GPUAccess() + self.last_buf = self.make_buf(0) self.next_buf = self.make_buf(32) @@ -134,6 +142,8 @@ class Display: self.draw_status(**kws) def draw_status(self, full=False, **kws): + self.gpu.take_spi() + if full: y = TOP_MARGIN self.dis.fill_rect(0, 0, WIDTH, y-1, 0x0) @@ -178,6 +188,7 @@ class Display: w,h, data = getattr(Graphics, name) if x == 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) @@ -224,18 +235,22 @@ class Display: def text(self, x,y, msg, font=None, invert=0, attr=None): # Draw at x,y (in cell positions, not pixels) - # Use invert=1 to get reverse video + # - use invert=1 to get reverse video + # - returns ending X position, if we centered it + end_x = None if x is None or x < 0: w = self.width(msg) if x == 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 @@ -253,9 +268,12 @@ class Display: self.next_buf[y][x] = 0 x += 1 + return end_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) @@ -267,8 +285,10 @@ class Display: # clear progress bar self.next_prog_x = 0 - def show(self, just_lines=None): + def show(self, just_lines=None, cursor=None): # Push internal screen representation to device, effeciently + self.gpu.take_spi() + lines = just_lines or range(CHARS_H) for y in lines: x = 0 @@ -311,13 +331,20 @@ class Display: # maybe update progress bar if self.next_prog_x != self.last_prog_x: + # NOTE: misc/gpu/lcd.c must be updated to match any changes here x = self.next_prog_x if x: self.dis.fill_rect(0, HEIGHT-3, x, 3, COL_PROGRESS) if x != WIDTH: self.dis.fill_rect(x, HEIGHT-3, WIDTH-x, 3, COL_BLACK) self.last_prog_x = x - + + if cursor: + # implement CursorSpec values + self.gpu.cursor_at(*cursor) + self.last_buf[cursor.y][cursor.x] = 0xfffd + if cursor.dbl_wide: + self.last_buf[cursor.y][cursor.x+1] = 0xfffd # rather than clearing and redrawing, use this buffer w/ fixed parts of screen # - obsolete concept @@ -329,14 +356,20 @@ class Display: raise NotImplementedError def hline(self, y): - self.dis.fill_rect(0,y, WIDTH, 1, 0xffff) + # used only in hsm_ux.py + #self.dis.fill_rect(0,y, WIDTH, 1, 0xffff) + pass + def vline(self, x): - self.dis.fill_rect(x,TOP_MARGIN, 1, ACTIVE_H, 0xffff) + # used only in hsm_ux.py + #self.dis.fill_rect(x,TOP_MARGIN, 1, ACTIVE_H, 0xffff) + pass def scroll_bar(self, fraction): # along right edge + # MAYBE TODO: make this internal, part of show and make fraction a var? + self.gpu.take_spi() self.dis.fill_rect(WIDTH-5, 0, 5, HEIGHT, 0) - #self.icon(WIDTH-3, 1, 'scroll'); // dots + arrow mm = HEIGHT-6 pos = min(int(mm*fraction), mm) self.dis.fill_rect(WIDTH-2, pos, 1, 16, 1) @@ -348,23 +381,6 @@ class Display: if percent is not None: self.progress_bar(percent) - def DELME_splash(self): - # test code - from qrs import QRDisplaySingle - import glob, time - glob.dis = self - #q = QRDisplaySingle(['mtHSVByP9EYZmB26jASDdPVm19gvpecb5R'], is_alnum=True) - #q2 = QRDisplaySingle(['R5bcepvg91mVPdDSAj62BmZYE9PyBVSHtm'], is_alnum=True) - q = QRDisplaySingle(['a'*2953], is_alnum=False) - q2 = QRDisplaySingle(['b'*2953], is_alnum=False) - q.redraw() - while 1: - #time.sleep_ms(250) - q2.redraw() - #time.sleep_ms(250) - q.redraw() - assert False - def splash(self): # display a splash screen with some version numbers self.real_clear() @@ -411,12 +427,18 @@ class Display: return def busy_bar(self, enable, speed_code=5): - # TODO: activate the GPU to render/animate this. - #print("busy_bar: %s" % enable) - - # impt, this show() is relied-upon by callers - self.next_prog_x = 0 - self.show() + # activate the GPU to render/animate this. + # - show() in this funct is relied-upon by callers + if enable: + self.last_prog_x = self.next_prog_x = -1 + self.show() + self.gpu.busy_bar(True) + else: + # - self.show will stop animation + # - and redraw w/ no bar visible + self.last_prog_x = -1 + self.next_prog_x = 0 + self.show() def set_brightness(self, val): # normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar) @@ -549,7 +571,8 @@ class Display: # - 8-bit aligned rows of data scan_w, _, data = qr_data.packed() - self.real_clear(_internal=True) + self.gpu.take_spi() + self.real_clear() self.dis.show_qr_data(x, TOP_MARGIN + y, w, expand, scan_w, data) self.mark_correct(x, TOP_MARGIN + y, qw, qw) @@ -564,7 +587,7 @@ class Display: # show path index number: just 1 or 2 digits self.text(-1, 0, idx_hint) - self.busy_bar(False) + self.show() # here for mpy reasons diff --git a/shared/ux_q1.py b/shared/ux_q1.py index 70794393..3f230235 100644 --- a/shared/ux_q1.py +++ b/shared/ux_q1.py @@ -5,11 +5,9 @@ from uasyncio import sleep_ms import utime, gc from charcodes import * -from lcd_display import CHARS_W +from lcd_display import CHARS_W, CursorSpec from exceptions import AbortInteraction -CURSOR = '█ ' - class PressRelease: def __init__(self, need_release=KEY_SELECT+KEY_CANCEL): # Manage key-repeat: track last key, measure time it's held down, etc. @@ -82,8 +80,8 @@ async def ux_enter_number(prompt, max_value, can_cancel=False): while 1: # TODO: check width, go to two lines if needed? depends on prompt text - bx = dis.text(2, 4, prompt + ' ' + value + CURSOR) - dis.show() + bx = dis.text(2, 4, prompt + ' ' + value) + dis.show(cursor=CursorSpec(bx, 4)) ch = await press.wait() if ch == KEY_SELECT: @@ -141,8 +139,9 @@ async def ux_input_text(value, confirm_exit=True, hex_only=False, max_len=100): press = PressRelease() while 1: - dis.text(1, 1, value + CURSOR) - dis.show() + dis.text(1, 1, value+' ') + bx = dis.width(value) + 1 + dis.show(cursor=CursorSpec(bx, 1, 0, 0)) ch = await press.wait() if ch == KEY_SELECT: @@ -171,10 +170,7 @@ def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw, # extra (empty) box after ln = len(pin) - FILLED = '◉' - EMPTY = '◯' #, '◌' - #msg = ''.join(FILLED if n < ln else EMPTY for n in range(6)) - msg = ('※ ' * ln) + '██' + msg = ('※ ' * ln) y = 1 if randomize else 2 if force_draw: @@ -218,16 +214,14 @@ def ux_show_pin(dis, pin, subtitle, is_first_part, is_confirmation, force_draw, dis.text(None, -1, cta) - # auto-center broken w/ double-wides - x = None # 10 - dis.text(None, y+2, ' '*20) - dis.text(x, y+2, msg) - dis.show() + y += 2 + x = dis.text(None, y, msg) + dis.show(cursor=CursorSpec(x, y, True, False)) async def ux_login_countdown(sec): # Show a countdown, which may need to # run for multiple **days** - # XXX untested + # XXX untested TODO from glob import dis from utime import ticks_ms, ticks_diff