#include "display.h" #include #include #include #include namespace { static constexpr uint8_t kDisplayWidth = 128; static constexpr uint8_t kDisplayHeight = 128; static constexpr uint8_t kDefaultI2cAddr = 0x3D; static constexpr uint8_t kGrayDim = 0x04; static constexpr uint8_t kGrayBar = 0x08; static constexpr uint8_t kGrayText = 0x0F; Adafruit_SSD1327 g_display(kDisplayWidth, kDisplayHeight, &Wire, -1, 400000, 100000); bool g_displayPresent = false; bool g_dirty = true; display_view_t g_view = DISPLAY_VIEW_VOLUME; uint8_t g_volume = 50; bool g_signerMode = false; char g_status[24] = "BOOT"; uint8_t g_menuSelected = 0; uint8_t g_mnemonicPage = 0; char g_mnemonic[256] = ""; char g_msgTitle[18] = ""; char g_msgLine1[22] = ""; char g_msgLine2[22] = ""; char g_enterSlots[12][16] = {{0}}; uint8_t g_enterActiveSlot = 0; uint8_t g_enterStage = 1; char g_enterBuilding[16] = ""; uint8_t g_enterCursorPos = 0; enum signer_screen_t { SIGNER_SCREEN_MENU = 0, SIGNER_SCREEN_ENTER_GRID = 1, SIGNER_SCREEN_MNEMONIC_PAGE = 2, SIGNER_SCREEN_MESSAGE = 3, SIGNER_SCREEN_READY = 4, }; signer_screen_t g_signerScreen = SIGNER_SCREEN_MENU; uint8_t clampVolume(uint8_t v) { return (v > 100) ? 100 : v; } void drawStatusLine() { g_display.setTextSize(1); g_display.setTextColor(kGrayText); g_display.setCursor(2, 118); g_display.print(g_status); } void drawButtonLegend(const char *left, const char *center, const char *right) { g_display.setTextSize(1); g_display.setTextColor(kGrayText); g_display.setTextWrap(false); if (left && left[0] != '\0') { g_display.setCursor(2, 118); g_display.print(left); } int16_t x1 = 0; int16_t y1 = 0; uint16_t w = 0; uint16_t h = 0; if (center && center[0] != '\0') { g_display.getTextBounds(center, 0, 0, &x1, &y1, &w, &h); const int16_t x = static_cast((kDisplayWidth - w) / 2); g_display.setCursor(x, 118); g_display.print(center); } if (right && right[0] != '\0') { g_display.getTextBounds(right, 0, 0, &x1, &y1, &w, &h); const int16_t x = static_cast(126 - w); g_display.setCursor(x, 118); g_display.print(right); } } void drawVolumeView() { g_display.setTextWrap(false); g_display.setTextColor(kGrayDim); g_display.setTextSize(2); g_display.setCursor(40, 18); g_display.print("VOL"); char volBuf[4]; snprintf(volBuf, sizeof(volBuf), "%u", g_volume); int16_t x1 = 0; int16_t y1 = 0; uint16_t w = 0; uint16_t h = 0; g_display.setTextSize(6); g_display.getTextBounds(volBuf, 0, 0, &x1, &y1, &w, &h); const int16_t x = static_cast((kDisplayWidth - w) / 2); const int16_t y = 42; g_display.setTextColor(kGrayText); g_display.setCursor(x, y); g_display.print(volBuf); const int16_t barX = 8; const int16_t barY = 102; const int16_t barW = kDisplayWidth - 16; const int16_t barH = 8; g_display.drawRect(barX, barY, barW, barH, kGrayDim); const int16_t fillW = static_cast((static_cast(barW - 2) * g_volume) / 100U); if (fillW > 0) { g_display.fillRect(barX + 1, barY + 1, fillW, barH - 2, kGrayBar); } drawButtonLegend("prev", "play", "next"); } void drawSignerMenu() { g_display.setTextWrap(false); g_display.setTextSize(1); g_display.setTextColor(kGrayDim); g_display.setCursor(2, 2); g_display.print("SIGNER MENU"); const char *items[2] = {"Generate new", "Enter mnemonic"}; g_display.setTextColor(kGrayText); g_display.setTextSize(2); for (uint8_t i = 0; i < 2; ++i) { g_display.setCursor(6, 28 + i * 24); g_display.print((i == g_menuSelected) ? ">" : " "); g_display.setCursor(20, 28 + i * 24); g_display.print(items[i]); } drawButtonLegend("", "select", ""); } void drawEnterMnemonic() { g_display.setTextWrap(false); g_display.setTextSize(1); g_display.setTextColor(kGrayDim); g_display.setCursor(2, 2); g_display.print("ENTER MNEMONIC"); g_display.setTextColor(kGrayText); for (uint8_t i = 0; i < 12; ++i) { const uint8_t row = (i < 6) ? i : (i - 6); const int16_t x = (i < 6) ? 0 : 64; const int16_t y = static_cast(18 + row * 9); const bool isActive = (i == g_enterActiveSlot); const char cursor = isActive ? '>' : ' '; const char *slotWord = g_enterSlots[i]; if (!slotWord) { slotWord = ""; } char activeBuf[16]; const char *shownWord = slotWord; if (isActive) { const char *src = g_enterBuilding; if (!src) { src = ""; } if (g_enterStage < 3) { size_t srcLen = strlen(src); if (srcLen > 14) { srcLen = 14; } size_t pos = g_enterCursorPos; if (pos > srcLen) { pos = srcLen; } size_t out = 0; for (size_t j = 0; j < pos && out < sizeof(activeBuf) - 1; ++j) { activeBuf[out++] = src[j]; } if (out < sizeof(activeBuf) - 1) { activeBuf[out++] = '_'; } for (size_t j = pos; j < srcLen && out < sizeof(activeBuf) - 1; ++j) { activeBuf[out++] = src[j]; } activeBuf[out] = '\0'; shownWord = activeBuf; } else if (src[0] != '\0') { shownWord = src; } } char lineBuf[22]; snprintf(lineBuf, sizeof(lineBuf), "%c%2u %s", cursor, (unsigned)(i + 1), shownWord); g_display.setCursor(x, y); g_display.print(lineBuf); } if (g_enterStage == 3) { drawButtonLegend("", "select", "back"); } else { drawButtonLegend("", "letter", "back"); } } void drawMnemonicPage() { g_display.setTextWrap(false); g_display.setTextSize(1); g_display.setTextColor(kGrayDim); g_display.setCursor(2, 2); g_display.print("GENERATED PHRASE"); char copy[256]; strncpy(copy, g_mnemonic, sizeof(copy) - 1); copy[sizeof(copy) - 1] = '\0'; char *words[12] = {nullptr}; uint8_t count = 0; char *saveptr = nullptr; char *tok = strtok_r(copy, " ", &saveptr); while (tok && count < 12) { words[count++] = tok; tok = strtok_r(nullptr, " ", &saveptr); } g_display.setTextColor(kGrayText); for (uint8_t i = 0; i < count; ++i) { const uint8_t row = (i < 6) ? i : (i - 6); const int16_t x = (i < 6) ? 0 : 64; const int16_t y = static_cast(18 + row * 9); char lineBuf[20]; snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(i + 1), words[i]); g_display.setCursor(x, y); g_display.print(lineBuf); } drawButtonLegend("cancel", "confirm", "new"); } void drawMessage() { g_display.setTextWrap(false); g_display.setTextSize(1); g_display.setTextColor(kGrayDim); g_display.setCursor(2, 2); g_display.print(g_msgTitle); g_display.setTextSize(2); g_display.setTextColor(kGrayText); g_display.setCursor(6, 34); g_display.print(g_msgLine1); g_display.setCursor(6, 62); g_display.print(g_msgLine2); const char *left = ""; const char *center = ""; const char *right = ""; if (strcmp(g_msgTitle, "APPROVAL") == 0) { left = "deny"; center = "allow"; } else if (strcmp(g_msgTitle, "INVALID") == 0) { center = "select"; right = "back"; } else if (strcmp(g_msgTitle, "ERROR") == 0) { center = "select"; } drawButtonLegend(left, center, right); } void drawSignerReady() { g_display.setTextWrap(false); g_display.setTextSize(1); g_display.setTextColor(kGrayDim); g_display.setCursor(2, 2); g_display.print("SIGNER READY"); g_display.drawRect(4, 22, 120, 62, kGrayDim); g_display.setTextSize(2); g_display.setTextColor(kGrayText); g_display.setCursor(16, 36); g_display.print("Awaiting"); g_display.setCursor(30, 58); g_display.print("host"); drawButtonLegend("", "", "menu"); } void renderFrame() { if (!g_displayPresent) { return; } g_display.clearDisplay(); if (g_view == DISPLAY_VIEW_VOLUME) { drawVolumeView(); } else { switch (g_signerScreen) { case SIGNER_SCREEN_MENU: drawSignerMenu(); break; case SIGNER_SCREEN_ENTER_GRID: drawEnterMnemonic(); break; case SIGNER_SCREEN_MNEMONIC_PAGE: drawMnemonicPage(); break; case SIGNER_SCREEN_MESSAGE: drawMessage(); break; case SIGNER_SCREEN_READY: drawSignerReady(); break; default: drawMessage(); break; } } g_display.display(); } void copy_text(char *dst, size_t dst_sz, const char *src) { if (!dst || dst_sz == 0) { return; } if (!src) { dst[0] = '\0'; return; } strncpy(dst, src, dst_sz - 1); dst[dst_sz - 1] = '\0'; } } // namespace void display_init() { Wire.setClock(400000); Wire.begin(); Wire.beginTransmission(kDefaultI2cAddr); const uint8_t probeErr = Wire.endTransmission(); if (probeErr != 0) { g_displayPresent = false; return; } g_displayPresent = g_display.begin(kDefaultI2cAddr, true); if (!g_displayPresent) { return; } g_dirty = true; renderFrame(); } bool display_is_present() { return g_displayPresent; } void display_set_view(display_view_t view) { if (g_view == view) { return; } g_view = view; g_dirty = true; } void display_set_mode(bool signer_mode) { if (g_signerMode == signer_mode) { return; } g_signerMode = signer_mode; g_dirty = true; } void display_set_volume(uint8_t volume) { const uint8_t v = clampVolume(volume); if (v == g_volume) { return; } g_volume = v; g_dirty = true; } void display_show_signer_menu(uint8_t selected_index) { g_signerScreen = SIGNER_SCREEN_MENU; g_menuSelected = selected_index > 1 ? 1 : selected_index; g_dirty = true; } void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage, const char *building_text, uint8_t cursor_pos) { g_signerScreen = SIGNER_SCREEN_ENTER_GRID; g_enterActiveSlot = (active_slot > 11) ? 11 : active_slot; g_enterStage = stage; g_enterCursorPos = cursor_pos; copy_text(g_enterBuilding, sizeof(g_enterBuilding), building_text); for (uint8_t i = 0; i < 12; ++i) { const char *src = ""; if (slots && slots[i]) { src = slots[i]; } copy_text(g_enterSlots[i], sizeof(g_enterSlots[i]), src); } g_dirty = true; } void display_show_mnemonic_page(const char *mnemonic, uint8_t page_index) { g_signerScreen = SIGNER_SCREEN_MNEMONIC_PAGE; g_mnemonicPage = page_index; copy_text(g_mnemonic, sizeof(g_mnemonic), mnemonic); g_dirty = true; } void display_show_message(const char *title, const char *line1, const char *line2) { g_signerScreen = SIGNER_SCREEN_MESSAGE; copy_text(g_msgTitle, sizeof(g_msgTitle), title); copy_text(g_msgLine1, sizeof(g_msgLine1), line1); copy_text(g_msgLine2, sizeof(g_msgLine2), line2); g_dirty = true; } void display_show_signer_ready() { g_signerScreen = SIGNER_SCREEN_READY; g_dirty = true; } // Note: status-line updates are currently disabled at call sites in // kb2040_hidden_signer.ino by commenting out display_set_status(...) // invocations to keep the bottom status line from being overwritten. void display_set_status(const char *status) { if (!status) { return; } copy_text(g_status, sizeof(g_status), status); g_dirty = true; } void display_tick() { if (!g_displayPresent || !g_dirty) { return; } renderFrame(); g_dirty = false; }