Skip to content

Commit dda8aef

Browse files
authored
Merge pull request #21 from oroderico/merge-into-address-blocks
Put an address beside its own QR code, on one screen
2 parents 18884cd + f861506 commit dda8aef

7 files changed

Lines changed: 403 additions & 89 deletions

File tree

‎host/origo_sdl.c‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ bool seedtool_display_qr(const char* title, const char* text)
169169
return ok;
170170
}
171171

172+
bool seedtool_display_qr_address(const char* title, const char* text)
173+
{
174+
const bool ok = seedtool_render_qr_address(title, text);
175+
if (ok) {
176+
present();
177+
}
178+
return ok;
179+
}
180+
172181
bool seedtool_display_qr_bytes(const char* title, const uint8_t* data, const size_t len)
173182
{
174183
const bool ok = seedtool_render_qr_bytes(title, data, len);

‎host/origo_simulator.c‎

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ static bool wire_order_is_big_endian(void)
322322
.confirm = NULL,
323323
.confirm_enabled = false,
324324
.back = false,
325-
.confirm_as_tick = true };
325+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
326326

327327
seedtool_render_nav_list(&nav, "Origo", items, 3, 0);
328328
const uint16_t* const pixels = seedtool_render_pixels();
@@ -1149,7 +1149,7 @@ static seedtool_nav_t stackbit_test_nav(void)
11491149
.confirm = NULL,
11501150
.confirm_enabled = false,
11511151
.back = true,
1152-
.confirm_as_tick = true };
1152+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
11531153
return nav;
11541154
}
11551155

@@ -1714,6 +1714,92 @@ static bool grouped_paging_preserves_the_value(void)
17141714
return true;
17151715
}
17161716

