Files
Crosspoint/src/activities/network/WifiSelectionActivity.cpp
T

824 lines
28 KiB
C++

#include "WifiSelectionActivity.h"
#include <GfxRenderer.h>
#include <HalClock.h>
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
#include <algorithm>
#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<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();
}
}
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<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{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();
}