This commit is contained in:
Daniel Poulter 2026-01-14 09:53:44 -05:00 committed by GitHub
commit 233978859a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 667 additions and 40 deletions

View File

@ -3,6 +3,11 @@
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <Serialization.h>
#include <WiFi.h>
#include <freertos/task.h>
#include "activities/ActivityWithSubactivity.h"
#include "activities/network/WifiSelectionActivity.h"
// Initialize the static instance
WifiCredentialStore WifiCredentialStore::instance;
@ -53,6 +58,10 @@ bool WifiCredentialStore::saveToFile() const {
serialization::writeString(file, obfuscatedPwd);
}
// Write default SSID
serialization::writeString(file, defaultSSID);
Serial.printf("[%lu] [WCS] Saving default SSID: %s\n", millis(), defaultSSID.c_str());
file.close();
Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size());
return true;
@ -95,6 +104,27 @@ bool WifiCredentialStore::loadFromFile() {
credentials.push_back(cred);
}
// Try to read default SSID if it exists
defaultSSID.clear();
if (file.available() >= 4) {
const uint32_t posBefore = file.position();
uint32_t len = 0;
serialization::readPod(file, len);
if (file.available() >= len && len <= 64) {
defaultSSID.resize(len);
const size_t bytesRead = file.read(reinterpret_cast<uint8_t*>(&defaultSSID[0]), len);
if (bytesRead == len) {
Serial.printf("[%lu] [WCS] Loaded default SSID: %s\n", millis(), defaultSSID.c_str());
} else {
file.seek(posBefore);
defaultSSID.clear();
}
} else {
file.seek(posBefore);
}
}
file.close();
Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size());
return true;
@ -127,6 +157,9 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) {
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
if (cred != credentials.end()) {
credentials.erase(cred);
if (defaultSSID == ssid) {
defaultSSID.clear();
}
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
return saveToFile();
}
@ -148,6 +181,107 @@ bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { re
void WifiCredentialStore::clearAll() {
credentials.clear();
defaultSSID.clear();
saveToFile();
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
}
void WifiCredentialStore::setDefaultSSID(const std::string& ssid) {
defaultSSID = ssid;
saveToFile();
Serial.printf("[%lu] [WCS] Set default SSID: %s\n", millis(), ssid.c_str());
}
bool WifiCredentialStore::connectToDefaultWifi(int timeoutMs) const {
if (defaultSSID.empty()) {
Serial.printf("[%lu] [WCS] No default SSID set\n", millis());
return false;
}
const auto* cred = findCredential(defaultSSID);
if (!cred) {
return false;
}
// Quick check: scan to see if the SSID is available before attempting connection
WiFi.mode(WIFI_STA);
WiFi.disconnect(false);
delay(100);
Serial.printf("[%lu] [WCS] Scanning for SSID: %s\n", millis(), defaultSSID.c_str());
WiFi.scanNetworks(false);
const unsigned long scanStart = millis();
int16_t scanResult = WiFi.scanComplete();
while (scanResult == WIFI_SCAN_RUNNING && millis() - scanStart < 3000) {
delay(100);
scanResult = WiFi.scanComplete();
}
if (scanResult > 0) {
bool ssidFound = false;
for (int i = 0; i < scanResult; i++) {
std::string scannedSSID = WiFi.SSID(i).c_str();
if (scannedSSID == defaultSSID) {
ssidFound = true;
break;
}
}
WiFi.scanDelete();
if (!ssidFound) {
Serial.printf("[%lu] [WCS] SSID not found in scan results, skipping connection attempt\n", millis());
return false;
}
} else {
WiFi.scanDelete();
return false;
}
WiFi.begin(defaultSSID.c_str(), cred->password.c_str());
Serial.printf("[%lu] [WCS] Connecting to default WiFi: %s\n", millis(), defaultSSID.c_str());
const unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startTime < static_cast<unsigned long>(timeoutMs)) {
vTaskDelay(100 / portTICK_PERIOD_MS);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("[%lu] [WCS] Connected to default WiFi: %s (IP: %s)\n", millis(), defaultSSID.c_str(),
WiFi.localIP().toString().c_str());
return true;
} else {
Serial.printf("[%lu] [WCS] Failed to connect to default WiFi: %s\n", millis(), defaultSSID.c_str());
return false;
}
}
void WifiCredentialStore::ensureWifiConnected(ActivityWithSubactivity& activity, GfxRenderer& renderer,
MappedInputManager& mappedInput, const std::function<void()>& onSuccess,
const std::function<void()>& onCancel, int timeoutMs) {
if (WiFi.status() == WL_CONNECTED) {
onSuccess();
return;
}
// Try to connect using default WiFi
WifiCredentialStore::getInstance().loadFromFile();
if (WifiCredentialStore::getInstance().connectToDefaultWifi(timeoutMs)) {
Serial.printf("[%lu] [WCS] Auto-connected to WiFi\n", millis());
onSuccess();
return;
}
// Auto-connect failed - show WiFi selection list
Serial.printf("[%lu] [WCS] Auto-connect failed, showing WiFi selection\n", millis());
activity.enterNewActivity(
new WifiSelectionActivity(renderer, mappedInput, [&activity, onSuccess, onCancel](bool connected) {
activity.exitActivity();
if (connected) {
onSuccess();
} else {
onCancel();
}
}));
}

View File

@ -1,7 +1,12 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
class ActivityWithSubactivity;
class GfxRenderer;
class MappedInputManager;
struct WifiCredential {
std::string ssid;
std::string password; // Stored obfuscated in file
@ -16,6 +21,7 @@ class WifiCredentialStore {
private:
static WifiCredentialStore instance;
std::vector<WifiCredential> credentials;
std::string defaultSSID;
static constexpr size_t MAX_NETWORKS = 8;
@ -50,6 +56,16 @@ class WifiCredentialStore {
// Clear all credentials
void clearAll();
// Default network management
void setDefaultSSID(const std::string& ssid);
const std::string& getDefaultSSID() const { return defaultSSID; }
bool connectToDefaultWifi(int timeoutMs = 5000) const;
// Helper function to try connecting to default WiFi, or show WiFi selection if it fails
static void ensureWifiConnected(ActivityWithSubactivity& activity, GfxRenderer& renderer,
MappedInputManager& mappedInput, const std::function<void()>& onSuccess,
const std::function<void()>& onCancel, int timeoutMs = 10000);
};
// Helper macro to access credentials store

View File

@ -6,10 +6,10 @@
class ActivityWithSubactivity : public Activity {
protected:
std::unique_ptr<Activity> subActivity = nullptr;
void exitActivity();
void enterNewActivity(Activity* activity);
public:
void exitActivity();
void enterNewActivity(Activity* activity);
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity(std::move(name), renderer, mappedInput) {}
void loop() override;

View File

@ -7,6 +7,7 @@
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "ScreenComponents.h"
#include "WifiCredentialStore.h"
#include "activities/network/WifiSelectionActivity.h"
#include "fontIds.h"
#include "network/HttpDownloader.h"
@ -154,6 +155,12 @@ void OpdsBookBrowserActivity::loop() {
void OpdsBookBrowserActivity::displayTaskLoop() {
while (true) {
// If a subactivity is active, yield CPU time but don't render
if (subActivity) {
vTaskDelay(10 / portTICK_PERIOD_MS);
continue;
}
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -374,8 +381,27 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
return;
}
// Not connected - launch WiFi selection screen directly
launchWifiSelection();
// Try to connect to default WiFi if available
WIFI_STORE.loadFromFile();
const bool hasDefaultSSID = !WIFI_STORE.getDefaultSSID().empty();
if (hasDefaultSSID) {
statusMessage = "Connecting to WiFi...";
updateRequired = true;
}
WIFI_STORE.ensureWifiConnected(
*this, renderer, mappedInput,
[this]() {
state = BrowserState::LOADING;
statusMessage = "Loading...";
updateRequired = true;
fetchFeed(currentPath);
},
[this]() {
// User cancelled WiFi selection - go home
onGoHome();
});
}
void OpdsBookBrowserActivity::launchWifiSelection() {

View File

@ -11,7 +11,7 @@
#include "MappedInputManager.h"
#include "NetworkModeSelectionActivity.h"
#include "WifiSelectionActivity.h"
#include "WifiCredentialStore.h"
#include "fontIds.h"
namespace {
@ -135,14 +135,24 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
exitActivity();
if (mode == NetworkMode::JOIN_NETWORK) {
// STA mode - launch WiFi selection
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
// STA mode - use ensureWifiConnected helper
Serial.printf("[%lu] [WEBACT] Checking WiFi connection...\n", millis());
WiFi.mode(WIFI_STA);
state = WebServerActivityState::WIFI_SELECTION;
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
WIFI_STORE.ensureWifiConnected(
*this, renderer, mappedInput,
[this]() {
// WiFi connected - start web server
connectedIP = WiFi.localIP().toString().c_str();
connectedSSID = WiFi.SSID().c_str();
isApMode = false;
onWifiSelectionComplete(true);
},
[this]() {
// WiFi connection cancelled - go back to mode selection
onWifiSelectionComplete(false);
});
} else {
// AP mode - start access point
state = WebServerActivityState::AP_STARTING;
@ -152,15 +162,15 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
}
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
Serial.printf("[%lu] [WEBACT] WiFi connection completed, connected=%d\n", millis(), connected);
if (connected) {
// Get connection info before exiting subactivity
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
connectedSSID = WiFi.SSID().c_str();
isApMode = false;
exitActivity();
// Exit any subactivity if present
if (subActivity) {
exitActivity();
}
// Start mDNS for hostname resolution
if (MDNS.begin(AP_HOSTNAME)) {

View File

@ -24,7 +24,7 @@ enum class WebServerActivityState {
* CrossPointWebServerActivity is the entry point for file transfer functionality.
* It:
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
* - For STA mode: Launches WifiSelectionActivity to connect to an existing network
* - For STA mode: Uses ensureWifiConnected to auto-connect to default WiFi or show WiFi selection
* - For AP mode: Creates an Access Point that clients can connect to
* - Starts the CrossPointWebServer when connected
* - Handles client requests in its loop() function

View File

@ -36,6 +36,7 @@ void WifiSelectionActivity::onEnter() {
usedSavedPassword = false;
savePromptSelection = 0;
forgetPromptSelection = 0;
setDefaultPromptSelection = 0;
// Trigger first update to show scanning message
updateRequired = true;
@ -229,6 +230,28 @@ void WifiSelectionActivity::attemptConnection() {
}
}
void WifiSelectionActivity::savePassword() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
xSemaphoreGive(renderingMutex);
}
void WifiSelectionActivity::displaySetDefaultPrompt() {
WIFI_STORE.loadFromFile();
const std::string currentDefault = WIFI_STORE.getDefaultSSID();
if (selectedSSID != currentDefault) {
state = WifiSelectionState::SET_DEFAULT_PROMPT;
setDefaultPromptSelection = 0;
updateRequired = true;
}
}
void WifiSelectionActivity::setDefaultNetwork() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.setDefaultSSID(selectedSSID);
xSemaphoreGive(renderingMutex);
}
void WifiSelectionActivity::checkConnectionStatus() {
if (state != WifiSelectionState::CONNECTING) {
return;
@ -243,16 +266,19 @@ void WifiSelectionActivity::checkConnectionStatus() {
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
connectedIP = ipStr;
// If we entered a new password, ask if user wants to save it
// Otherwise, immediately complete so parent can start web server
// If we entered a new password, save it (with or without prompt based on fromSettingsScreen)
if (!usedSavedPassword && !enteredPassword.empty()) {
state = WifiSelectionState::SAVE_PROMPT;
savePromptSelection = 0; // Default to "Yes"
updateRequired = true;
} else {
// Using saved password or open network - complete immediately
Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis());
onComplete(true);
if (fromSettingsScreen) {
// Always save without prompting
savePassword();
displaySetDefaultPrompt();
return;
} else {
state = WifiSelectionState::SAVE_PROMPT;
savePromptSelection = 0;
updateRequired = true;
}
}
return;
}
@ -318,11 +344,9 @@ void WifiSelectionActivity::loop() {
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (savePromptSelection == 0) {
// User chose "Yes" - save the password
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
xSemaphoreGive(renderingMutex);
savePassword();
displaySetDefaultPrompt();
}
// Complete - parent will start web server
onComplete(true);
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
// Skip saving, complete anyway
@ -331,6 +355,44 @@ void WifiSelectionActivity::loop() {
return;
}
// Handle set default prompt state
if (state == WifiSelectionState::SET_DEFAULT_PROMPT) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (setDefaultPromptSelection > 0) {
setDefaultPromptSelection--;
updateRequired = true;
}
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (setDefaultPromptSelection < 1) {
setDefaultPromptSelection++;
updateRequired = true;
}
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (setDefaultPromptSelection == 0) {
// User chose "Yes" - set as default
setDefaultNetwork();
}
// Disconnect if from settings screen before completing
if (fromSettingsScreen) {
WiFi.disconnect(false);
delay(100);
WiFi.mode(WIFI_OFF);
}
onComplete(true);
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
// Disconnect if from settings screen before completing
if (fromSettingsScreen) {
WiFi.disconnect(false);
delay(100);
WiFi.mode(WIFI_OFF);
}
onComplete(true);
}
return;
}
// Handle forget prompt state (connection failed with saved credentials)
if (state == WifiSelectionState::FORGET_PROMPT) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
@ -489,6 +551,9 @@ void WifiSelectionActivity::render() const {
case WifiSelectionState::SAVE_PROMPT:
renderSavePrompt();
break;
case WifiSelectionState::SET_DEFAULT_PROMPT:
renderSetDefaultPrompt();
break;
case WifiSelectionState::CONNECTION_FAILED:
renderConnectionFailed();
break;
@ -655,6 +720,46 @@ void WifiSelectionActivity::renderSavePrompt() const {
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
}
void WifiSelectionActivity::renderSetDefaultPrompt() 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, "Connected!", true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + 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, "Set as default WiFi?");
// 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 (setDefaultPromptSelection == 0) {
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]");
} else {
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes");
}
// Draw "No" button
if (setDefaultPromptSelection == 1) {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]");
} else {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No");
}
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
}
void WifiSelectionActivity::renderConnectionFailed() const {
const auto pageHeight = renderer.getScreenHeight();
const auto height = renderer.getLineHeight(UI_10_FONT_ID);

View File

@ -21,14 +21,15 @@ struct WifiNetworkInfo {
// WiFi selection states
enum class WifiSelectionState {
SCANNING, // Scanning for networks
NETWORK_LIST, // Displaying available networks
PASSWORD_ENTRY, // Entering password for selected network
CONNECTING, // Attempting to connect
CONNECTED, // Successfully connected
SAVE_PROMPT, // Asking user if they want to save the password
CONNECTION_FAILED, // Connection failed
FORGET_PROMPT // Asking user if they want to forget the network
SCANNING, // Scanning for networks
NETWORK_LIST, // Displaying available networks
PASSWORD_ENTRY, // Entering password for selected network
CONNECTING, // Attempting to connect
CONNECTED, // Successfully connected
SAVE_PROMPT, // Asking user if they want to save the password
SET_DEFAULT_PROMPT, // Asking user if they want to set as default
CONNECTION_FAILED, // Connection failed
FORGET_PROMPT // Asking user if they want to forget the network
};
/**
@ -65,9 +66,13 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
// Whether network was connected using a saved password (skip save prompt)
bool usedSavedPassword = false;
// Save/forget prompt selection (0 = Yes, 1 = No)
// Whether launched from settings screen (affects save behavior and disconnection)
bool fromSettingsScreen = false;
// Save/forget/set default prompt selection (0 = Yes, 1 = No)
int savePromptSelection = 0;
int forgetPromptSelection = 0;
int setDefaultPromptSelection = 0;
// Connection timeout
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
@ -81,6 +86,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
void renderConnecting() const;
void renderConnected() const;
void renderSavePrompt() const;
void renderSetDefaultPrompt() const;
void renderConnectionFailed() const;
void renderForgetPrompt() const;
@ -91,10 +97,17 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
void checkConnectionStatus();
std::string getSignalStrengthIndicator(int32_t rssi) const;
// Helper methods
void savePassword();
void displaySetDefaultPrompt();
void setDefaultNetwork();
public:
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void(bool connected)>& onComplete)
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
const std::function<void(bool connected)>& onComplete, bool fromSettingsScreen = false)
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput),
onComplete(onComplete),
fromSettingsScreen(fromSettingsScreen) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@ -9,6 +9,7 @@
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "WifiConnectionsActivity.h"
#include "fontIds.h"
// Define the static settings list
@ -42,6 +43,7 @@ const SettingInfo settingsList[settingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Action("Calibre Settings"),
SettingInfo::Action("WiFi Connections"),
SettingInfo::Action("Check for updates")};
} // namespace
@ -147,6 +149,14 @@ void SettingsActivity::toggleCurrentSetting() {
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "WiFi Connections") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new WifiConnectionsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Check for updates") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();

View File

@ -0,0 +1,267 @@
#include "WifiConnectionsActivity.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include "MappedInputManager.h"
#include "WifiCredentialStore.h"
#include "activities/network/WifiSelectionActivity.h"
#include "fontIds.h"
namespace {
constexpr int PAGE_ITEMS = 23;
constexpr int SKIP_PAGE_MS = 700;
constexpr unsigned long IGNORE_INPUT_MS = 300;
} // namespace
void WifiConnectionsActivity::taskTrampoline(void* param) {
auto* self = static_cast<WifiConnectionsActivity*>(param);
self->displayTaskLoop();
}
void WifiConnectionsActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
state = State::LIST;
selectorIndex = 0;
settingsSelection = 0;
selectedNetwork.clear();
enterTime = millis();
updateRequired = true;
WIFI_STORE.loadFromFile();
xTaskCreate(&WifiConnectionsActivity::taskTrampoline, "WifiConnectionsTask", 4096, this, 1, &displayTaskHandle);
}
void WifiConnectionsActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void WifiConnectionsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
const unsigned long timeSinceEnter = millis() - enterTime;
if (timeSinceEnter < IGNORE_INPUT_MS) {
return;
}
if (state == State::SETTINGS_MENU) {
// Check if this network is already the default
WIFI_STORE.loadFromFile();
const std::string currentDefault = WIFI_STORE.getDefaultSSID();
const bool isDefault = (selectedNetwork == currentDefault);
// Handle settings menu
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
mappedInput.wasReleased(MappedInputManager::Button::Left)) {
if (settingsSelection > 0) {
settingsSelection--;
updateRequired = true;
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Down) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
if (settingsSelection < 1) {
settingsSelection++;
updateRequired = true;
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (settingsSelection == 0) {
if (isDefault) {
removeDefault();
} else {
setDefault();
}
} else {
deleteNetwork();
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
cancelSettings();
}
return;
}
// Handle list navigation
const auto& credentials = WIFI_STORE.getCredentials();
const size_t totalItems = credentials.size() + 1; // +1 for "Add new connection"
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
mappedInput.wasReleased(MappedInputManager::Button::Right);
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectorIndex == 0) {
// "Add new connection" selected - launch WiFi selection
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new WifiSelectionActivity(
renderer, mappedInput,
[this](bool connected) {
// Reload credentials after WiFi selection
WIFI_STORE.loadFromFile();
exitActivity();
enterTime = millis(); // Reset enter time to ignore input after subactivity exits
updateRequired = true;
},
true)); // true = fromSettingsScreen (always save password and disconnect after)
xSemaphoreGive(renderingMutex);
} else {
// Regular credential selected
handleSettings();
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onBack();
} else if (prevReleased) {
if (skipPage) {
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + totalItems) % totalItems;
} else {
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
}
updateRequired = true;
} else if (nextReleased) {
if (skipPage) {
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % totalItems;
} else {
selectorIndex = (selectorIndex + 1) % totalItems;
}
updateRequired = true;
}
}
void WifiConnectionsActivity::handleSettings() {
const auto& credentials = WIFI_STORE.getCredentials();
// selectorIndex 0 is "Add new connection", so credentials start at index 1
if (selectorIndex == 0 || selectorIndex > credentials.size()) {
return;
}
selectedNetwork = credentials[selectorIndex - 1].ssid;
state = State::SETTINGS_MENU;
settingsSelection = 0;
updateRequired = true;
}
void WifiConnectionsActivity::setDefault() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.setDefaultSSID(selectedNetwork);
xSemaphoreGive(renderingMutex);
state = State::LIST;
selectedNetwork.clear();
updateRequired = true;
}
void WifiConnectionsActivity::removeDefault() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.setDefaultSSID("");
xSemaphoreGive(renderingMutex);
state = State::LIST;
selectedNetwork.clear();
updateRequired = true;
}
void WifiConnectionsActivity::deleteNetwork() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.removeCredential(selectedNetwork);
xSemaphoreGive(renderingMutex);
// Reload to get updated list
WIFI_STORE.loadFromFile();
const auto& credentials = WIFI_STORE.getCredentials();
const size_t totalItems = credentials.size() + 1;
if (selectorIndex >= totalItems) {
selectorIndex = totalItems - 1;
}
state = State::LIST;
selectedNetwork.clear();
updateRequired = true;
}
void WifiConnectionsActivity::cancelSettings() {
state = State::LIST;
selectedNetwork.clear();
updateRequired = true;
}
void WifiConnectionsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void WifiConnectionsActivity::render() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Connections", true, EpdFontFamily::BOLD);
if (state == State::SETTINGS_MENU) {
const int centerY = pageHeight / 2;
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Settings", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 20, selectedNetwork.c_str());
// Check if this network is already the default
const std::string currentDefault = WIFI_STORE.getDefaultSSID();
const bool isDefault = (selectedNetwork == currentDefault);
const char* defaultText = settingsSelection == 0 ? (isDefault ? "> Remove Default" : "> Set Default")
: (isDefault ? " Remove Default" : " Set Default");
const char* deleteText = settingsSelection == 1 ? "> Delete" : " Delete";
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 20, defaultText);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 40, deleteText);
const auto labels = mappedInput.mapLabels("Cancel", "Confirm", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} else {
const auto& credentials = WIFI_STORE.getCredentials();
const auto labels = mappedInput.mapLabels("« Back", selectorIndex == 0 ? "Add" : "Settings", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
const size_t totalItems = credentials.size() + 1;
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
const std::string& defaultSSID = WIFI_STORE.getDefaultSSID();
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
for (size_t i = pageStartIndex; i < totalItems && i < pageStartIndex + PAGE_ITEMS; i++) {
std::string displayText;
if (i == 0) {
displayText = "+ Add new connection";
} else {
displayText = credentials[i - 1].ssid;
if (credentials[i - 1].ssid == defaultSSID) {
displayText += " [Default]";
}
}
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40);
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
}
}
renderer.displayBuffer();
}

View File

@ -0,0 +1,46 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include "activities/ActivityWithSubactivity.h"
/**
* Activity for managing saved WiFi connections.
* Shows a list of saved WiFi networks and allows deletion with confirmation.
*/
class WifiConnectionsActivity final : public ActivityWithSubactivity {
public:
explicit WifiConnectionsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: ActivityWithSubactivity("WifiConnections", renderer, mappedInput), onBack(onBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
private:
enum class State { LIST, SETTINGS_MENU };
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
State state = State::LIST;
size_t selectorIndex = 0;
int settingsSelection = 0;
std::string selectedNetwork;
unsigned long enterTime = 0;
const std::function<void()> onBack;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
void handleSettings();
void setDefault();
void removeDefault();
void deleteNetwork();
void cancelSettings();
};