## New - "Welcome" screen shown on first-time setup (before touch calibration), with the splash screen's animated radar graphic and a Start button. - The "Setup complete" screen now counts down 7 seconds with a button matching the Welcome screen's style, then automatically continues into the app (previously required a tap). - System menu "Backup & Reset" page (Backup, Restore, and a new "Reset settings" option that wipes the on-device data and re-runs first-time setup - useful for testing or handing the device to someone else). - Touch calibration targets are now green mini radar reticles instead of red crosshairs. ## Improved - The SD card's data folder is now only created after tapping Start on the Welcome screen, instead of on every boot regardless. - Flash partition layout changed to free up roughly 850 KB of firmware space (no OTA update slot needed, since firmware is always flashed via cable/USB) - lowers flash usage from ~95% to ~59%. ## Fixed - The Start button and the WiFi scanning screen now show a loading indicator instead of appearing frozen during SD-card/WiFi operations. - A touch-input bug during first-time setup that could corrupt touch calibration and cause taps to land on the wrong on-screen element afterward (most noticeable in the WiFi network selection list). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
443 lines
18 KiB
C++
443 lines
18 KiB
C++
#include "wifi_setup_screen.h"
|
|
#include "touch_input.h"
|
|
#include "wifi_manager.h"
|
|
#include "menu_stars.h"
|
|
#include "config.h"
|
|
#include "i18n.h"
|
|
|
|
namespace WifiSetupScreen {
|
|
|
|
namespace {
|
|
struct Rect {
|
|
int16_t x, y, w, h;
|
|
bool contains(int16_t px, int16_t py) const {
|
|
return px >= x && px < x + w && py >= y && py < y + h;
|
|
}
|
|
};
|
|
|
|
enum class KeyType { Char, Shift, Backspace, TogglePage, Space, Connect };
|
|
|
|
struct KeyDef {
|
|
KeyType type;
|
|
char ch;
|
|
const char* label;
|
|
};
|
|
|
|
constexpr KeyDef ROW_LETTERS_A[] = {
|
|
{KeyType::Char,'q',nullptr},{KeyType::Char,'w',nullptr},{KeyType::Char,'e',nullptr},
|
|
{KeyType::Char,'r',nullptr},{KeyType::Char,'t',nullptr},{KeyType::Char,'y',nullptr},
|
|
{KeyType::Char,'u',nullptr},{KeyType::Char,'i',nullptr},{KeyType::Char,'o',nullptr},
|
|
{KeyType::Char,'p',nullptr},
|
|
};
|
|
constexpr KeyDef ROW_LETTERS_B[] = {
|
|
{KeyType::Char,'a',nullptr},{KeyType::Char,'s',nullptr},{KeyType::Char,'d',nullptr},
|
|
{KeyType::Char,'f',nullptr},{KeyType::Char,'g',nullptr},{KeyType::Char,'h',nullptr},
|
|
{KeyType::Char,'j',nullptr},{KeyType::Char,'k',nullptr},{KeyType::Char,'l',nullptr},
|
|
};
|
|
constexpr KeyDef ROW_LETTERS_C[] = {
|
|
{KeyType::TogglePage,0,"123"},{KeyType::Shift,0,"^"},
|
|
{KeyType::Char,'z',nullptr},{KeyType::Char,'x',nullptr},{KeyType::Char,'c',nullptr},
|
|
{KeyType::Char,'v',nullptr},{KeyType::Char,'b',nullptr},{KeyType::Char,'n',nullptr},
|
|
{KeyType::Char,'m',nullptr},{KeyType::Backspace,0,"<-"},
|
|
};
|
|
|
|
constexpr KeyDef ROW_SYMBOLS_A[] = {
|
|
{KeyType::Char,'1',nullptr},{KeyType::Char,'2',nullptr},{KeyType::Char,'3',nullptr},
|
|
{KeyType::Char,'4',nullptr},{KeyType::Char,'5',nullptr},{KeyType::Char,'6',nullptr},
|
|
{KeyType::Char,'7',nullptr},{KeyType::Char,'8',nullptr},{KeyType::Char,'9',nullptr},
|
|
{KeyType::Char,'0',nullptr},
|
|
};
|
|
constexpr KeyDef ROW_SYMBOLS_B[] = {
|
|
{KeyType::Char,'-',nullptr},{KeyType::Char,'_',nullptr},{KeyType::Char,'=',nullptr},
|
|
{KeyType::Char,'+',nullptr},{KeyType::Char,':',nullptr},{KeyType::Char,';',nullptr},
|
|
{KeyType::Char,'\'',nullptr},{KeyType::Char,'"',nullptr},{KeyType::Char,'?',nullptr},
|
|
{KeyType::Char,'!',nullptr},
|
|
};
|
|
constexpr KeyDef ROW_SYMBOLS_C[] = {
|
|
{KeyType::TogglePage,0,"ABC"},
|
|
{KeyType::Char,'@',nullptr},{KeyType::Char,'#',nullptr},{KeyType::Char,'$',nullptr},
|
|
{KeyType::Char,'%',nullptr},{KeyType::Char,'&',nullptr},{KeyType::Char,'*',nullptr},
|
|
{KeyType::Char,'(',nullptr},{KeyType::Char,')',nullptr},{KeyType::Backspace,0,"<-"},
|
|
};
|
|
|
|
constexpr int16_t KB_TOP = 132;
|
|
constexpr int16_t ROW_H = 34;
|
|
constexpr int16_t ROW_GAP = 4;
|
|
constexpr int16_t KEY_GAP = 3;
|
|
constexpr int16_t SIDE_MARGIN = 4;
|
|
|
|
enum class Stage { Scanning, PickSsid, EnterPassword, Connecting, Done };
|
|
Stage stage = Stage::Scanning;
|
|
|
|
constexpr uint8_t MAX_LIST = 16;
|
|
constexpr uint8_t VISIBLE_ITEMS = 7;
|
|
String ssidList[MAX_LIST];
|
|
uint8_t ssidCount = 0;
|
|
int8_t selectedIndex = -1;
|
|
uint8_t scrollOffset = 0;
|
|
|
|
char passwordBuf[64] = {0};
|
|
uint8_t passwordLen = 0;
|
|
|
|
enum class ShiftState { Off, OneShot, Locked };
|
|
ShiftState shiftState = ShiftState::Off;
|
|
uint32_t lastShiftTapMs = 0;
|
|
constexpr uint32_t SHIFT_DOUBLE_TAP_MS = 400;
|
|
uint8_t page = 0;
|
|
|
|
bool skipped = false;
|
|
bool connectSucceeded = false;
|
|
bool needsRedraw = true;
|
|
|
|
Rect cancelBtn = {Config::SCREEN_WIDTH - 34, 4, 30, 24};
|
|
|
|
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label, bool highlighted = false) {
|
|
uint16_t bg = highlighted ? TFT_GREEN : TFT_BLACK;
|
|
uint16_t fg = highlighted ? TFT_BLACK : TFT_GREEN;
|
|
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, bg);
|
|
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_GREEN);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(fg, bg);
|
|
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
|
tft.setTextDatum(TL_DATUM);
|
|
}
|
|
|
|
template <size_t N>
|
|
void layoutRow(const KeyDef (&row)[N], int16_t y, Rect outRects[N]) {
|
|
int16_t usableW = Config::SCREEN_WIDTH - 2 * SIDE_MARGIN;
|
|
int16_t keyW = (usableW - (int16_t)(N - 1) * KEY_GAP) / (int16_t)N;
|
|
int16_t x = SIDE_MARGIN;
|
|
for (size_t i = 0; i < N; i++) {
|
|
outRects[i] = {x, y, keyW, ROW_H};
|
|
x += keyW + KEY_GAP;
|
|
}
|
|
}
|
|
|
|
String keyLabel(const KeyDef& k) {
|
|
if (k.label) return String(k.label);
|
|
char c = (shiftState != ShiftState::Off) ? (char)toupper(k.ch) : k.ch;
|
|
return String(c);
|
|
}
|
|
|
|
template <size_t N>
|
|
bool handleRowTap(const KeyDef (&row)[N], int16_t y, int16_t tx, int16_t ty) {
|
|
Rect rects[N];
|
|
layoutRow(row, y, rects);
|
|
for (size_t i = 0; i < N; i++) {
|
|
if (!rects[i].contains(tx, ty)) continue;
|
|
const KeyDef& k = row[i];
|
|
switch (k.type) {
|
|
case KeyType::Char:
|
|
if (passwordLen < sizeof(passwordBuf) - 1) {
|
|
bool upper = (shiftState != ShiftState::Off);
|
|
passwordBuf[passwordLen++] = upper ? (char)toupper(k.ch) : k.ch;
|
|
passwordBuf[passwordLen] = 0;
|
|
if (shiftState == ShiftState::OneShot) shiftState = ShiftState::Off;
|
|
}
|
|
break;
|
|
case KeyType::Shift: {
|
|
uint32_t nowMs = millis();
|
|
bool isDoubleTap = (nowMs - lastShiftTapMs) <= SHIFT_DOUBLE_TAP_MS;
|
|
lastShiftTapMs = nowMs;
|
|
if (isDoubleTap) {
|
|
shiftState = (shiftState == ShiftState::Locked) ? ShiftState::Off : ShiftState::Locked;
|
|
} else if (shiftState == ShiftState::Off) {
|
|
shiftState = ShiftState::OneShot;
|
|
} else {
|
|
shiftState = ShiftState::Off;
|
|
}
|
|
break;
|
|
}
|
|
case KeyType::Backspace:
|
|
if (passwordLen > 0) {
|
|
passwordLen--;
|
|
passwordBuf[passwordLen] = 0;
|
|
}
|
|
break;
|
|
case KeyType::TogglePage:
|
|
page = page == 0 ? 1 : 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <size_t N>
|
|
void renderRow(TFT_eSPI& tft, const KeyDef (&row)[N], int16_t y) {
|
|
Rect rects[N];
|
|
layoutRow(row, y, rects);
|
|
for (size_t i = 0; i < N; i++) {
|
|
bool hl = (row[i].type == KeyType::Shift && shiftState != ShiftState::Off);
|
|
drawButton(tft, rects[i], keyLabel(row[i]), hl);
|
|
}
|
|
}
|
|
|
|
void resetKeyboardState() {
|
|
passwordLen = 0;
|
|
passwordBuf[0] = 0;
|
|
shiftState = ShiftState::Off;
|
|
page = 0;
|
|
}
|
|
|
|
void drawCancelButton(TFT_eSPI& tft) {
|
|
drawButton(tft, cancelBtn, "X");
|
|
}
|
|
|
|
// Small loading indicator ("."/".."/"...") during the WiFi scan (which
|
|
// can take several seconds) - without visible feedback the screen
|
|
// looked "frozen" during that time and people tapped impatiently,
|
|
// which (see comment at the end of the Scanning->PickSsid transition
|
|
// below) could cause mistaps in the freshly shown network list.
|
|
uint8_t scanDotPhase = 0;
|
|
uint32_t lastScanDotMs = 0;
|
|
constexpr uint32_t SCAN_DOT_INTERVAL_MS = 350;
|
|
|
|
void updateScanningDots(TFT_eSPI& tft) {
|
|
uint32_t now = millis();
|
|
if (now - lastScanDotMs < SCAN_DOT_INTERVAL_MS) return;
|
|
lastScanDotMs = now;
|
|
scanDotPhase = (uint8_t)((scanDotPhase + 1) % 4);
|
|
tft.fillRect(10, 26, 40, 12, TFT_BLACK);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 26);
|
|
for (uint8_t i = 0; i < scanDotPhase; i++) tft.print(".");
|
|
}
|
|
}
|
|
|
|
bool run(TFT_eSPI& tft) {
|
|
MenuStars::reset();
|
|
stage = Stage::Scanning;
|
|
skipped = false;
|
|
connectSucceeded = false;
|
|
ssidCount = 0;
|
|
selectedIndex = -1;
|
|
scrollOffset = 0;
|
|
resetKeyboardState();
|
|
needsRedraw = true;
|
|
scanDotPhase = 0;
|
|
lastScanDotMs = 0;
|
|
|
|
WifiMgr::beginScan();
|
|
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setTextSize(1);
|
|
tft.setCursor(10, 14);
|
|
tft.println(I18n::t(StringId::WIFI_SCANNING));
|
|
drawCancelButton(tft);
|
|
|
|
while (!skipped) {
|
|
TouchInput::Point tap;
|
|
bool tapped = TouchInput::wasTapped(tap);
|
|
|
|
if (tapped && cancelBtn.contains(tap.x, tap.y)) {
|
|
skipped = true;
|
|
break;
|
|
}
|
|
|
|
if (stage == Stage::Scanning && WifiMgr::isScanComplete()) {
|
|
ssidCount = (uint8_t)min((int)WifiMgr::getScanResultCount(), (int)MAX_LIST);
|
|
for (uint8_t i = 0; i < ssidCount; i++) ssidList[i] = WifiMgr::getScanResultSSID(i);
|
|
stage = Stage::PickSsid;
|
|
needsRedraw = true;
|
|
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.println(ssidCount == 0 ? I18n::t(StringId::WIFI_NO_NETWORKS) : I18n::t(StringId::WIFI_SELECT));
|
|
drawCancelButton(tft);
|
|
|
|
// "tapped"/"tap" were captured for the previous state
|
|
// (Stage::Scanning) - without discarding it, a tap that
|
|
// happened to land exactly in the frame the scan finished
|
|
// would immediately be evaluated below against the
|
|
// BRAND-NEW (and, to the user, not yet visible) network list
|
|
// - at a position the user never intentionally tapped. That
|
|
// is what let impatient repeated tapping during the scan
|
|
// eventually pick a "random" wrong network.
|
|
tapped = false;
|
|
}
|
|
|
|
if (stage == Stage::Connecting) {
|
|
WifiMgr::update();
|
|
if (WifiMgr::getState() == WifiMgr::State::Connected) {
|
|
connectSucceeded = true;
|
|
WifiMgr::addNetwork(ssidList[selectedIndex].c_str(), passwordBuf);
|
|
stage = Stage::Done;
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.println(I18n::t(StringId::WIFI_CONNECTED_BANG));
|
|
delay(900);
|
|
return true;
|
|
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
|
|
stage = Stage::Done;
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.setTextColor(TFT_RED, TFT_BLACK);
|
|
tft.println(I18n::t(StringId::WIFI_CONNECTION_FAILED));
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 30);
|
|
tft.println(I18n::t(StringId::WIFI_BACK_TO_LIST_MSG));
|
|
delay(1400);
|
|
stage = Stage::PickSsid;
|
|
WifiMgr::beginScan();
|
|
stage = Stage::Scanning;
|
|
}
|
|
}
|
|
|
|
if (tapped && stage == Stage::PickSsid) {
|
|
if (ssidCount > 0) {
|
|
for (uint8_t row = 0; row < VISIBLE_ITEMS; row++) {
|
|
uint8_t idx = scrollOffset + row;
|
|
if (idx >= ssidCount) break;
|
|
Rect r = {10, (int16_t)(34 + row * 34), (int16_t)(Config::SCREEN_WIDTH - 20), 30};
|
|
if (r.contains(tap.x, tap.y)) {
|
|
selectedIndex = idx;
|
|
resetKeyboardState();
|
|
stage = Stage::EnterPassword;
|
|
needsRedraw = true;
|
|
// Prevents this same tap (on a network name) from
|
|
// also being evaluated against the password
|
|
// keyboard further down in the same pass - "stage"
|
|
// is already EnterPassword at this point, and the
|
|
// network list rows and keyboard key rows
|
|
// partially overlap in Y, so without this a
|
|
// network selection could immediately also type a
|
|
// stray character.
|
|
tapped = false;
|
|
}
|
|
}
|
|
if (ssidCount > VISIBLE_ITEMS) {
|
|
Rect upBtn = {Config::SCREEN_WIDTH - 34, 34, 30, 26};
|
|
Rect downBtn = {Config::SCREEN_WIDTH - 34, 64 + (VISIBLE_ITEMS - 1) * 34, 30, 26};
|
|
if (upBtn.contains(tap.x, tap.y) && scrollOffset > 0) { scrollOffset--; needsRedraw = true; }
|
|
if (downBtn.contains(tap.x, tap.y) && scrollOffset + VISIBLE_ITEMS < ssidCount) { scrollOffset++; needsRedraw = true; }
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tapped && stage == Stage::EnterPassword) {
|
|
int16_t rowY0 = KB_TOP;
|
|
int16_t rowY1 = KB_TOP + (ROW_H + ROW_GAP);
|
|
int16_t rowY2 = KB_TOP + 2 * (ROW_H + ROW_GAP);
|
|
int16_t rowYFn = KB_TOP + 3 * (ROW_H + ROW_GAP);
|
|
|
|
bool handled = false;
|
|
if (page == 0) {
|
|
handled = handleRowTap(ROW_LETTERS_A, rowY0, tap.x, tap.y) ||
|
|
handleRowTap(ROW_LETTERS_B, rowY1, tap.x, tap.y) ||
|
|
handleRowTap(ROW_LETTERS_C, rowY2, tap.x, tap.y);
|
|
} else {
|
|
handled = handleRowTap(ROW_SYMBOLS_A, rowY0, tap.x, tap.y) ||
|
|
handleRowTap(ROW_SYMBOLS_B, rowY1, tap.x, tap.y) ||
|
|
handleRowTap(ROW_SYMBOLS_C, rowY2, tap.x, tap.y);
|
|
}
|
|
|
|
if (!handled) {
|
|
Rect spaceBtn = {SIDE_MARGIN, rowYFn, 150, ROW_H};
|
|
Rect connectBtn = {SIDE_MARGIN + 154, rowYFn, Config::SCREEN_WIDTH - 2*SIDE_MARGIN - 154, ROW_H};
|
|
Rect backBtn = {SIDE_MARGIN, (int16_t)(rowYFn + ROW_H + ROW_GAP), (int16_t)(Config::SCREEN_WIDTH - 2*SIDE_MARGIN), 28};
|
|
|
|
if (spaceBtn.contains(tap.x, tap.y)) {
|
|
if (passwordLen < sizeof(passwordBuf) - 1) {
|
|
passwordBuf[passwordLen++] = ' ';
|
|
passwordBuf[passwordLen] = 0;
|
|
}
|
|
} else if (connectBtn.contains(tap.x, tap.y)) {
|
|
if (WifiMgr::networkCount() >= Config::MAX_WIFI_NETWORKS) {
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.setTextColor(TFT_RED, TFT_BLACK);
|
|
tft.println(I18n::t(StringId::WIFI_ALREADY_3));
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 30);
|
|
tft.println(I18n::t(StringId::WIFI_REMOVE_ONE_FIRST));
|
|
delay(1600);
|
|
skipped = true;
|
|
break;
|
|
}
|
|
WifiMgr::connectTo(ssidList[selectedIndex].c_str(), passwordBuf);
|
|
stage = Stage::Connecting;
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.println(I18n::t(StringId::WIFI_CONNECTING));
|
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
|
stage = Stage::PickSsid;
|
|
needsRedraw = true;
|
|
}
|
|
}
|
|
needsRedraw = true;
|
|
}
|
|
|
|
if (!needsRedraw) {
|
|
if (stage == Stage::Scanning) updateScanningDots(tft);
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
continue;
|
|
}
|
|
needsRedraw = false;
|
|
|
|
if (stage == Stage::PickSsid) {
|
|
tft.fillRect(0, 30, Config::SCREEN_WIDTH, Config::SCREEN_HEIGHT - 30, TFT_BLACK);
|
|
for (uint8_t row = 0; row < VISIBLE_ITEMS; row++) {
|
|
uint8_t idx = scrollOffset + row;
|
|
if (idx >= ssidCount) break;
|
|
Rect r = {10, (int16_t)(34 + row * 34), (int16_t)(Config::SCREEN_WIDTH - 20), 30};
|
|
drawButton(tft, r, ssidList[idx]);
|
|
}
|
|
if (ssidCount > VISIBLE_ITEMS) {
|
|
Rect upBtn = {Config::SCREEN_WIDTH - 34, 34, 30, 26};
|
|
Rect downBtn = {Config::SCREEN_WIDTH - 34, 64 + (VISIBLE_ITEMS - 1) * 34, 30, 26};
|
|
drawButton(tft, upBtn, "^");
|
|
drawButton(tft, downBtn, "v");
|
|
}
|
|
drawCancelButton(tft);
|
|
} else if (stage == Stage::EnterPassword) {
|
|
tft.fillRect(0, 0, Config::SCREEN_WIDTH, KB_TOP - 4, TFT_BLACK);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.printf("%s%s", I18n::t(StringId::WIFI_LABEL_PREFIX), ssidList[selectedIndex].c_str());
|
|
tft.setCursor(10, 22);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.println(I18n::t(StringId::WIFI_PASSWORD_LABEL));
|
|
tft.fillRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_BLACK);
|
|
tft.drawRect(8, 38, Config::SCREEN_WIDTH - 16, 22, TFT_GREEN);
|
|
tft.setCursor(12, 56);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.print(passwordBuf);
|
|
|
|
int16_t rowY0 = KB_TOP;
|
|
int16_t rowY1 = KB_TOP + (ROW_H + ROW_GAP);
|
|
int16_t rowY2 = KB_TOP + 2 * (ROW_H + ROW_GAP);
|
|
int16_t rowYFn = KB_TOP + 3 * (ROW_H + ROW_GAP);
|
|
|
|
if (page == 0) {
|
|
renderRow(tft, ROW_LETTERS_A, rowY0);
|
|
renderRow(tft, ROW_LETTERS_B, rowY1);
|
|
renderRow(tft, ROW_LETTERS_C, rowY2);
|
|
} else {
|
|
renderRow(tft, ROW_SYMBOLS_A, rowY0);
|
|
renderRow(tft, ROW_SYMBOLS_B, rowY1);
|
|
renderRow(tft, ROW_SYMBOLS_C, rowY2);
|
|
}
|
|
|
|
Rect spaceBtn = {SIDE_MARGIN, rowYFn, 150, ROW_H};
|
|
Rect connectBtn = {SIDE_MARGIN + 154, rowYFn, Config::SCREEN_WIDTH - 2*SIDE_MARGIN - 154, ROW_H};
|
|
Rect backBtn = {SIDE_MARGIN, (int16_t)(rowYFn + ROW_H + ROW_GAP), (int16_t)(Config::SCREEN_WIDTH - 2*SIDE_MARGIN), 28};
|
|
drawButton(tft, spaceBtn, I18n::t(StringId::WIFI_SPACE));
|
|
drawButton(tft, connectBtn, I18n::t(StringId::WIFI_CONNECT));
|
|
drawButton(tft, backBtn, I18n::t(StringId::WIFI_BACK_TO_LIST_BTN));
|
|
}
|
|
|
|
delay(20);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
} |