BBQr improvements
This commit is contained in:
parent
65cbefdc80
commit
fd21495b10
@ -24,11 +24,11 @@
|
||||
|
||||
# Releases
|
||||
|
||||
## 0.0.3 - 2024-02-08
|
||||
## 0.0.3Q - 2024-02-08
|
||||
|
||||
- first test-only release
|
||||
|
||||
## 0.0.4 - 2024-02-15
|
||||
## 0.0.4Q - 2024-02-15
|
||||
|
||||
- BBQr animation display smoother
|
||||
- test cases fixed, bugs that were exposed, fixed.
|
||||
@ -36,7 +36,7 @@
|
||||
- "Ready to Sign" messaging improved, slot B support.
|
||||
- block firmware upgrade when battery very low
|
||||
|
||||
## 0.0.5 - 2024-02-16
|
||||
## 0.0.5Q - 2024-02-16
|
||||
|
||||
- fixes and changes from version 5.2.2 of Mk4 encorporated
|
||||
- bugfix: save bip-39 password to absent SD card
|
||||
@ -45,7 +45,7 @@
|
||||
- bugfix: cant detect SD card in Ready to Sign...
|
||||
- WIF private key detected when scaning QR (display only for now)
|
||||
|
||||
## 0.0.6 - 2024-02-22
|
||||
## 0.0.6Q - 2024-02-22
|
||||
|
||||
- bugfix: randomize keys for PIN entry
|
||||
- when picking files, we just skip to showing you the files options (or picking the
|
||||
@ -59,7 +59,7 @@
|
||||
- cleanups, bugfixes
|
||||
|
||||
|
||||
## 0.0.7 - 2024-02-26
|
||||
## 0.0.7Q - 2024-02-26
|
||||
|
||||
- bugfix: BBQr display of some segwit transactions would sometimes fail with message
|
||||
about "non hex digit"
|
||||
@ -69,3 +69,15 @@
|
||||
- Supports QR export from Wallet Exports: will be either text file (U) or JSON (J)
|
||||
BBQr sequence, but only if it cannot fit into normal single QR.
|
||||
|
||||
## 0.0.8Q - 2024-03-xx
|
||||
|
||||
- BBQr display changes:
|
||||
- if less than 12 frames would result, uses simpliest QR that can fit on
|
||||
screen at 2x or 3x size. Result is easier to scan BBQr's.
|
||||
- progress bar along bottom drawn differently
|
||||
- in some cases, the status bar area (at top) will be used to show QR
|
||||
- added: Advanced > Danger Zone > Debug Functions > BBQr Demo
|
||||
- Says "Loading..." not "Wait..." during login process.
|
||||
- Many more test cases.
|
||||
|
||||
|
||||
|
||||
@ -1603,6 +1603,12 @@ async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None,
|
||||
async def debug_assert(*a):
|
||||
assert False, "failed assertion"
|
||||
|
||||
async def debug_bbqr_test(*a):
|
||||
# Q only: test BBQr w/ lots of data
|
||||
from ux_q1 import show_bbqr_codes
|
||||
from gpu_binary import BINARY
|
||||
await show_bbqr_codes('B', BINARY*3, 'GPU binary')
|
||||
|
||||
async def debug_except(*a):
|
||||
print(34 / 0)
|
||||
|
||||
|
||||
@ -28,6 +28,94 @@ def int2base36(n):
|
||||
|
||||
return tostr(a) + tostr(b)
|
||||
|
||||
def num_qr_needed_ll(char_capacity, ll, split_mod):
|
||||
# Determine number of QR's would be needed to hold ll alnum characters,
|
||||
# if each QR holds char_capacity of chars.
|
||||
# - when 2 or more QR, consider the exact split point cannot be between encoded symbols
|
||||
# - ok to return huge numbers for unlikely cases
|
||||
# - returns (number of QR needed), (# of chars in each), (# of bytes in each)
|
||||
from math import ceil
|
||||
|
||||
cap = char_capacity - 8 # 8==HEADER_LEN
|
||||
|
||||
if ll < cap:
|
||||
# no alignment concerns
|
||||
return 1, ll
|
||||
|
||||
# max per non-final qr
|
||||
cap2 = cap - (cap % split_mod)
|
||||
need = ceil(ll / cap2)
|
||||
|
||||
assert need >= 2
|
||||
|
||||
# going to be 2 or more, gotta be precise
|
||||
# - final part doesn't need to be "encoding aligned"
|
||||
actual = ((need - 1) * cap2) + cap
|
||||
#print("act=%d ll=%d need=%d c=%d c2=%d" % (actual, ll, need, cap, cap2))
|
||||
|
||||
if ll > actual:
|
||||
need += 1
|
||||
|
||||
# TODO: the final QR might have just a a few chars in it, if we redistribute
|
||||
# the data into the request (need) parts, then each QR can have more forward
|
||||
# error correction and be more robust. subject to split_mod
|
||||
|
||||
return need, cap2
|
||||
|
||||
def num_qr_needed(encoding, data_len):
|
||||
# returns (QR version, num_parts, part_size[bytes])
|
||||
# - lots of Q-related policy here
|
||||
|
||||
# Just a few key values, picked because the height of the QR must
|
||||
# fit vertically (240 px tall) ... see "bbqr table"
|
||||
CHARS_PER_VERSION = [
|
||||
# (QR version, alnum capacity)
|
||||
(40, 4296), # 177px tall, shown 1:1 pixels -- phones can scan fine
|
||||
(15, 758), # 77px x 3: 77*3 = 231px tall
|
||||
(25, 1853), # 117px, doubled: 234px tall
|
||||
(40, 4296), # give up and just make it work!
|
||||
]
|
||||
|
||||
if encoding == 'H':
|
||||
char_len = data_len * 2
|
||||
split_mod = 2
|
||||
else:
|
||||
# plan for Base32, always best option
|
||||
# - five inputs bytes => 8 alnum chars
|
||||
# - for final set of 1-5 we remove padding == , so between 2..7 chars
|
||||
char_len = ((data_len//5) * 8) + { 0:0, 1:2, 2:4, 3:5, 4:7 }[data_len % 5]
|
||||
split_mod = 8
|
||||
|
||||
# try a few select resolutions (sizes) in order such that we use either single QR
|
||||
# or the least-dense option that gives reasonable number of QR's
|
||||
for target_vers, capacity in CHARS_PER_VERSION:
|
||||
num_parts, part_size = num_qr_needed_ll(capacity, char_len, split_mod)
|
||||
if num_parts == 1:
|
||||
# great, no animation needed!
|
||||
break
|
||||
if target_vers != 40 and num_parts <= 12:
|
||||
# will be reasonable animation, so use this size
|
||||
break
|
||||
|
||||
# convert # of chars per QR, into bytes per each (last one may be less)
|
||||
if num_parts > 1:
|
||||
assert part_size % split_mod == 0
|
||||
if encoding == 'H':
|
||||
pkt_size = part_size // 2
|
||||
else:
|
||||
pkt_size = part_size * 5 // 8
|
||||
else:
|
||||
pkt_size = data_len
|
||||
|
||||
#print('bbqr: %d bytes => %d chars (%s enc) => v%d in %d parts of %d char / %d bytes each'
|
||||
# % (data_len, char_len, encoding, target_vers, num_parts, part_size, pkt_size))
|
||||
|
||||
assert num_parts * pkt_size >= data_len
|
||||
|
||||
#assert part_size % split_mod == 0, (target_vers, part_size, split_mod, char_len, data_len)
|
||||
return target_vers, num_parts, pkt_size
|
||||
|
||||
|
||||
|
||||
class BBQrHeader:
|
||||
def __init__(self, taste):
|
||||
|
||||
@ -224,6 +224,7 @@ DebugFunctionsMenu = [
|
||||
# xxxxxxxxxxxxxxxx
|
||||
MenuItem("Lamp Test", f=lamp_test),
|
||||
MenuItem("Keyboard Test", f=keyboard_test),
|
||||
MenuItem('BBQr Demo', f=debug_bbqr_test, predicate=lambda: version.has_qwerty),
|
||||
MenuItem('Debug: assert', f=debug_assert),
|
||||
MenuItem('Debug: except', f=debug_except),
|
||||
MenuItem('Check: BL FW', f=check_firewall_read),
|
||||
|
||||
@ -120,10 +120,8 @@ class Display:
|
||||
self.next_buf = self.make_buf(32)
|
||||
|
||||
# state of progress bar (bottom edge)
|
||||
self.last_prog_x = -1
|
||||
self.last_prog_w = -1
|
||||
self.next_prog_x = 0
|
||||
self.next_prog_w = 0
|
||||
self.last_prog = (-1, -1)
|
||||
self.next_prog = (0, -1)
|
||||
|
||||
# state of scroll bar (right side)
|
||||
self.last_scroll = 0.0
|
||||
@ -170,15 +168,13 @@ class Display:
|
||||
# full brightness when on VBUS and when showing QR's
|
||||
self.dis.backlight.intensity(255)
|
||||
|
||||
def draw_status(self, full=False, redraw_line=False, **kws):
|
||||
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)
|
||||
kws = get_sys_status()
|
||||
|
||||
if full or redraw_line:
|
||||
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:
|
||||
@ -316,20 +312,27 @@ class Display:
|
||||
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_w = 0
|
||||
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_w = 0
|
||||
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)
|
||||
@ -371,23 +374,8 @@ class Display:
|
||||
self.last_buf[y][:] = self.next_buf[y]
|
||||
|
||||
# maybe update progress bar
|
||||
if (self.next_prog_x, self.next_prog_w) != (self.last_prog_x, self.last_prog_w):
|
||||
x = self.next_prog_x
|
||||
w = self.next_prog_w
|
||||
h = PROGRESS_BAR_H # NOTE: misc/gpu/lcd.c will need update if H changes
|
||||
if x == 0 and self.last_prog_x == x and self.last_prog_w <= w:
|
||||
# no need to undraw, we can just draw on top
|
||||
pass
|
||||
else:
|
||||
# erase under
|
||||
self.dis.fill_rect(0, HEIGHT-h, WIDTH, h, COL_BLACK)
|
||||
|
||||
# draw new bar
|
||||
if w:
|
||||
self.dis.fill_rect(x, HEIGHT-h, w, h, COL_PROGRESS)
|
||||
|
||||
self.last_prog_x = x
|
||||
self.last_prog_w = w
|
||||
if self.next_prog != self.last_prog:
|
||||
self._draw_progress_bar()
|
||||
|
||||
# maybe update right hand scroll bar
|
||||
if self.next_scroll != self.last_scroll:
|
||||
@ -414,13 +402,12 @@ class Display:
|
||||
|
||||
# When drawing another screen for a bit, then coming back, use these
|
||||
def save_state(self):
|
||||
# TODO: should be a dataclass w/ all our state details
|
||||
# should really be a dataclass w/ all our state details
|
||||
return ([array.array('I', ln) for ln in self.last_buf],
|
||||
self.last_prog_x, self.last_prog_w,
|
||||
self.last_scroll)
|
||||
self.last_prog, self.last_scroll)
|
||||
|
||||
def restore_state(self, old_state):
|
||||
rows, self.next_prog_x, self.next_prog_w, self.next_scroll = 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()
|
||||
@ -450,6 +437,29 @@ class Display:
|
||||
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
|
||||
@ -500,20 +510,22 @@ class Display:
|
||||
# Horizontal progress bar
|
||||
# takes 0.0 .. 1.0 as fraction of doneness
|
||||
percent = max(0, min(1.0, percent))
|
||||
self.next_prog_x = 0
|
||||
self.next_prog_w = int(WIDTH * 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 if one or none of them
|
||||
self.next_prog_x = self.next_prog_w = 0
|
||||
# turn off bar segment display if one or none of them
|
||||
self.next_prog = (0, -1)
|
||||
else:
|
||||
w = WIDTH // m
|
||||
self.next_prog_x = (n * w)
|
||||
self.next_prog_w = w
|
||||
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
|
||||
@ -531,16 +543,14 @@ class Display:
|
||||
# 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.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_x = -1
|
||||
self.last_prog_w = -1
|
||||
self.next_prog_x = 0
|
||||
self.next_prog_w = 0
|
||||
self.last_prog = (WIDTH, -1)
|
||||
self.next_prog = (0, -1)
|
||||
self.show()
|
||||
|
||||
def set_brightness(self, val):
|
||||
@ -623,7 +633,6 @@ class Display:
|
||||
# Show a QR code on screen w/ some text under it
|
||||
# - invert not supported on Q1
|
||||
# - sidebar not supported here (see users.py)
|
||||
# - we need one more (white) pixel on all sides
|
||||
from utils import word_wrap
|
||||
|
||||
# maybe show something other than QR contents under it
|
||||
@ -650,26 +659,27 @@ class Display:
|
||||
num_lines = 0
|
||||
del parts
|
||||
|
||||
self.clear()
|
||||
if partial_bar is not None:
|
||||
self.progress_part_bar(partial_bar)
|
||||
|
||||
# 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
|
||||
|
||||
self.gpu.take_spi()
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
if w == 109:
|
||||
# v23 => w=109 ACTIVE_H=220
|
||||
# - to make v23 fit, have to loose one line of QR margin at bottom
|
||||
# - and kill text, and corrupt status line (y=-1)
|
||||
if w == 77:
|
||||
# v15 => 77px x 3: 77*3 = 231px
|
||||
expand = 3
|
||||
num_lines = 0
|
||||
fullscreen = True
|
||||
elif w == 117:
|
||||
# v25 =>=117px x 2 => 234px
|
||||
expand = 2
|
||||
num_lines = 0
|
||||
fullscreen = True
|
||||
elif expand == 1 and num_lines:
|
||||
# Maybe loose the text lines?
|
||||
expand2 = max(1, ACTIVE_H // (w+2))
|
||||
@ -679,27 +689,53 @@ class Display:
|
||||
|
||||
# vert center in available space
|
||||
qw = (w+2) * expand
|
||||
y = max(-1, (ACTIVE_H - (num_lines * CELL_H) - qw) // 2)
|
||||
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)
|
||||
|
||||
# horz center
|
||||
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.dis.show_qr_data(x, TOP_MARGIN + y, w, expand, scan_w, data)
|
||||
self.mark_correct(x, TOP_MARGIN + y, qw, qw)
|
||||
self.clear()
|
||||
|
||||
if num_lines:
|
||||
# centered text under that
|
||||
y = CHARS_H - num_lines
|
||||
for line in parts:
|
||||
self.text(None, y, line)
|
||||
y += 1
|
||||
self.dis.show_qr_data(x, y, w, expand, scan_w, data, trim_lines)
|
||||
|
||||
if idx_hint:
|
||||
# show path index number: just 1 or 2 digits
|
||||
self.text(-1, 0, idx_hint)
|
||||
if partial_bar is not None:
|
||||
self.progress_part_bar(partial_bar)
|
||||
|
||||
# pass a max brightness flag here, which will be cleared after next show
|
||||
self.show(max_bright=True)
|
||||
if not fullscreen:
|
||||
self.mark_correct(x, y, qw, qw)
|
||||
|
||||
if num_lines:
|
||||
# centered text under that
|
||||
y = CHARS_H - num_lines
|
||||
for line in parts:
|
||||
self.text(None, y, line)
|
||||
y += 1
|
||||
|
||||
if idx_hint:
|
||||
# show path index number: just 1 or 2 digits
|
||||
self.text(-1, 0, 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
|
||||
@ -766,7 +802,7 @@ class Display:
|
||||
# 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_x = -1
|
||||
self.last_prog = (-1, -1)
|
||||
|
||||
|
||||
# here for mpy reasons
|
||||
|
||||
@ -89,10 +89,10 @@ class ST7788():
|
||||
#assert len(palette) == 2 * 16
|
||||
lcd.send_packed(self.spi, x, y, w, h, palette, pixels)
|
||||
|
||||
def show_qr_data(self, x, y, w, expand, scan_w, packed_data):
|
||||
def show_qr_data(self, x, y, w, expand, scan_w, packed_data, trim_lines=0):
|
||||
# 8-bit packed QR data, and where to draw it, expanded by 'expand'
|
||||
assert len(packed_data) == (scan_w*w) // 8
|
||||
lcd.send_qr(self.spi, x, y, w, expand, scan_w, packed_data)
|
||||
lcd.send_qr(self.spi, x, y, w, expand, scan_w, packed_data, trim_lines)
|
||||
|
||||
def fill_rect(self, x,y, w,h, pixel=0x0000):
|
||||
# set a rectangle to a single colour
|
||||
|
||||
@ -1076,68 +1076,58 @@ async def ux_visualize_textqr(txt, maxlen=200):
|
||||
"We can't do any more with it." % txt, title="Simple Text")
|
||||
|
||||
async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
||||
# Compress, encode and split data. Then show it animated
|
||||
# Compress, encode and split data, then show it animated...
|
||||
# - happily goes to version 40 if needed
|
||||
# - needs to pre-render the QR to get animation to be faster
|
||||
# - version of first QR is used for all ther others
|
||||
# - on Q: ver 23 => 109x109 is largest that can be pixel-doubled, can do v40 tho at 1:1
|
||||
# - screen resolution is considered when picking QR version number
|
||||
# - data may point to output side of PSRAM area
|
||||
# - Should always doZlib compression (because it nearly always helps)
|
||||
# - BUT: need zlib compress (not present)
|
||||
# - SO: write C code that compresses from one area memory (or PSRAM) into PSRAM
|
||||
# and also does Base32 expansion at same time.
|
||||
# - just doing HEX encoding because it is easier for now
|
||||
# - TODO this code needs better home
|
||||
from bbqr import TYPE_LABELS, int2base36
|
||||
# - Should always do zlib compression (because it nearly always helps)
|
||||
# - BUT: need zlib compress (not present) .. delayed for now
|
||||
from bbqr import TYPE_LABELS, int2base36, b32encode, num_qr_needed
|
||||
from glob import PSRAM, dis
|
||||
from ux import ux_wait_keyup, ux_wait_keydown
|
||||
import uqr
|
||||
|
||||
PAYLOAD_PER_V40 = 2144 # if HEX encoded, active payload (max) per v40 QR
|
||||
PAYLOAD_PER_V23 = 790 # if HEX encoded, active payload (max) per v23 QR
|
||||
|
||||
assert not PSRAM.is_at(data, 0) # input data would be overwritten with our work
|
||||
assert type_code in TYPE_LABELS
|
||||
|
||||
data_len = len(data)
|
||||
dis.fullscreen('Generating BBQr...', .1)
|
||||
|
||||
if already_hex:
|
||||
data_len //= 2
|
||||
encoding = 'H'
|
||||
data_len = len(data) // 2
|
||||
else:
|
||||
# default to Base32, because always best option
|
||||
encoding = '2'
|
||||
data_len = len(data)
|
||||
|
||||
# assume V40 and split, if that doesn't work, drop to 23 for BBQr multiples
|
||||
for target_vers, capacity in [ (40, PAYLOAD_PER_V40), (23, PAYLOAD_PER_V23) ]:
|
||||
num_parts = int(round((data_len / capacity) + 0.5, 0))
|
||||
if num_parts == 1:
|
||||
# use V40 only if whole thing fits, otherwise favour v23
|
||||
break
|
||||
|
||||
part_size = data_len // num_parts
|
||||
runt_size = data_len - (num_parts * part_size)
|
||||
if runt_size:
|
||||
# spread data evenly between min-required parts
|
||||
num_parts += 1
|
||||
part_size = (data_len+num_parts-1) // num_parts
|
||||
assert part_size <= capacity
|
||||
# try a few select resolutions (sizes) in order such that we use either single QR
|
||||
# or the least-dense option that gives reasonable number of QR's
|
||||
target_vers, num_parts, part_size = num_qr_needed(encoding, data_len)
|
||||
|
||||
assert num_parts * part_size >= data_len
|
||||
|
||||
dis.fullscreen('Generating BBQr...', .1)
|
||||
|
||||
pos = 0
|
||||
force_version = 40
|
||||
for pkt in range(num_parts):
|
||||
# BBQr header
|
||||
hdr = 'B$H' + type_code + int2base36(num_parts) + int2base36(pkt)
|
||||
hdr = 'B$' + encoding + type_code + int2base36(num_parts) + int2base36(pkt)
|
||||
|
||||
# encode the hex
|
||||
# encode the bytes
|
||||
assert pos < data_len
|
||||
if already_hex:
|
||||
body = data[pos*2:(pos+part_size)*2].decode()
|
||||
# not encoding, just chars->bytes
|
||||
body = data[pos:pos+(part_size*2)].decode()
|
||||
pos += part_size*2
|
||||
else:
|
||||
body = b2a_hex(data[pos:pos+part_size]).upper().decode()
|
||||
pos += part_size
|
||||
# base32 encoding
|
||||
body = b32encode(data[pos:pos+part_size])
|
||||
pos += part_size
|
||||
|
||||
# do the hard work
|
||||
qr_data = uqr.make(hdr+body, min_version=(10 if pkt == 0 else force_version),
|
||||
max_version=force_version, encoding=uqr.Mode_ALPHANUMERIC)
|
||||
max_version=force_version, encoding=uqr.Mode_ALPHANUMERIC)
|
||||
|
||||
# save the rendered QR
|
||||
if pkt == 0:
|
||||
@ -1161,6 +1151,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
||||
|
||||
# hide Generating... text
|
||||
dis.fullscreen(' ', 1)
|
||||
dis.show()
|
||||
|
||||
ch = None
|
||||
while not ch:
|
||||
@ -1179,8 +1170,7 @@ async def show_bbqr_codes(type_code, data, msg, already_hex=False):
|
||||
if ch: break
|
||||
|
||||
# after QR drawing, we need to correct some pixels
|
||||
dis.real_clear()
|
||||
dis.draw_status(redraw_line=True)
|
||||
dis.clear()
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
//
|
||||
// AUTO-generated.
|
||||
//
|
||||
// built: 2024-02-26
|
||||
// version: 0.0.7Q
|
||||
// built: 2024-02-29
|
||||
// version: 0.0.8Q
|
||||
//
|
||||
#include <stdint.h>
|
||||
|
||||
// this overrides ports/stm32/fatfs_port.c
|
||||
uint32_t get_fattime(void) {
|
||||
return 0x585a0000UL;
|
||||
return 0x585d0000UL;
|
||||
}
|
||||
|
||||
@ -163,8 +163,9 @@ STATIC mp_obj_t send_qr(size_t n_args, const mp_obj_t *args)
|
||||
// - expands it by "expand"
|
||||
// - adds one-unit border to all sides
|
||||
// - scan_w: scanline width (packed to mod 8)
|
||||
// - called: lcd.send_qr(self.spi, x, y, w, expand, scan_w, packed_data)
|
||||
// - called: lcd.send_qr(self.spi, x, y, w, expand, scan_w, packed_data, trim_lines)
|
||||
// - tearing should happen, but because we draw line-wise, seems invisible.
|
||||
// - if set, trim_lines randomly skips a few lines in their interior to make it work
|
||||
// - test with:
|
||||
// from h import *; from ux_q1 import *; arun(show_bbqr_codes('B', bytes(ngu.random.bytes(4096)), 'foo'))
|
||||
//
|
||||
@ -185,6 +186,12 @@ STATIC mp_obj_t send_qr(size_t n_args, const mp_obj_t *args)
|
||||
mp_raise_ValueError(NULL);
|
||||
}
|
||||
|
||||
mp_int_t trim_lines = mp_obj_get_int(args[7]);
|
||||
if(trim_lines && expand == 1) {
|
||||
// need to have some space to work
|
||||
mp_raise_ValueError(NULL);
|
||||
}
|
||||
|
||||
// operate line by line
|
||||
const uint32_t W = (w+2) * expand;
|
||||
uint16_t line[(w+2) * expand];
|
||||
@ -196,11 +203,11 @@ STATIC mp_obj_t send_qr(size_t n_args, const mp_obj_t *args)
|
||||
// top, bot white lines, around the QR body
|
||||
set_window(spi, x, y, W, expand);
|
||||
write_data_repeated(spi, expand, sizeof(line), (const uint8_t *)line);
|
||||
set_window(spi, x, y+W-expand, W, expand);
|
||||
set_window(spi, x, y+W-expand-trim_lines, W, expand);
|
||||
write_data_repeated(spi, expand, sizeof(line), (const uint8_t *)line);
|
||||
|
||||
// body of QR
|
||||
set_window(spi, x, y+expand, W, expand*w);
|
||||
set_window(spi, x, y+expand, W, (expand*w)-trim_lines);
|
||||
|
||||
const uint8_t *ptr = boxes.buf;
|
||||
for(int Y=0; Y < w; Y++, ptr += (scan_w/8)) {
|
||||
@ -216,12 +223,17 @@ STATIC mp_obj_t send_qr(size_t n_args, const mp_obj_t *args)
|
||||
p++;
|
||||
}
|
||||
|
||||
write_data_repeated(spi, expand, sizeof(line), (const uint8_t *)line);
|
||||
if(trim_lines && Y && ((Y % 23) == 0)) {
|
||||
trim_lines--;
|
||||
write_data_repeated(spi, expand-1, sizeof(line), (const uint8_t *)line);
|
||||
} else {
|
||||
write_data_repeated(spi, expand, sizeof(line), (const uint8_t *)line);
|
||||
}
|
||||
}
|
||||
|
||||
return mp_const_none;
|
||||
}
|
||||
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(send_qr_obj, 7, 7, send_qr);
|
||||
MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(send_qr_obj, 8, 8, send_qr);
|
||||
|
||||
STATIC mp_obj_t fill_rect(size_t n_args, const mp_obj_t *args)
|
||||
{
|
||||
|
||||
@ -578,15 +578,11 @@ def cap_screen_qr(cap_image):
|
||||
img = ImageOps.expand(img, 16, 0) # add border
|
||||
img = img.resize( (256, 256))
|
||||
else:
|
||||
# Q1 - trim status line, convert to greyscale
|
||||
# Q - convert to greyscale
|
||||
# - and trim progress bar (does cause readability issues)
|
||||
# - MAYBE: blow up the size, helps on fine 1:1 QR cases.
|
||||
w, h = orig_img.size # 320x240
|
||||
# - remove status bar (harmless)
|
||||
# - and progress bar (does cause readability issues)
|
||||
# - MAYBE: blow up the size, helps on fine QR.
|
||||
# - but some are still unreadable!?!?
|
||||
img = orig_img.crop( (0, 15, w, h-5) ).convert('L')
|
||||
#w, h = img.size
|
||||
#img = img.resize( (w*9, h*9))
|
||||
img = orig_img.crop( (0, 0, w, h-5) ).convert('L')
|
||||
|
||||
img.save('debug/last-qr.png')
|
||||
#img.show()
|
||||
|
||||
@ -110,19 +110,21 @@ def render_bbqr(need_keypress, cap_screen_qr, sim_exec, readback_bbqr_ll):
|
||||
cmd += f'import ux_q1,main; main.TT = asyncio.create_task(ux_q1.show_bbqr_codes'\
|
||||
f'("{file_type}", {data}, {msg!r}));'
|
||||
print(f"CMD: {cmd}")
|
||||
resp = sim_exec(cmd)
|
||||
print(f"RESP: {resp}")
|
||||
assert 'error' not in resp.lower()
|
||||
try:
|
||||
resp = sim_exec(cmd)
|
||||
print(f"RESP: {resp}")
|
||||
assert 'error' not in resp.lower()
|
||||
|
||||
num_parts, encoding, rb_ft, parts = readback_bbqr_ll()
|
||||
assert rb_ft == file_type
|
||||
num_parts, encoding, rb_ft, parts = readback_bbqr_ll()
|
||||
assert rb_ft == file_type
|
||||
|
||||
print(sim_exec(f'import main; main.TT.cancel()'))
|
||||
need_keypress('0') # for menu to redraw
|
||||
finally:
|
||||
print(sim_exec(f'import main; main.TT.cancel()'))
|
||||
need_keypress('0') # for menu to redraw
|
||||
|
||||
# we only can decode simple BBQr here
|
||||
assert encoding == 'H'
|
||||
body = a2b_hex(''.join(p[8:] for p in [parts[i] for i in range(num_parts)]))
|
||||
assert encoding in 'HZ2'
|
||||
_, body = join_qrs(parts.values())
|
||||
|
||||
if file_type == 'U':
|
||||
body = body.decode('utf-8')
|
||||
@ -241,4 +243,33 @@ def test_bbqr_psbt(size, encoding, max_ver, partial, segwit, scan_a_qr, readback
|
||||
|
||||
press_cancel() # back to menu
|
||||
|
||||
@pytest.mark.parametrize('size', [7854, ] + list(range(1, (12*2680), 197)))
|
||||
@pytest.mark.parametrize('encoding', '2H')
|
||||
def test_split_unit(size, encoding, sim_exec, sim_eval):
|
||||
# unit test for: bbqr.test_split_unit()
|
||||
|
||||
cmd = f'import bbqr; RV.write(repr(bbqr.num_qr_needed( {encoding!r}, {size} )))'
|
||||
print(f"CMD: {cmd}")
|
||||
resp = sim_exec(cmd)
|
||||
print(f"RESP: {resp}")
|
||||
assert 'error' not in resp.lower()
|
||||
|
||||
target_ver, num_parts, part_size = eval(resp)
|
||||
|
||||
|
||||
assert num_parts * part_size >= size
|
||||
|
||||
if size == 7854 and encoding == '2':
|
||||
assert target_ver == 25
|
||||
assert num_parts == 7
|
||||
|
||||
if encoding == 'H':
|
||||
assert 1 <= part_size <= 2144
|
||||
elif encoding == '2':
|
||||
assert 1 <= part_size <= 2680
|
||||
|
||||
assert 15 <= target_ver <= 40
|
||||
if num_parts > 12:
|
||||
assert target_ver == 40
|
||||
|
||||
# EOF
|
||||
|
||||
@ -267,11 +267,11 @@ class LCDSimulator(SimulatedScreen):
|
||||
def new_contents(self, readable):
|
||||
# got bytes for new update. expect a header and packed pixels
|
||||
while 1:
|
||||
prefix = readable.read(11)
|
||||
prefix = readable.read(13)
|
||||
if not prefix:
|
||||
break
|
||||
|
||||
mode, X,Y, w, h, count = struct.unpack('<s5H', prefix)
|
||||
mode, X,Y, w, h, count, argX = struct.unpack('<s6H', prefix)
|
||||
mode = mode.decode('ascii')
|
||||
here = readable.read(count)
|
||||
|
||||
@ -330,6 +330,8 @@ class LCDSimulator(SimulatedScreen):
|
||||
expand = h
|
||||
h = w
|
||||
scan_w = (w+7)//8
|
||||
trim_lines = argX
|
||||
|
||||
#print(f'QR: {scan_w=} {expand=} {w=}')
|
||||
assert 21 <= w <= 177 and (w%2) == 1, w
|
||||
|
||||
@ -341,9 +343,16 @@ class LCDSimulator(SimulatedScreen):
|
||||
qr = ImageOps.expand(tmp, expand, 0)
|
||||
assert qr.size == (W, W)
|
||||
|
||||
delme = {}
|
||||
if trim_lines:
|
||||
# remove every 47th line, up to trim_lines qty
|
||||
delme = list(range(47, W, 47))[0:trim_lines]
|
||||
|
||||
pos = 0
|
||||
pixels = list(qr.getdata(0))
|
||||
for y in range(Y, Y+W):
|
||||
for y in range(Y, Y+W-trim_lines):
|
||||
if y in delme:
|
||||
pos += W
|
||||
for x in range(X, X+W):
|
||||
self.mv[x][y] = 0x0000 if pixels[pos] else 0xffff
|
||||
pos += 1
|
||||
|
||||
@ -31,12 +31,12 @@ class ST7788:
|
||||
# for use by variant/gpu.py code
|
||||
if len(args) < 4:
|
||||
args += (0,)*(4-len(args))
|
||||
hdr = struct.pack('<s5H', cmd, *args)
|
||||
hdr = struct.pack('<s6H', cmd, *args)
|
||||
self.pipe.write(hdr)
|
||||
|
||||
def show_zpixels(self, x, y, w, h, zpixels):
|
||||
# display compressed pixel data
|
||||
hdr = struct.pack('<s5H', 'z', x, y, w, h, len(zpixels))
|
||||
hdr = struct.pack('<s6H', 'z', x, y, w, h, len(zpixels), 0)
|
||||
self.pipe.write(hdr + zpixels)
|
||||
|
||||
def fill_screen(self, pixel=0x0000):
|
||||
@ -44,7 +44,7 @@ class ST7788:
|
||||
self.fill_rect(0,0, 320,240, pixel)
|
||||
|
||||
def fill_rect(self, x,y, w,h, pixel=0x0000):
|
||||
msg = struct.pack('<s5HH', 'f', x, y, w, h, 2, pixel)
|
||||
msg = struct.pack('<s6HH', 'f', x, y, w, h, 2, 0, pixel)
|
||||
self.pipe.write(msg)
|
||||
|
||||
def show_pal_pixels(self, x, y, w, h, palette, pixels):
|
||||
@ -52,18 +52,18 @@ class ST7788:
|
||||
assert len(palette) == 2 * 16
|
||||
assert len(pixels) == w * h // 2
|
||||
|
||||
hdr = struct.pack('<s5H', 't', x, y, w, h, len(pixels)+len(palette))
|
||||
hdr = struct.pack('<s6H', 't', x, y, w, h, len(pixels)+len(palette), 0)
|
||||
self.pipe.write(hdr + palette + pixels)
|
||||
|
||||
def show_qr_data(self, x, y, w, expand, scan_w, packed_data):
|
||||
def show_qr_data(self, x, y, w, expand, scan_w, packed_data, trim_lines=0):
|
||||
# 8-bit packed QR data, and where to draw it, expanded
|
||||
assert len(packed_data) == (scan_w*w) // 8, [len(packed_data), w, scan_w]
|
||||
hdr = struct.pack('<s5H', 'q', x, y, w, expand, len(packed_data))
|
||||
hdr = struct.pack('<s6H', 'q', x, y, w, expand, len(packed_data), trim_lines)
|
||||
self.pipe.write(hdr + packed_data)
|
||||
|
||||
def save_snapshot(self, full_path):
|
||||
# save into PNG in local filesystem, return file name
|
||||
hdr = struct.pack('<s5H', 's', 0, 0, 0, 0, len(full_path))
|
||||
hdr = struct.pack('<s6H', 's', 0, 0, 0, 0, len(full_path), 0)
|
||||
self.pipe.write(hdr + full_path.encode())
|
||||
|
||||
# EOF
|
||||
|
||||
Loading…
Reference in New Issue
Block a user