384 lines
8.8 KiB
C++
384 lines
8.8 KiB
C++
#include "display.h"
|
|
|
|
#include <Arduino.h>
|
|
#include <Wire.h>
|
|
|
|
#include <Adafruit_GFX.h>
|
|
#include <Adafruit_SSD1327.h>
|
|
|
|
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_wordSlot = 0;
|
|
uint8_t g_wordTotal = 12;
|
|
uint16_t g_wordIndex = 0;
|
|
char g_word[16] = "abandon";
|
|
uint8_t g_mnemonicPage = 0;
|
|
char g_mnemonic[256] = "";
|
|
char g_msgTitle[18] = "";
|
|
char g_msgLine1[22] = "";
|
|
char g_msgLine2[22] = "";
|
|
|
|
enum signer_screen_t {
|
|
SIGNER_SCREEN_MENU = 0,
|
|
SIGNER_SCREEN_WORD_PICK = 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 drawVolumeView() {
|
|
g_display.setTextWrap(false);
|
|
g_display.setTextSize(1);
|
|
g_display.setTextColor(kGrayDim);
|
|
g_display.setCursor(6, 4);
|
|
g_display.print(g_signerMode ? "MODE: SIGNER" : "MODE: MEDIA");
|
|
|
|
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<int16_t>((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<int16_t>((static_cast<uint32_t>(barW - 2) * g_volume) / 100U);
|
|
if (fillW > 0) {
|
|
g_display.fillRect(barX + 1, barY + 1, fillW, barH - 2, kGrayBar);
|
|
}
|
|
|
|
drawStatusLine();
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
drawStatusLine();
|
|
}
|
|
|
|
void drawWordPick() {
|
|
g_display.setTextWrap(false);
|
|
g_display.setTextSize(1);
|
|
g_display.setTextColor(kGrayDim);
|
|
g_display.setCursor(2, 2);
|
|
char hdr[20];
|
|
snprintf(hdr, sizeof(hdr), "WORD %u/%u", (unsigned)(g_wordSlot + 1), (unsigned)g_wordTotal);
|
|
g_display.print(hdr);
|
|
|
|
char idxBuf[20];
|
|
snprintf(idxBuf, sizeof(idxBuf), "IDX %u", (unsigned)g_wordIndex);
|
|
g_display.setCursor(82, 2);
|
|
g_display.print(idxBuf);
|
|
|
|
g_display.drawRect(4, 24, 120, 56, kGrayDim);
|
|
g_display.setTextSize(2);
|
|
g_display.setTextColor(kGrayText);
|
|
g_display.setCursor(10, 44);
|
|
g_display.print(g_word);
|
|
|
|
g_display.setTextSize(1);
|
|
g_display.setCursor(6, 88);
|
|
g_display.print("ENC scroll PLAY select");
|
|
g_display.setCursor(6, 98);
|
|
g_display.print("NEXT back");
|
|
|
|
drawStatusLine();
|
|
}
|
|
|
|
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';
|
|
|
|
const uint8_t perPage = 4;
|
|
const uint8_t startWord = g_mnemonicPage * perPage;
|
|
const uint8_t endWord = startWord + perPage;
|
|
|
|
uint8_t wordNum = 0;
|
|
char *saveptr = nullptr;
|
|
char *tok = strtok_r(copy, " ", &saveptr);
|
|
uint8_t line = 0;
|
|
while (tok) {
|
|
if (wordNum >= startWord && wordNum < endWord) {
|
|
char lineBuf[32];
|
|
snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(wordNum + 1), tok);
|
|
g_display.setTextColor(kGrayText);
|
|
g_display.setCursor(8, 24 + line * 18);
|
|
g_display.print(lineBuf);
|
|
line++;
|
|
}
|
|
wordNum++;
|
|
tok = strtok_r(nullptr, " ", &saveptr);
|
|
}
|
|
|
|
g_display.setTextSize(1);
|
|
g_display.setCursor(6, 98);
|
|
g_display.print("ENC page PLAY confirm");
|
|
g_display.setCursor(6, 108);
|
|
g_display.print("NEXT cancel");
|
|
|
|
drawStatusLine();
|
|
}
|
|
|
|
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);
|
|
|
|
drawStatusLine();
|
|
}
|
|
|
|
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");
|
|
|
|
g_display.setTextSize(1);
|
|
g_display.setCursor(6, 96);
|
|
g_display.print("NEXT/PLAY: menu");
|
|
|
|
drawStatusLine();
|
|
}
|
|
|
|
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_WORD_PICK:
|
|
drawWordPick();
|
|
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_word_pick(uint8_t slot_index, uint8_t total_slots, const char *word, uint16_t word_index) {
|
|
g_signerScreen = SIGNER_SCREEN_WORD_PICK;
|
|
g_wordSlot = slot_index;
|
|
g_wordTotal = total_slots;
|
|
g_wordIndex = word_index;
|
|
copy_text(g_word, sizeof(g_word), word);
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|