1717+
/* The QR screen draws the address beside the code, and the margin it has is
1718+
* not always enough: a taproot address is 62 characters against the 48 two
1719+
* columns of four hold there. seedtool_render_qr_address must refuse those
1720+
* outright rather than draw as far as it reaches, because an address cut off
1721+
* mid-value looks exactly like one that ended there and nothing on screen
1722+
* says otherwise. This is the check grouped_paging_preserves_the_value does
1723+
* not make: that one walks seedtool_render_fit_grouped, the paged path at the
1724+
* body face, and says nothing about groups drawn by any other route. The first
1725+
* version of this screen truncated a taproot address to 48 of its characters
1726+
* with every other check still passing.
1727+
*
1728+
* Proving the tail is drawn without restating the renderer's own geometry:
1729+
* change only the value's last group and the margin must change with it. The
1730+
* margin is everything left of the code, found by looking for the first column
1731+
* carrying a run of white tall enough to be the code itself rather than a line
1732+
* of text. */
1733+
static int qr_code_left_edge(void)
1734+
{
1735+
const uint16_t* const pixels = seedtool_render_pixels();
1736+
for (int x = 0; x < SEEDTOOL_DISPLAY_WIDTH; ++x) {
1737+
int run = 0;
1738+
for (int y = 0; y < SEEDTOOL_DISPLAY_HEIGHT; ++y) {
1739+
run = pixels[y * SEEDTOOL_DISPLAY_WIDTH + x] == 0xffff ? run + 1 : 0;
1740+
if (run > 40) {
1741+
return x;
1742+
}
1743+
}
1744+
}
1745+
return -1;
1746+
}
1747+
1748+
static bool qr_address_draws_every_group(void)
1749+
{
1750+
static const char* const fits[] = {
1751+
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
1752+
"1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
1753+
"3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy",
1754+
};
1755+
static uint16_t before[SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT];
1756+
1757+
for (size_t v = 0; v < sizeof(fits) / sizeof(fits[0]); ++v) {
1758+
const char* const value = fits[v];
1759+
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", value)) {
1760+
return false; /* these do fit; refusing them is its own failure */
1761+
}
1762+
const int edge = qr_code_left_edge();
1763+
if (edge <= 0) {
1764+
return false;
1765+
}
1766+
memcpy(before, seedtool_render_pixels(), sizeof(before));
1767+
1768+
/* The same value with its last group altered, same length so the code
1769+
* keeps its version and the margin keeps its width. */
1770+
char altered[80] = { 0 };
1771+
const size_t total = strlen(value);
1772+
if (total + 1 > sizeof(altered) || total < SEEDTOOL_GROUP_LEN) {
1773+
return false;
1774+
}
1775+
memcpy(altered, value, total);
1776+
for (size_t i = total - SEEDTOOL_GROUP_LEN; i < total; ++i) {
1777+
altered[i] = altered[i] == 'q' ? 'p' : 'q';
1778+
}
1779+
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", altered)) {
1780+
return false;
1781+
}
1782+
const uint16_t* const after = seedtool_render_pixels();
1783+
1784+
bool margin_changed = false;
1785+
for (int y = 0; y < SEEDTOOL_DISPLAY_HEIGHT && !margin_changed; ++y) {
1786+
for (int x = 0; x < edge; ++x) {
1787+
if (before[y * SEEDTOOL_DISPLAY_WIDTH + x] != after[y * SEEDTOOL_DISPLAY_WIDTH + x]) {
1788+
margin_changed = true;
1789+
break;
1790+
}
1791+
}
1792+
}
1793+
if (!margin_changed) {
1794+
return false; /* the tail was never drawn */
1795+
}
1796+
}
1797+
1798+
/* And the one the margin cannot hold, which must say so rather than clip. */
1799+
return !seedtool_render_qr_address(
1800+
"m/86'/0'/0'/0/0", "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr");
1801+
}
1802+
17171803
/* The grouped line is centred with its gaps counted in, so it can run off an
17181804
* edge in a way the plain centred line cannot. Measured, not assumed. */
17191805
static bool grouped_lines_clear_the_edges(void)
@@ -1766,7 +1852,7 @@ static bool nav_chrome_bands_do_not_collide(void)
17661852
.confirm = "Continue",
17671853
.confirm_enabled = confirm_enabled,
17681854
.back = true,
1769-
.confirm_as_tick = true };
1855+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
17701856
seedtool_render_nav_list(&nav, titles[i], words, 12, 0);
17711857
/* Title bar ends at 20, rows run 21..116 (LIST_TOP + 3 *
17721858
* NAV_ROW_HEIGHT, less the 2px each row leaves under itself), the
@@ -1803,13 +1889,13 @@ static bool nav_chrome_bands_do_not_collide(void)
18031889
/* A menu wears the chrome with no confirm at all: its rows are its
18041890
* actions, so the arrow is the only control. Both places the confirm could
18051891
* appear must stay empty - the band along the bottom, and the title bar's
1806-
* right slot. The slot is the one that caught a real bug: confirm_as_tick
1892+
* right slot. The slot is the one that caught a real bug: the tick
18071893
* is set for every list, so the tick drew on the Origo and Wallet menus,
18081894
* offering an answer to a question those screens never ask. Nothing here
18091895
* noticed, because every check only ever asserted the tick was present. */
18101896
for (size_t s = 0; s < 2; ++s) {
18111897
const seedtool_nav_t nav
1812-
= { .selected = s ? SEEDTOOL_NAV_BACK : 0, .back = true, .confirm_as_tick = true };
1898+
= { .selected = s ? SEEDTOOL_NAV_BACK : 0, .back = true, .confirm_style = SEEDTOOL_CONFIRM_TICK };
18131899
seedtool_render_nav_list(&nav, "Word entry", words, 2, 0);
18141900
if (!nav_band_is_clear(NAV_BAR_BAND_Y, SEEDTOOL_DISPLAY_HEIGHT) || nav_right_slot_is_drawn()) {
18151901
return false;
@@ -1847,15 +1933,15 @@ static bool nav_chrome_bands_do_not_collide(void)
18471933
};
18481934
for (size_t i = 0; i < sizeof(screens) / sizeof(screens[0]); ++i) {
18491935
for (int on_back = 0; on_back < 2; ++on_back) {
1850-
/* confirm_as_tick, because that is what nav_screen sets and these
1936+
/* confirm_style, because that is what nav_screen sets and these
18511937
* are its screens: the confirm is a box in the title bar's right
18521938
* slot and no bar is drawn. Rendering them with a bar would have
18531939
* this table checking a layout the firmware stopped shipping. */
18541940
const seedtool_nav_t nav = { .selected = on_back ? SEEDTOOL_NAV_BACK : SEEDTOOL_NAV_CONFIRM,
18551941
.confirm = screens[i].label,
18561942
.confirm_enabled = true,
18571943
.back = true,
1858-
.confirm_as_tick = true };
1944+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
18591945
seedtool_render_nav_text(&nav, screens[i].title, screens[i].line1, screens[i].line2, NULL);
18601946
/* Line 2 sits at 65 and the 16px face is that tall again, so its
18611947
* wraps land at 81, 97, 113 - and 113 is inside the bar's own
@@ -1914,7 +2000,7 @@ static bool nav_chrome_bands_do_not_collide(void)
19142000
.confirm = dice[i].label,
19152001
.confirm_enabled = true,
19162002
.back = true,
1917-
.confirm_as_tick = true };
2003+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
19182004
seedtool_render_nav_dice(&nav, dice[i].title, dice[i].line1, dice[i].line2, &full);
19192005
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(104, 118)) {
19202006
return false;
@@ -1954,7 +2040,7 @@ static bool nav_chrome_bands_do_not_collide(void)
19542040
.confirm = NULL,
19552041
.confirm_enabled = true,
19562042
.back = true,
1957-
.confirm_as_tick = true };
2043+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
19582044
seedtool_render_nav_text(&nav, notices[i].title, notices[i].line1, notices[i].line2, NULL);
19592045
/* A notice's only control is now the arrow - if that failed to draw the
19602046
* screen would be one the reader cannot leave at all, which is worth
@@ -1981,7 +2067,7 @@ static bool nav_chrome_bands_do_not_collide(void)
19812067
.confirm_enabled = true,
19822068
.back = true,
19832069
.counter = "8/8",
1984-
.confirm_as_tick = true };
2070+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
19852071
seedtool_render_nav_text(&paged, "Canonical transcript", "20-1-2-1-4-1-1-1-1-1-1-1-1-",
19862072
"1-1-1-1-1-1-1-1-1-20-1-1-1-", "1-1-1-1-1-1-1-1-1-1");
19872073
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(116, 118)) {
@@ -2005,7 +2091,7 @@ static bool nav_chrome_bands_do_not_collide(void)
20052091
.confirm_enabled = true,
20062092
.back = true,
20072093
.counter = "6/6",
2008-
.confirm_as_tick = true };
2094+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
20092095
seedtool_render_nav_rows(&listed, "BIP39 word numbers", rows, 4);
20102096
if (!nav_band_is_clear(20, 21) || !nav_band_is_clear(116, 118)) {
20112097
return false;
@@ -2347,6 +2433,10 @@ static int self_test(void)
23472433
fputs("Origo grouped address geometry self-test failed\n", stderr);
23482434
return 1;
23492435
}
2436+
if (!qr_address_draws_every_group()) {
2437+
fputs("Origo address-beside-QR self-test failed\n", stderr);
2438+
return 1;
2439+
}
23502440
if (!nav_chrome_bands_do_not_collide()) {
23512441
fputs("Origo nav chrome geometry self-test failed\n", stderr);
23522442
return 1;
@@ -2388,12 +2478,12 @@ static int self_test(void)
23882478
.confirm = NULL,
23892479
.confirm_enabled = false,
23902480
.back = true,
2391-
.confirm_as_tick = true };
2481+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
23922482
const seedtool_nav_t at_start = { .selected = 0,
23932483
.confirm = NULL,
23942484
.confirm_enabled = false,
23952485
.back = true,
2396-
.confirm_as_tick = true };
2486+
.confirm_style = SEEDTOOL_CONFIRM_TICK };
23972487
seedtool_render_nav_list(&scrolled, "Wallet", menu_labels, 7, 4);
23982488
seedtool_render_nav_list(&at_start, "Word 3/12 aba", menu_labels, MENU_LABEL_COUNT, 0);
23992489
/* Draw the real layouts, each opened at its centre, so one that overflows a

‎main/seedtool_app.c‎

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ static int nav_screen(const char* title, const char* one, const char* two, const
260260
* tells the reader what taking it does, on every screen here but the
261261
* two entropy verdicts, which pass no title and say it in the body
262262
* instead for the reason given at their call. */
263-
.confirm_as_tick = true,
263+
.confirm_style = SEEDTOOL_CONFIRM_TICK,
264264
};
265265
for (;;) {
266266
if (progress) {
@@ -371,7 +371,7 @@ static int choose_nav(const char* title, const char* const* items, const size_t
371371
* ring has one discontinuity wherever it is put, and putting it
372372
* here sets the two ways out of a screen side by side rather than
373373
* at opposite ends of the glass. */
374-
.confirm_as_tick = true,
374+
.confirm_style = SEEDTOOL_CONFIRM_TICK,
375375
};
376376
seedtool_display_nav_list(&nav, title, items, count, top);
377377
const size_t ring = nav_ring_size(count, confirm_enabled, back);
@@ -426,25 +426,28 @@ static size_t page_selection(const size_t position, const size_t pages)
426426
}
427427

428428
/* The chrome a paged screen wears, so page_text and show_numbered_list say it
429-
* once each rather than both spelling out the same four fields. */
430-
static seedtool_nav_t page_nav(
431-
const size_t position, const size_t pages, const char* counter, const bool confirmable)
429+
* once each rather than both spelling out the same four fields.
430+
*
431+
* `style` is theirs to choose because their confirms mean different things:
432+
* paged text hands off to another screen, a numbered list is agreed to. */
433+
static seedtool_nav_t page_nav(const size_t position, const size_t pages, const char* counter,
434+
const bool confirmable, const seedtool_confirm_t style)
432435
{
433436
const seedtool_nav_t nav = {
434437
.selected = page_selection(position, pages),
435-
/* No label is what removes the tick, the same way a menu removes it:
436-
* the reading screens have nothing for the reader to agree to, so the
437-
* corner that would mean "I accept" is left empty rather than given a
438-
* meaning the caller then discards. */
438+
/* No label is what removes the control, the same way a menu removes
439+
* it: the reading screens have nothing for the reader to agree to or
440+
* to go on to, so the corner is left empty rather than given a meaning
441+
* the caller then discards. */
439442
.confirm = confirmable ? "Continue" : NULL,
440443
.confirm_enabled = true,
441444
.back = true,
442445
.counter = counter,
443-
/* Same tick as everywhere else. On a paged screen the ring is the
444-
* reading order - arrow, page 1..N, then done - so the confirm being
445-
* a corner rather than a bar changes where the cursor lands after the
446-
* last page and nothing about how the pages are read. */
447-
.confirm_as_tick = true,
446+
/* On a paged screen the ring is the reading order - arrow, page 1..N,
447+
* then the corner - so the confirm being a corner rather than a bar
448+
* changes where the cursor lands after the last page and nothing about
449+
* how the pages are read. */
450+
.confirm_style = style,
448451
};
449452
return nav;
450453
}
@@ -773,7 +776,7 @@ static bool page_text_impl(const char* title, const char* text, const bool confi
773776
line3[length[first + 2]] = '\0';
774777
}
775778
(void)snprintf(footer, sizeof(footer), "%u/%u", (unsigned)(page + 1), (unsigned)pages);
776-
const seedtool_nav_t nav = page_nav(position, pages, footer, confirmable);
779+
const seedtool_nav_t nav = page_nav(position, pages, footer, confirmable, SEEDTOOL_CONFIRM_FORWARD);
777780
if (grouped) {
778781
/* Where each line sits in the whole value, in groups, so the
779782
* alternating ink carries across the line break rather than
@@ -830,13 +833,6 @@ static bool page_text(const char* title, const char* text)
830833
return page_text_impl(title, text, true, false);
831834
}
832835

833-
/* page_text for a value with no word shapes to read by - an address. Drawn in
834-
* groups of four so the reader transcribing it has somewhere to keep their
835-
* place; the string itself is untouched. */
836-
static bool page_grouped(const char* title, const char* text)
837-
{
838-
return page_text_impl(title, text, true, true);
839-
}
840836

841837
/* Paged text there is nothing to take. Returns nothing because there is
842838
* nothing to return: the reader pages down and leaves by the arrow, and a
@@ -1942,19 +1938,35 @@ static void show_descriptor(const char* mnemonic, const char* passphrase, const
19421938
seedtool_zero(value, sizeof(value));
19431939
}
19441940

1945-
/* A single address's own QR, opened from the address list: no account key in
1946-
* this view to warn about or to carousel over to, since a photo of one
1947-
* address on its own reveals nothing the address itself did not already. */
1941+
/* A single address's own QR, and the first thing opening an address shows: no
1942+
* account key in this view to warn about or to carousel over to, since a photo
1943+
* of one address on its own reveals nothing the address itself did not
1944+
* already. Scanning is what a reader almost always came for, so the code, its
1945+
* path and the address itself share one screen rather than two.
1946+
*
1947+
* Not every address fits beside its code: a taproot address is 62 characters
1948+
* against the 48 the margin holds. seedtool_display_qr_address refuses those
1949+
* rather than drawing as far as it reaches - an address cut off mid-value
1950+
* looks exactly like one that ended there - and they fall back to the code
1951+
* alone, then the paged text, which has the width for them. */
19481952
static void show_address_qr(const char* title, const char* address)
19491953
{
19501954
for (;;) {
1955+
if (seedtool_display_qr_address(title, address)) {
1956+
if (wait_key() == KEY_REDRAW) {
1957+
continue;
1958+
}
1959+
return;
1960+
}
19511961
if (!seedtool_display_qr(title, address)) {
19521962
notice("Too long for a QR", title, "Read it as text instead");
19531963
return;
19541964
}
1955-
if (wait_key() != KEY_REDRAW) {
1956-
return;
1965+
if (wait_key() == KEY_REDRAW) {
1966+
continue;
19571967
}
1968+
(void)page_text_impl(title, address, false, true);
1969+
return;
19581970
}
19591971
}
19601972

@@ -2235,9 +2247,11 @@ static void show_addresses(
22352247
* make. */
22362248
char path[sizeof(prefix) + 12];
22372249
(void)snprintf(path, sizeof(path), "%s/%u", prefix, (unsigned)index);
2238-
if (page_grouped(path, address)) {
2239-
show_address_qr(path, address);
2240-
}
2250+
/* One screen: the code, its path, and the address itself beside it.
2251+
* Opening an address used to land on the text and reach the QR through
2252+
* it, which cost a step to the reader who came to scan - and left the
2253+
* two halves of the same fact on separate screens. */
2254+
show_address_qr(path, address);
22412255
seedtool_zero(address, sizeof(address));
22422256
}
22432257
seedtool_zero(addresses, sizeof(addresses));
@@ -2279,7 +2293,7 @@ static void show_stackbit(const char* mnemonic)
22792293
char footer[16];
22802294
(void)snprintf(footer, sizeof(footer), "%u/%u", (unsigned)(selected + 1), (unsigned)count);
22812295
const char* const word = seedtool_word(numbers[selected] - 1);
2282-
const seedtool_nav_t nav = page_nav(position, count, NULL, false);
2296+
const seedtool_nav_t nav = page_nav(position, count, NULL, false, SEEDTOOL_CONFIRM_TICK);
22832297
if (layout == 0) {
22842298
seedtool_display_stackbit_screen(&nav, "Stackbit 1248", numbers[selected], word, footer);
22852299
} else {
@@ -2374,7 +2388,7 @@ static bool show_numbered_list_impl(const char* mnemonic, const bool show_words,
23742388
* the firmware build even though the host build lets it pass. */
23752389
char footer[24];
23762390
(void)snprintf(footer, sizeof(footer), "%u/%u", (unsigned)(page + 1), (unsigned)pages);
2377-
const seedtool_nav_t nav = page_nav(cursor, pages, footer, confirmable);
2391+
const seedtool_nav_t nav = page_nav(cursor, pages, footer, confirmable, SEEDTOOL_CONFIRM_TICK);
23782392
const char* const rows[] = { lines[0], lines[1], lines[2], lines[3] };
23792393
seedtool_display_nav_rows(&nav, show_words ? "BIP39 words" : "BIP39 word numbers", rows, 4);
23802394
switch (wait_key()) {

0 commit comments

Comments
 (0)