BBQr improvements

This commit is contained in:
Peter D. Gray 2024-03-01 16:37:26 -05:00
parent 65cbefdc80
commit fd21495b10
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
13 changed files with 331 additions and 150 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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):

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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)
{

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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