#include "WifiSelectionActivity.h" #include #include #include #include #include #include #include "CrossPointSettings.h" #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; manualNetworkListRequested = false; autoAttemptedSsids.clear(); autoAttemptedSsids.reserve(WIFI_STORE.getCredentials().size()); // 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 known networks. Try the last successful // network first for speed, then scan and try any visible saved networks by // signal strength. The user can interrupt this and show the scan result. if (allowAutoConnect && !WIFI_STORE.getCredentials().empty()) { const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); if (!lastSsid.empty()) { const auto* cred = WIFI_STORE.findCredential(lastSsid); if (cred && tryAutoConnectCredential(*cred)) { return; } } startWifiScan(true); 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(const bool autoScan) { autoConnecting = autoScan; manualNetworkListRequested = 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) { autoConnecting = false; manualNetworkListRequested = false; state = WifiSelectionState::NETWORK_LIST; requestUpdate(); return; } // Scan complete, process results — deduplicate in-place, keeping strongest signal networks.clear(); networks.reserve(scanResult); for (int i = 0; i < scanResult; i++) { char ssid[33]; strlcpy(ssid, WiFi.SSID(i).c_str(), sizeof(ssid)); const int32_t rssi = WiFi.RSSI(i); // Skip hidden networks (empty SSID) if (ssid[0] == '\0') { continue; } auto it = std::find_if(networks.begin(), networks.end(), [&ssid](const WifiNetworkInfo& n) { return n.ssid == ssid; }); if (it == networks.end()) { WifiNetworkInfo network; network.ssid = ssid; network.rssi = rssi; network.isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid); networks.push_back(std::move(network)); } else if (rssi > it->rssi) { it->rssi = rssi; it->isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); } } // 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(); if (autoConnecting && !manualNetworkListRequested && tryNextSavedNetworkFromScan()) { return; } autoConnecting = false; manualNetworkListRequested = false; state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); } void WifiSelectionActivity::selectNetwork(const int index) { if (index < 0 || index >= static_cast(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(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(result.data).text; // state will be updated in next loop iteration } }); } else { // Connect directly for open networks attemptConnection(); } } bool WifiSelectionActivity::hasAttemptedAutoSsid(const std::string& ssid) const { return std::find(autoAttemptedSsids.begin(), autoAttemptedSsids.end(), ssid) != autoAttemptedSsids.end(); } bool WifiSelectionActivity::tryAutoConnectCredential(const WifiCredential& cred) { if (hasAttemptedAutoSsid(cred.ssid)) { return false; } LOG_DBG("WIFI", "Attempting saved network: %s", cred.ssid.c_str()); autoAttemptedSsids.push_back(cred.ssid); selectedSSID = cred.ssid; enteredPassword = cred.password; selectedRequiresPassword = !cred.password.empty(); usedSavedPassword = true; autoConnecting = true; manualNetworkListRequested = false; attemptConnection(); requestUpdate(); return true; } bool WifiSelectionActivity::tryNextSavedNetworkFromScan() { for (const auto& network : networks) { if (!network.hasSavedPassword || hasAttemptedAutoSsid(network.ssid)) { continue; } const auto* cred = WIFI_STORE.findCredential(network.ssid); if (cred && tryAutoConnectCredential(*cred)) { return true; } } return false; } void WifiSelectionActivity::handleAutoConnectFailure() { LOG_DBG("WIFI", "Saved network failed: %s", selectedSSID.c_str()); WiFi.disconnect(); if (!networks.empty()) { if (tryNextSavedNetworkFromScan()) { return; } autoConnecting = false; state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); return; } startWifiScan(true); } void WifiSelectionActivity::showNetworkListFromAutoConnect() { LOG_DBG("WIFI", "User requested manual network list"); WiFi.disconnect(); autoConnecting = false; manualNetworkListRequested = true; if (networks.empty()) { startWifiScan(false); return; } state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); } 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; // Sync RTC from NTP on the first successful WiFi connection only. The DS3231 // drifts ~2 ppm so one sync is enough; users can force a re-sync from // Settings > Customise Status Bar > Sync clock now. if (halClock.isAvailable() && !SETTINGS.clockHasBeenSynced) { if (halClock.syncFromNTP()) { SETTINGS.clockHasBeenSynced = 1; SETTINGS.saveToFile(); } } // 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); } if (autoConnecting) { handleAutoConnectFailure(); return; } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; } // Check for timeout const unsigned long timeoutMs = autoConnecting ? AUTO_CONNECTION_TIMEOUT_MS : CONNECTION_TIMEOUT_MS; if (millis() - connectionStartTime > timeoutMs) { WiFi.disconnect(); connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); if (autoConnecting) { handleAutoConnectFailure(); return; } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; } } void WifiSelectionActivity::loop() { // Check scan progress if (state == WifiSelectionState::SCANNING) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { WiFi.scanDelete(); onComplete(false); return; } if (autoConnecting && mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { autoConnecting = false; manualNetworkListRequested = true; requestUpdate(); } processWifiScanResults(); return; } // Check connection progress if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { if (state == WifiSelectionState::AUTO_CONNECTING) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { WiFi.disconnect(); onComplete(false); return; } if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { showNetworkListFromAutoConnect(); return; } } 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(); auto& theme = UITheme::getInstance(); auto metrics = theme.getMetrics(); Rect screen = theme.getScreenSafeArea(renderer, true, false); // Draw header char countStr[32]; snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, tr(STR_WIFI_NETWORKS), countStr); GUI.drawSubHeader( renderer, Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight}, cachedMacAddress.c_str()); switch (state) { case WifiSelectionState::AUTO_CONNECTING: renderConnecting(&screen, &metrics); break; case WifiSelectionState::SCANNING: renderConnecting(&screen, &metrics); // Reuse connecting screen with different message break; case WifiSelectionState::NETWORK_LIST: renderNetworkList(&screen, &metrics); break; case WifiSelectionState::CONNECTING: renderConnecting(&screen, &metrics); break; case WifiSelectionState::CONNECTED: renderConnected(&screen, &metrics); break; case WifiSelectionState::SAVE_PROMPT: renderSavePrompt(&screen, &metrics); break; case WifiSelectionState::CONNECTION_FAILED: renderConnectionFailed(&screen, &metrics); break; case WifiSelectionState::FORGET_PROMPT: renderForgetPrompt(&screen, &metrics); break; } renderer.displayBuffer(); } void WifiSelectionActivity::renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const { if (networks.empty()) { // No networks found or scan failed const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height) / 2; UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); UITheme::drawCenteredText(renderer, *screen, SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); } else { int contentTop = screen->y + metrics->topPadding + metrics->headerHeight + metrics->tabBarHeight + metrics->verticalSpacing; int contentHeight = screen->height - contentTop - metrics->verticalSpacing * 2; GUI.drawList( renderer, Rect{screen->x, contentTop, screen->width, contentHeight}, static_cast(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{screen->x, screen->y + screen->height - metrics->contentSidePadding - 15, screen->width, 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 Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height) / 2; if (state == WifiSelectionState::SCANNING) { UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, autoConnecting ? tr(STR_FINDING_SAVED_WIFI) : tr(STR_SCANNING)); if (autoConnecting) { const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SHOW_NETWORKS), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } } else { UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, autoConnecting ? tr(STR_CONNECTING_SAVED_WIFI) : 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, "..."); } UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); if (autoConnecting) { const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SHOW_NETWORKS), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } } } void WifiSelectionActivity::renderConnected(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height * 4) / 2; UITheme::drawCenteredText(renderer, *screen, 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, "..."); } UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 10, ssidInfo.c_str()); const std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP; UITheme::drawCenteredText(renderer, *screen, 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 Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height * 3) / 2; UITheme::drawCenteredText(renderer, *screen, 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, "..."); } UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); UITheme::drawCenteredText(renderer, *screen, 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 = screen->x + (screen->width - 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 Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height * 2) / 2; UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); UITheme::drawCenteredText(renderer, *screen, 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 Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = screen->y + (screen->height - height * 3) / 2; UITheme::drawCenteredText(renderer, *screen, 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, "..."); } UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); UITheme::drawCenteredText(renderer, *screen, 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 = screen->x + (screen->width - 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(); }