Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
25 kB · 700 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701#include "WifiSelectionActivity.h"
#include <GfxRenderer.h>#include <I18n.h>#include <Logging.h>#include <WiFi.h>
#include <map>
#include "MappedInputManager.h"#include "WifiCredentialStore.h"#include "activities/util/KeyboardEntryActivity.h"#include "components/UITheme.h"#include "fontIds.h"
void WifiSelectionActivity::onEnter() { Activity::onEnter();
// Load saved WiFi credentials - SD card operations need lock as we use SPI // for both { RenderLock lock(*this); WIFI_STORE.loadFromFile(); }
// Reset state selectedNetworkIndex = 0; networks.clear(); state = WifiSelectionState::SCANNING; selectedSSID.clear(); connectedIP.clear(); connectionError.clear(); enteredPassword.clear(); usedSavedPassword = false; savePromptSelection = 0; forgetPromptSelection = 0; autoConnecting = false;
// Cache MAC address for display uint8_t mac[6]; WiFi.macAddress(mac); char macStr[64]; snprintf(macStr, sizeof(macStr), "%s %02x-%02x-%02x-%02x-%02x-%02x", tr(STR_MAC_ADDRESS), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); cachedMacAddress = std::string(macStr);
// Trigger first update to show scanning message requestUpdate();
// Attempt to auto-connect to the last network if (allowAutoConnect) { const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); if (!lastSsid.empty()) { const auto* cred = WIFI_STORE.findCredential(lastSsid); if (cred) { LOG_DBG("WIFI", "Attempting to auto-connect to %s", lastSsid.c_str()); selectedSSID = cred->ssid; enteredPassword = cred->password; selectedRequiresPassword = !cred->password.empty(); usedSavedPassword = true; autoConnecting = true; attemptConnection(); requestUpdate(); return; } } }
// Fallback to scanning startWifiScan();}
void WifiSelectionActivity::onExit() { Activity::onExit();
LOG_DBG("WIFI", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
// Stop any ongoing WiFi scan LOG_DBG("WIFI", "Deleting WiFi scan..."); WiFi.scanDelete(); LOG_DBG("WIFI", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap());
// Note: We do NOT disconnect WiFi here - the parent activity // (CrossPointWebServerActivity) manages WiFi connection state. We just clean // up the scan and task.
LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());}
void WifiSelectionActivity::startWifiScan() { autoConnecting = false; state = WifiSelectionState::SCANNING; networks.clear(); requestUpdate();
// Set WiFi mode to station WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100);
// Start async scan WiFi.scanNetworks(true); // true = async scan}
void WifiSelectionActivity::processWifiScanResults() { const int16_t scanResult = WiFi.scanComplete();
if (scanResult == WIFI_SCAN_RUNNING) { // Scan still in progress return; }
if (scanResult == WIFI_SCAN_FAILED) { state = WifiSelectionState::NETWORK_LIST; requestUpdate(); return; }
// Scan complete, process results // Use a map to deduplicate networks by SSID, keeping the strongest signal std::map<std::string, WifiNetworkInfo> uniqueNetworks;
for (int i = 0; i < scanResult; i++) { std::string ssid = WiFi.SSID(i).c_str(); const int32_t rssi = WiFi.RSSI(i);
// Skip hidden networks (empty SSID) if (ssid.empty()) { continue; }
// Check if we've already seen this SSID auto it = uniqueNetworks.find(ssid); if (it == uniqueNetworks.end() || rssi > it->second.rssi) { // New network or stronger signal than existing entry WifiNetworkInfo network; network.ssid = ssid; network.rssi = rssi; network.isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid); uniqueNetworks[ssid] = network; } }
// Convert map to vector networks.clear(); for (const auto& pair : uniqueNetworks) { // cppcheck-suppress useStlAlgorithm networks.push_back(pair.second); }
// Sort: saved-password networks first, then by signal strength (strongest first) std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { if (a.hasSavedPassword != b.hasSavedPassword) { return a.hasSavedPassword; } return a.rssi > b.rssi; });
WiFi.scanDelete(); state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate();}
void WifiSelectionActivity::selectNetwork(const int index) { if (index < 0 || index >= static_cast<int>(networks.size())) { return; }
const auto& network = networks[index]; selectedSSID = network.ssid; selectedRequiresPassword = network.isEncrypted; usedSavedPassword = false; enteredPassword.clear(); autoConnecting = false;
// Check if we have saved credentials for this network const auto* savedCred = WIFI_STORE.findCredential(selectedSSID); if (savedCred && !savedCred->password.empty()) { // Use saved password - connect directly enteredPassword = savedCred->password; usedSavedPassword = true; LOG_DBG("WiFi", "Using saved password for %s, length: %zu", selectedSSID.c_str(), enteredPassword.size()); attemptConnection(); return; }
if (selectedRequiresPassword) { // Show password entry state = WifiSelectionState::PASSWORD_ENTRY; // Don't allow screen updates while changing activity startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), "", // No initial text 64, // Max password length InputType::Password), [this](const ActivityResult& result) { if (result.isCancelled) { state = WifiSelectionState::NETWORK_LIST; } else { enteredPassword = std::get<KeyboardResult>(result.data).text; // state will be updated in next loop iteration } }); } else { // Connect directly for open networks attemptConnection(); }}
void WifiSelectionActivity::attemptConnection() { state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; connectionStartTime = millis(); connectedIP.clear(); connectionError.clear(); requestUpdate();
WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect WiFi.mode(WIFI_STA); WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID delay(100);
// Set hostname so routers show "CrossPoint-Reader-AABBCCDDEEFF" instead of "esp32-XXXXXXXXXXXX" String mac = WiFi.macAddress(); mac.replace(":", ""); String hostname = "CrossPoint-Reader-" + mac; WiFi.setHostname(hostname.c_str());
if (selectedRequiresPassword && !enteredPassword.empty()) { WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); } else { WiFi.begin(selectedSSID.c_str()); }}
void WifiSelectionActivity::checkConnectionStatus() { if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) { return; }
const wl_status_t status = WiFi.status();
if (status == WL_CONNECTED) { // Successfully connected IPAddress ip = WiFi.localIP(); char ipStr[16]; snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); connectedIP = ipStr; autoConnecting = false;
// Save this as the last connected network - SD card operations need lock as // we use SPI for both { RenderLock lock(*this); WIFI_STORE.setLastConnectedSsid(selectedSSID); }
// If we entered a new password, ask if user wants to save it // Otherwise, immediately complete so parent can start web server if (!usedSavedPassword && !enteredPassword.empty()) { state = WifiSelectionState::SAVE_PROMPT; savePromptSelection = 0; // Default to "Yes" requestUpdate(); } else { // Using saved password or open network - complete immediately LOG_DBG("WIFI", "Connected with saved/open credentials, " "completing immediately"); onComplete(true); } return; }
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { connectionError = tr(STR_ERROR_GENERAL_FAILURE); if (status == WL_NO_SSID_AVAIL) { connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND); } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; }
// Check for timeout if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { WiFi.disconnect(); connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; }}
void WifiSelectionActivity::loop() { // Check scan progress if (state == WifiSelectionState::SCANNING) { processWifiScanResults(); return; }
// Check connection progress if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { checkConnectionStatus(); return; }
if (state == WifiSelectionState::PASSWORD_ENTRY) { // Reach here once password entry finished in subactivity attemptConnection(); return; }
// Handle save prompt state if (state == WifiSelectionState::SAVE_PROMPT) { if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { if (savePromptSelection > 0) { savePromptSelection--; requestUpdate(); } } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { if (savePromptSelection < 1) { savePromptSelection++; requestUpdate(); } } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (savePromptSelection == 0) { // User chose "Yes" - save the password RenderLock lock(*this); WIFI_STORE.addCredential(selectedSSID, enteredPassword); } // Complete - parent will start web server onComplete(true); } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { // Skip saving, complete anyway onComplete(true); } return; }
// Handle forget prompt state (connection failed with saved credentials) if (state == WifiSelectionState::FORGET_PROMPT) { if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { if (forgetPromptSelection > 0) { forgetPromptSelection--; requestUpdate(); } } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { if (forgetPromptSelection < 1) { forgetPromptSelection++; requestUpdate(); } } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (forgetPromptSelection == 1) { RenderLock lock(*this); // User chose "Forget network" - forget the network WIFI_STORE.removeCredential(selectedSSID); // Update the network list to reflect the change const auto network = find_if(networks.begin(), networks.end(), [this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; }); if (network != networks.end()) { network->hasSavedPassword = false; } } // Go back to network list (whether Cancel or Forget network was selected) startWifiScan(); } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { // Skip forgetting, go back to network list startWifiScan(); } return; }
// Handle connected state (should not normally be reached - connection // completes immediately) if (state == WifiSelectionState::CONNECTED) { // Safety fallback - immediately complete onComplete(true); return; }
// Handle connection failed state if (state == WifiSelectionState::CONNECTION_FAILED) { if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { // If we were auto-connecting or using a saved credential, offer to forget // the network if (autoConnecting || usedSavedPassword) { autoConnecting = false; state = WifiSelectionState::FORGET_PROMPT; forgetPromptSelection = 0; // Default to "Cancel" } else { // Go back to network list on failure for non-saved credentials state = WifiSelectionState::NETWORK_LIST; } requestUpdate(); return; } }
// Handle network list state if (state == WifiSelectionState::NETWORK_LIST) { // Check for Back button to exit (cancel) if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onComplete(false); return; }
// Check for Confirm button to select network or rescan if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (!networks.empty()) { selectNetwork(selectedNetworkIndex); } else { startWifiScan(); } return; }
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { startWifiScan(); return; }
const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left); if (leftPressed) { const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; if (hasSavedPassword) { selectedSSID = networks[selectedNetworkIndex].ssid; state = WifiSelectionState::FORGET_PROMPT; forgetPromptSelection = 0; // Default to "Cancel" requestUpdate(); return; } }
// Handle navigation buttonNavigator.onNext([this] { selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); requestUpdate(); });
buttonNavigator.onPrevious([this] { selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); requestUpdate(); }); }}
std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi) const { // Convert RSSI to signal bars representation if (rssi >= -50) { return "||||"; // Excellent } if (rssi >= -60) { return " |||"; // Good } if (rssi >= -70) { return " ||"; // Fair } return " |"; // Very weak}
void WifiSelectionActivity::render(RenderLock&&) { // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning // from the keyboard subactivity back to the main activity if (state == WifiSelectionState::PASSWORD_ENTRY) { return; }
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight();
// Draw header char countStr[32]; snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_WIFI_NETWORKS), countStr); GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, cachedMacAddress.c_str());
switch (state) { case WifiSelectionState::AUTO_CONNECTING: renderConnecting(); break; case WifiSelectionState::SCANNING: renderConnecting(); // Reuse connecting screen with different message break; case WifiSelectionState::NETWORK_LIST: renderNetworkList(); break; case WifiSelectionState::CONNECTING: renderConnecting(); break; case WifiSelectionState::CONNECTED: renderConnected(); break; case WifiSelectionState::SAVE_PROMPT: renderSavePrompt(); break; case WifiSelectionState::CONNECTION_FAILED: renderConnectionFailed(); break; case WifiSelectionState::FORGET_PROMPT: renderForgetPrompt(); break; }
renderer.displayBuffer();}
void WifiSelectionActivity::renderNetworkList() const { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight();
if (networks.empty()) { // No networks found or scan failed const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height) / 2; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); } else { int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(networks.size()), selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr, [this](int index) { auto network = networks[index]; return std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") + getSignalStrengthIndicator(network.rssi); }); }
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, tr(STR_NETWORK_LEGEND));
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : "";
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_CONNECT), forgetLabel, tr(STR_RETRY)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);}
void WifiSelectionActivity::renderConnecting() const { const auto pageHeight = renderer.getScreenHeight(); const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height) / 2;
if (state == WifiSelectionState::SCANNING) { renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_SCANNING)); } else { renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_CONNECTING), true, EpdFontFamily::BOLD);
std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID; if (ssidInfo.length() > 25) { ssidInfo.replace(22, ssidInfo.length() - 22, "..."); } renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); }}
void WifiSelectionActivity::renderConnected() const { const auto pageHeight = renderer.getScreenHeight(); const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 4) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 30, tr(STR_CONNECTED), true, EpdFontFamily::BOLD);
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); } renderer.drawCenteredText(UI_10_FONT_ID, top + 10, ssidInfo.c_str());
const std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP; renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str());
// Use centralized button hints const auto labels = mappedInput.mapLabels("", tr(STR_DONE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);}
void WifiSelectionActivity::renderSavePrompt() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_CONNECTED), true, EpdFontFamily::BOLD);
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); } renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, tr(STR_SAVE_PASSWORD));
// Draw Yes/No buttons const int buttonY = top + 80; constexpr int buttonWidth = 60; constexpr int buttonSpacing = 30; constexpr int totalWidth = buttonWidth * 2 + buttonSpacing; const int startX = (pageWidth - totalWidth) / 2;
// Draw "Yes" button if (savePromptSelection == 0) { std::string text = "[" + std::string(tr(STR_YES)) + "]"; renderer.drawText(UI_10_FONT_ID, startX, buttonY, text.c_str()); } else { renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, tr(STR_YES)); }
// Draw "No" button if (savePromptSelection == 1) { std::string text = "[" + std::string(tr(STR_NO)) + "]"; renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, text.c_str()); } else { renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, tr(STR_NO)); }
// Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);}
void WifiSelectionActivity::renderConnectionFailed() const { const auto pageHeight = renderer.getScreenHeight(); const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 2) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str());
// Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);}
void WifiSelectionActivity::renderForgetPrompt() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_FORGET_NETWORK), true, EpdFontFamily::BOLD);
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); } renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, tr(STR_FORGET_AND_REMOVE));
// Draw Cancel/Forget network buttons const int buttonY = top + 80; constexpr int buttonWidth = 120; constexpr int buttonSpacing = 30; constexpr int totalWidth = buttonWidth * 2 + buttonSpacing; const int startX = (pageWidth - totalWidth) / 2;
// Draw "Cancel" button if (forgetPromptSelection == 0) { std::string text = "[" + std::string(tr(STR_CANCEL)) + "]"; renderer.drawText(UI_10_FONT_ID, startX, buttonY, text.c_str()); } else { renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, tr(STR_CANCEL)); }
// Draw "Forget network" button if (forgetPromptSelection == 1) { std::string text = "[" + std::string(tr(STR_FORGET_BUTTON)) + "]"; renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, text.c_str()); } else { renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, tr(STR_FORGET_BUTTON)); }
// Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);}
void WifiSelectionActivity::onComplete(const bool connected) { ActivityResult result; result.isCancelled = !connected; if (connected) { result.data = WifiResult{true, selectedSSID, connectedIP}; } setResult(std::move(result)); finish();}