Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
824 lines
28 KiB
C++
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();
|
|
}
|