#include "ui.h" #include #include #include #include #include "buttons.h" #include "display.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "mnemonic.h" #include "secure_mem.h" typedef enum { UI_MAIN_GENERATE = 0, UI_MAIN_ENTER, UI_MAIN_VIEW, } ui_main_menu_item_t; #define UI_CHAR_W_SMALL 6 #define UI_CHAR_W_LARGE 12 typedef struct { size_t count; size_t first_idx[3]; int exact_idx; } prefix_matches_t; typedef struct { char phrase[256]; bool has_phrase; ui_main_menu_item_t main_sel; int letter_idx; /* 0..25 */ char prefix[12]; size_t prefix_len; char entered_words[12][12]; size_t entered_count; } ui_state_t; static btn_event_t wait_any_button(void) { btn_event_t evt; do { evt = buttons_wait(0xFFFFFFFFu); } while (evt.kind == BTN_EVT_NONE); return evt; } static void draw_splash(void) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(60, "N_SIGNER", RGB565_RED, RGB565_BLACK)); } static void draw_main_menu_row(bool has_phrase, int row_idx, bool selected) { static const char *items[3] = {"Generate", "Enter", "View"}; static const int ys[3] = {28, 52, 76}; bool disabled = (row_idx == UI_MAIN_VIEW && !has_phrase); char line[24]; snprintf(line, sizeof(line), "%s%s", selected ? "> " : " ", items[row_idx]); ESP_ERROR_CHECK(display_fill_rect(24, ys[row_idx], 200, 16, RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text( 24, ys[row_idx], line, disabled ? RGB565_DIM_GRAY : (selected ? RGB565_RED : RGB565_WHITE), RGB565_BLACK)); } static void draw_main_menu_full(const ui_state_t *st) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(4, "MAIN MENU", RGB565_WHITE, RGB565_BLACK)); for (int i = 0; i < 3; ++i) { draw_main_menu_row(st->has_phrase, i, ((int)st->main_sel == i)); } ESP_ERROR_CHECK(display_draw_text_small(12, 120, "D2 up D1 dn D0 ok", RGB565_DIM_GRAY, RGB565_BLACK)); } static void draw_main_menu_selection_change(bool has_phrase, int prev_sel, int new_sel) { draw_main_menu_row(has_phrase, prev_sel, false); draw_main_menu_row(has_phrase, new_sel, true); } static void split_phrase_words(const char *phrase, char words[12][12]) { char buf[256]; char *tok; size_t i = 0; memset(words, 0, 12 * 12); if (phrase == NULL) { return; } strncpy(buf, phrase, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; tok = strtok(buf, " \t\r\n"); while (tok != NULL && i < 12) { strncpy(words[i], tok, 11); words[i][11] = '\0'; i++; tok = strtok(NULL, " \t\r\n"); } } static void draw_word_grid(const char words[12][12], int highlight_idx) { const int col_w = 80; const int row_h = 24; for (int i = 0; i < 12; ++i) { int row = i / 3; int col = i % 3; int x = col * col_w + 2; int y = 14 + row * row_h; char line[20]; snprintf(line, sizeof(line), "%2d. %.11s", i + 1, words[i]); ESP_ERROR_CHECK(display_draw_text_small( x, y, line, (i == highlight_idx) ? RGB565_RED : RGB565_WHITE, RGB565_BLACK)); } } static void draw_view_page(const ui_state_t *st) { char words[12][12]; split_phrase_words(st->phrase, words); ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_small_centered(2, "YOUR MNEMONIC", RGB565_WHITE, RGB565_BLACK)); draw_word_grid(words, -1); ESP_ERROR_CHECK(display_draw_text_small_centered(120, "any key = back", RGB565_DIM_GRAY, RGB565_BLACK)); } static void draw_generate_page(void) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "GENERATING...", RGB565_WHITE, RGB565_BLACK)); } static void clear_enter_words(ui_state_t *st) { secure_memzero(st->entered_words, sizeof(st->entered_words)); st->entered_count = 0; st->prefix_len = 0; st->prefix[0] = '\0'; } static void letter_cell_pos(int idx, int *x, int *y) { int row = (idx < 13) ? 0 : 1; int col = (idx < 13) ? idx : (idx - 13); *x = 10 + col * 16; *y = row ? 68 : 44; } static void compute_allowed_next_letters(const ui_state_t *st, bool allowed[26]) { for (int i = 0; i < 26; ++i) { allowed[i] = false; } if (st->prefix_len == 0) { for (int i = 0; i < 26; ++i) { allowed[i] = true; } return; } for (size_t i = 0; i < 2048; ++i) { const char *w = mnemonic_word_at(i); size_t wlen; char c; int idx; if (w == NULL) { continue; } if (strncmp(w, st->prefix, st->prefix_len) != 0) { continue; } wlen = strlen(w); if (wlen <= st->prefix_len) { continue; } c = w[st->prefix_len]; if (c < 'a' || c > 'z') { continue; } idx = (int)(c - 'a'); allowed[idx] = true; } } static int find_next_allowed_letter(int current, int delta, const bool allowed[26]) { int idx = current; for (int i = 0; i < 26; ++i) { idx = (idx + delta + 26) % 26; if (allowed[idx]) { return idx; } } return current; } static int first_allowed_letter(const bool allowed[26]) { for (int i = 0; i < 26; ++i) { if (allowed[i]) { return i; } } return 0; } static void draw_enter_letter_cell(int idx, bool selected, const bool allowed[26]) { int x; int y; char c[2] = {(char)('A' + idx), '\0'}; uint16_t color = RGB565_WHITE; letter_cell_pos(idx, &x, &y); ESP_ERROR_CHECK(display_fill_rect(x, y, 12, 16, RGB565_BLACK)); if (!allowed[idx]) { color = RGB565_DIM_GRAY; } if (selected && allowed[idx]) { color = RGB565_RED; } ESP_ERROR_CHECK(display_draw_text(x, y, c, color, RGB565_BLACK)); } static void draw_enter_letter_cursor_change(int prev_idx, int new_idx, const bool allowed[26]) { draw_enter_letter_cell(prev_idx, false, allowed); draw_enter_letter_cell(new_idx, true, allowed); } static void compute_prefix_matches(const ui_state_t *st, prefix_matches_t *m) { m->count = 0; m->first_idx[0] = 0; m->first_idx[1] = 0; m->first_idx[2] = 0; m->exact_idx = -1; if (st->prefix_len == 0) { return; } for (size_t i = 0; i < 2048; ++i) { const char *w = mnemonic_word_at(i); if (w == NULL) { continue; } if (strncmp(w, st->prefix, st->prefix_len) == 0) { if (m->count < 3) { m->first_idx[m->count] = i; } m->count++; if (strcmp(w, st->prefix) == 0 && m->exact_idx < 0) { m->exact_idx = (int)i; } } } } static void draw_enter_compact_static(const ui_state_t *st, const bool allowed[26]) { char hdr[24]; unsigned current_word = (unsigned)(st->entered_count + 1); ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); if (current_word > 12) { current_word = 12; } snprintf(hdr, sizeof(hdr), "WORD %u/12", current_word); ESP_ERROR_CHECK(display_draw_text(10, 8, hdr, RGB565_WHITE, RGB565_BLACK)); for (int i = 0; i < 26; ++i) { draw_enter_letter_cell(i, i == st->letter_idx, allowed); } } static int draw_word_with_prefix_large(int x, int y, const char *word, size_t prefix_len) { char pref[12]; char rest[12]; size_t wlen; size_t plen; if (word == NULL) { return x; } wlen = strlen(word); plen = (prefix_len > wlen) ? wlen : prefix_len; memset(pref, 0, sizeof(pref)); memset(rest, 0, sizeof(rest)); if (plen > 0) { memcpy(pref, word, plen); } if (wlen > plen) { memcpy(rest, word + plen, wlen - plen); } ESP_ERROR_CHECK(display_draw_text(x, y, pref, RGB565_RED, RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text(x + ((int)plen * UI_CHAR_W_LARGE), y, rest, RGB565_WHITE, RGB565_BLACK)); return x + ((int)wlen * UI_CHAR_W_LARGE); } static void draw_match_row_large(const ui_state_t *st) { int x = 4; for (size_t i = 0; i < 2048; ++i) { const char *w = mnemonic_word_at(i); size_t wlen; int needed; if (w == NULL) { continue; } if (strncmp(w, st->prefix, st->prefix_len) != 0) { continue; } wlen = strlen(w); needed = (int)(wlen * UI_CHAR_W_LARGE) + UI_CHAR_W_LARGE; if (x + needed > DISPLAY_WIDTH) { break; } x = draw_word_with_prefix_large(x, 88, w, st->prefix_len); ESP_ERROR_CHECK(display_draw_text(x, 88, " ", RGB565_WHITE, RGB565_BLACK)); x += UI_CHAR_W_LARGE; } } static void draw_enter_compact_dynamic(const ui_state_t *st, const prefix_matches_t *m) { ESP_ERROR_CHECK(display_fill_rect(0, 86, DISPLAY_WIDTH, 49, RGB565_BLACK)); if (st->prefix_len == 0) { ESP_ERROR_CHECK(display_draw_text_small(4, 118, "D0=letter D1h=back D2h=del", RGB565_DIM_GRAY, RGB565_BLACK)); return; } if (m->count == 0) { ESP_ERROR_CHECK(display_draw_text_centered(88, "NO MATCH", RGB565_RED, RGB565_BLACK)); } else { draw_match_row_large(st); } if (m->count == 1) { ESP_ERROR_CHECK(display_draw_text_small(4, 108, "One word left: D0=YES D2=NO", RGB565_RED, RGB565_BLACK)); } if (st->entered_count < 12) { ESP_ERROR_CHECK(display_draw_text_small(4, 118, "D0=letter D1h=back D2h=del", RGB565_DIM_GRAY, RGB565_BLACK)); } else { ESP_ERROR_CHECK(display_draw_text_small(4, 118, "12/12: D0 hold=save D2 hold=del", RGB565_DIM_GRAY, RGB565_BLACK)); } } static int build_phrase_from_entered(ui_state_t *st) { size_t cursor = 0; st->phrase[0] = '\0'; for (size_t i = 0; i < st->entered_count; ++i) { size_t wlen = strlen(st->entered_words[i]); if (i > 0) { if (cursor + 1 >= sizeof(st->phrase)) { return -1; } st->phrase[cursor++] = ' '; } if (cursor + wlen >= sizeof(st->phrase)) { return -1; } memcpy(st->phrase + cursor, st->entered_words[i], wlen); cursor += wlen; st->phrase[cursor] = '\0'; } return 0; } int ui_run_until_working(char out_mnemonic[256]) { ui_state_t st; bool main_menu_drawn = false; if (out_mnemonic == NULL) { return -1; } memset(&st, 0, sizeof(st)); st.main_sel = UI_MAIN_GENERATE; draw_splash(); (void)wait_any_button(); while (1) { btn_event_t evt; if (!main_menu_drawn) { draw_main_menu_full(&st); main_menu_drawn = true; } evt = wait_any_button(); if (evt.id == BTN_D2) { int prev = (int)st.main_sel; if (st.has_phrase) { st.main_sel = (st.main_sel == UI_MAIN_GENERATE) ? UI_MAIN_VIEW : (ui_main_menu_item_t)((int)st.main_sel - 1); } else { st.main_sel = (st.main_sel == UI_MAIN_GENERATE) ? UI_MAIN_ENTER : UI_MAIN_GENERATE; } if ((int)st.main_sel != prev) { draw_main_menu_selection_change(st.has_phrase, prev, (int)st.main_sel); } continue; } if (evt.id == BTN_D1) { int prev = (int)st.main_sel; if (st.has_phrase) { st.main_sel = (st.main_sel == UI_MAIN_VIEW) ? UI_MAIN_GENERATE : (ui_main_menu_item_t)((int)st.main_sel + 1); } else { st.main_sel = (st.main_sel == UI_MAIN_ENTER) ? UI_MAIN_GENERATE : UI_MAIN_ENTER; } if ((int)st.main_sel != prev) { draw_main_menu_selection_change(st.has_phrase, prev, (int)st.main_sel); } continue; } if (evt.id != BTN_D0) { continue; } if (st.main_sel == UI_MAIN_VIEW) { if (!st.has_phrase) { continue; } draw_view_page(&st); (void)wait_any_button(); main_menu_drawn = false; continue; } if (st.main_sel == UI_MAIN_GENERATE) { draw_generate_page(); if (generate_mnemonic_12(st.phrase, sizeof(st.phrase)) != 0) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "GEN FAILED", RGB565_RED, RGB565_BLACK)); vTaskDelay(pdMS_TO_TICKS(2000)); main_menu_drawn = false; continue; } st.has_phrase = true; draw_view_page(&st); (void)wait_any_button(); strncpy(out_mnemonic, st.phrase, 255); out_mnemonic[255] = '\0'; secure_memzero(&st, sizeof(st)); return 0; } clear_enter_words(&st); st.letter_idx = 0; { prefix_matches_t matches; bool allowed_letters[26]; compute_prefix_matches(&st, &matches); compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); while (1) { evt = wait_any_button(); if (evt.id == BTN_D2 && evt.kind == BTN_EVT_PRESS) { if (matches.count == 1 && st.prefix_len > 0) { st.prefix_len = 0; st.prefix[0] = '\0'; compute_prefix_matches(&st, &matches); draw_enter_compact_dynamic(&st, &matches); } else { int prev_idx = st.letter_idx; st.letter_idx = find_next_allowed_letter(st.letter_idx, -1, allowed_letters); draw_enter_letter_cursor_change(prev_idx, st.letter_idx, allowed_letters); } continue; } if (evt.id == BTN_D1 && evt.kind == BTN_EVT_PRESS) { int prev_idx = st.letter_idx; st.letter_idx = find_next_allowed_letter(st.letter_idx, +1, allowed_letters); draw_enter_letter_cursor_change(prev_idx, st.letter_idx, allowed_letters); continue; } if (evt.id == BTN_D1 && evt.kind == BTN_EVT_LONG_PRESS) { if (st.prefix_len > 0) { st.prefix_len--; st.prefix[st.prefix_len] = '\0'; } compute_prefix_matches(&st, &matches); compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); continue; } if (evt.id == BTN_D2 && evt.kind == BTN_EVT_LONG_PRESS) { if (st.prefix_len > 0) { st.prefix_len = 0; st.prefix[0] = '\0'; } else if (st.entered_count > 0) { secure_memzero(st.entered_words[st.entered_count - 1], sizeof(st.entered_words[0])); st.entered_count--; } compute_prefix_matches(&st, &matches); compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); continue; } if (evt.id == BTN_D0 && evt.kind == BTN_EVT_PRESS) { if (st.entered_count >= 12) { if (build_phrase_from_entered(&st) == 0 && mnemonic_validate(st.phrase) == 0) { draw_view_page(&st); (void)wait_any_button(); strncpy(out_mnemonic, st.phrase, 255); out_mnemonic[255] = '\0'; secure_memzero(&st, sizeof(st)); return 0; } continue; } if (matches.count == 1 && st.prefix_len > 0) { const char *w = mnemonic_word_at(matches.first_idx[0]); if (w != NULL) { strncpy(st.entered_words[st.entered_count], w, 11); st.entered_words[st.entered_count][11] = '\0'; st.entered_count++; } st.prefix_len = 0; st.prefix[0] = '\0'; if (st.entered_count == 12) { if (build_phrase_from_entered(&st) != 0 || mnemonic_validate(st.phrase) != 0) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "BAD CHECKSUM", RGB565_RED, RGB565_BLACK)); vTaskDelay(pdMS_TO_TICKS(1200)); compute_prefix_matches(&st, &matches); compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); continue; } draw_view_page(&st); (void)wait_any_button(); strncpy(out_mnemonic, st.phrase, 255); out_mnemonic[255] = '\0'; secure_memzero(&st, sizeof(st)); return 0; } compute_prefix_matches(&st, &matches); compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); continue; } if (st.prefix_len + 1 < sizeof(st.prefix)) { st.prefix[st.prefix_len++] = (char)('a' + st.letter_idx); st.prefix[st.prefix_len] = '\0'; compute_prefix_matches(&st, &matches); if (matches.count == 0) { st.prefix_len--; st.prefix[st.prefix_len] = '\0'; compute_prefix_matches(&st, &matches); } compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); } continue; } if (evt.id == BTN_D0 && evt.kind == BTN_EVT_LONG_PRESS) { compute_prefix_matches(&st, &matches); if (st.entered_count == 12 && st.prefix_len == 0) { if (build_phrase_from_entered(&st) != 0 || mnemonic_validate(st.phrase) != 0) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "BAD CHECKSUM", RGB565_RED, RGB565_BLACK)); vTaskDelay(pdMS_TO_TICKS(1200)); compute_prefix_matches(&st, &matches); compute_allowed_next_letters(&st, allowed_letters); st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters); draw_enter_compact_static(&st, allowed_letters); draw_enter_compact_dynamic(&st, &matches); continue; } strncpy(out_mnemonic, st.phrase, 255); out_mnemonic[255] = '\0'; secure_memzero(&st, sizeof(st)); return 0; } if (st.prefix_len == 0) { clear_enter_words(&st); break; } continue; } } } main_menu_drawn = false; } }