Files
n_signer/firmware/feather_s3_tft/main/ui.c

681 lines
21 KiB
C

#include "ui.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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;
}
}