diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index 59c3a7d3..4ac6629b 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -9,7 +9,7 @@ WifiCredentialStore WifiCredentialStore::instance; namespace { // File format version -constexpr uint8_t WIFI_FILE_VERSION = 1; +constexpr uint8_t WIFI_FILE_VERSION = 2; // Increased version // WiFi credentials file path constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin"; @@ -38,6 +38,7 @@ bool WifiCredentialStore::saveToFile() const { // Write header serialization::writePod(file, WIFI_FILE_VERSION); + serialization::writeString(file, lastConnectedSsid); // Save last connected SSID serialization::writePod(file, static_cast(credentials.size())); // Write each credential @@ -67,12 +68,18 @@ bool WifiCredentialStore::loadFromFile() { // Read and verify version uint8_t version; serialization::readPod(file, version); - if (version != WIFI_FILE_VERSION) { + if (version > WIFI_FILE_VERSION) { Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version); file.close(); return false; } + if (version >= 2) { + serialization::readString(file, lastConnectedSsid); + } else { + lastConnectedSsid.clear(); + } + // Read credential count uint8_t count; serialization::readPod(file, count); @@ -128,6 +135,9 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) { if (cred != credentials.end()) { credentials.erase(cred); Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str()); + if (ssid == lastConnectedSsid) { + clearLastConnectedSsid(); + } return saveToFile(); } return false; // Not found @@ -146,8 +156,25 @@ const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssi bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; } +void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) { + if (lastConnectedSsid != ssid) { + lastConnectedSsid = ssid; + saveToFile(); + } +} + +const std::string& WifiCredentialStore::getLastConnectedSsid() const { return lastConnectedSsid; } + +void WifiCredentialStore::clearLastConnectedSsid() { + if (!lastConnectedSsid.empty()) { + lastConnectedSsid.clear(); + saveToFile(); + } +} + void WifiCredentialStore::clearAll() { credentials.clear(); + lastConnectedSsid.clear(); saveToFile(); Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis()); } diff --git a/src/WifiCredentialStore.h b/src/WifiCredentialStore.h index 0004dc9b..2e2fd6ba 100644 --- a/src/WifiCredentialStore.h +++ b/src/WifiCredentialStore.h @@ -16,6 +16,7 @@ class WifiCredentialStore { private: static WifiCredentialStore instance; std::vector credentials; + std::string lastConnectedSsid; static constexpr size_t MAX_NETWORKS = 8; @@ -48,6 +49,11 @@ class WifiCredentialStore { // Check if a network is saved bool hasSavedCredential(const std::string& ssid) const; + // Last connected network + void setLastConnectedSsid(const std::string& ssid); + const std::string& getLastConnectedSsid() const; + void clearLastConnectedSsid(); + // Clear all credentials void clearAll(); }; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 5475251e..83af4e07 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -21,7 +21,8 @@ void WifiSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); - // Load saved WiFi credentials - SD card operations need lock as we use SPI for both + // Load saved WiFi credentials - SD card operations need lock as we use SPI + // for both xSemaphoreTake(renderingMutex, portMAX_DELAY); WIFI_STORE.loadFromFile(); xSemaphoreGive(renderingMutex); @@ -37,6 +38,7 @@ void WifiSelectionActivity::onEnter() { usedSavedPassword = false; savePromptSelection = 0; forgetPromptSelection = 0; + autoConnecting = false; // Cache MAC address for display uint8_t mac[6]; @@ -46,9 +48,7 @@ void WifiSelectionActivity::onEnter() { mac[5]); cachedMacAddress = std::string(macStr); - // Trigger first update to show scanning message - updateRequired = true; - + // Task creation xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask", 4096, // Stack size (larger for WiFi operations) this, // Parameters @@ -56,7 +56,26 @@ void WifiSelectionActivity::onEnter() { &displayTaskHandle // Task handle ); - // Start WiFi scan + // Attempt to auto-connect to the last network + if (allowAutoConnect) { + const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); + if (!lastSsid.empty()) { + const auto* cred = WIFI_STORE.findCredential(lastSsid); + if (cred) { + Serial.printf("[%lu] [WIFI] Attempting to auto-connect to %s\n", millis(), lastSsid.c_str()); + selectedSSID = cred->ssid; + enteredPassword = cred->password; + selectedRequiresPassword = !cred->password.empty(); + usedSavedPassword = true; + autoConnecting = true; + attemptConnection(); + updateRequired = true; + return; + } + } + } + + // Fallback to scanning startWifiScan(); } @@ -70,15 +89,17 @@ void WifiSelectionActivity::onExit() { WiFi.scanDelete(); Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), 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. + // Note: We do NOT disconnect WiFi here - the parent activity + // (CrossPointWebServerActivity) manages WiFi connection state. We just clean + // up the scan and task. // Acquire mutex before deleting task to ensure task isn't using it // This prevents hangs/crashes if the task holds the mutex when deleted Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis()); xSemaphoreTake(renderingMutex, portMAX_DELAY); - // Delete the display task (we now hold the mutex, so task is blocked if it needs it) + // Delete the display task (we now hold the mutex, so task is blocked if it + // needs it) Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis()); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); @@ -96,6 +117,7 @@ void WifiSelectionActivity::onExit() { } void WifiSelectionActivity::startWifiScan() { + autoConnecting = false; state = WifiSelectionState::SCANNING; networks.clear(); updateRequired = true; @@ -181,6 +203,7 @@ void WifiSelectionActivity::selectNetwork(const int index) { 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); @@ -223,7 +246,7 @@ void WifiSelectionActivity::selectNetwork(const int index) { } void WifiSelectionActivity::attemptConnection() { - state = WifiSelectionState::CONNECTING; + state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; connectionStartTime = millis(); connectedIP.clear(); connectionError.clear(); @@ -239,7 +262,7 @@ void WifiSelectionActivity::attemptConnection() { } void WifiSelectionActivity::checkConnectionStatus() { - if (state != WifiSelectionState::CONNECTING) { + if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) { return; } @@ -251,6 +274,13 @@ void WifiSelectionActivity::checkConnectionStatus() { char ipStr[16]; snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); connectedIP = ipStr; + autoConnecting = false; + + // Save this as the last connected network - SD card operations need lock as + // we use SPI for both + xSemaphoreTake(renderingMutex, portMAX_DELAY); + WIFI_STORE.setLastConnectedSsid(selectedSSID); + xSemaphoreGive(renderingMutex); // If we entered a new password, ask if user wants to save it // Otherwise, immediately complete so parent can start web server @@ -260,7 +290,10 @@ void WifiSelectionActivity::checkConnectionStatus() { updateRequired = true; } else { // Using saved password or open network - complete immediately - Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis()); + Serial.printf( + "[%lu] [WIFI] Connected with saved/open credentials, " + "completing immediately\n", + millis()); onComplete(true); } return; @@ -299,7 +332,7 @@ void WifiSelectionActivity::loop() { } // Check connection progress - if (state == WifiSelectionState::CONNECTING) { + if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { checkConnectionStatus(); return; } @@ -368,17 +401,16 @@ void WifiSelectionActivity::loop() { } } // Go back to network list (whether Cancel or Forget network was selected) - state = WifiSelectionState::NETWORK_LIST; - updateRequired = true; + startWifiScan(); } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { // Skip forgetting, go back to network list - state = WifiSelectionState::NETWORK_LIST; - updateRequired = true; + startWifiScan(); } return; } - // Handle connected state (should not normally be reached - connection completes immediately) + // Handle connected state (should not normally be reached - connection + // completes immediately) if (state == WifiSelectionState::CONNECTED) { // Safety fallback - immediately complete onComplete(true); @@ -389,12 +421,14 @@ void WifiSelectionActivity::loop() { if (state == WifiSelectionState::CONNECTION_FAILED) { if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - // If we used saved credentials, offer to forget the network - if (usedSavedPassword) { + // 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 + // Go back to network list on failure for non-saved credentials state = WifiSelectionState::NETWORK_LIST; } updateRequired = true; @@ -420,6 +454,23 @@ void WifiSelectionActivity::loop() { 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" + updateRequired = true; + return; + } + } + // Handle navigation buttonNavigator.onNext([this] { selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); @@ -479,6 +530,9 @@ void WifiSelectionActivity::render() const { renderer.clearScreen(); switch (state) { + case WifiSelectionState::AUTO_CONNECTING: + renderConnecting(); + break; case WifiSelectionState::SCANNING: renderConnecting(); // Reuse connecting screen with different message break; @@ -582,7 +636,11 @@ void WifiSelectionActivity::renderNetworkList() const { // Draw help text renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); - const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); + + const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; + const char* forgetLabel = hasSavedPassword ? "Forget" : ""; + + const auto labels = mappedInput.mapLabels("« Back", "Connect", forgetLabel, "Refresh"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } @@ -686,8 +744,7 @@ void WifiSelectionActivity::renderForgetPrompt() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD); - + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index ae1702ea..32eb36db 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -22,6 +22,7 @@ struct WifiNetworkInfo { // WiFi selection states enum class WifiSelectionState { + AUTO_CONNECTING, // Trying to connect to the last known network SCANNING, // Scanning for networks NETWORK_LIST, // Displaying available networks PASSWORD_ENTRY, // Entering password for selected network @@ -70,6 +71,12 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { // Whether network was connected using a saved password (skip save prompt) bool usedSavedPassword = false; + // Whether to attempt auto-connect on entry + const bool allowAutoConnect; + + // Whether we are attempting to auto-connect + bool autoConnecting = false; + // Save/forget prompt selection (0 = Yes, 1 = No) int savePromptSelection = 0; int forgetPromptSelection = 0; @@ -98,8 +105,10 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { public: explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onComplete) - : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {} + const std::function& onComplete, bool autoConnect = true) + : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), + onComplete(onComplete), + allowAutoConnect(autoConnect) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 14a2f8a0..7d3a6016 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -11,6 +11,7 @@ #include "MappedInputManager.h" #include "OtaUpdateActivity.h" #include "SettingsList.h" +#include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -46,11 +47,13 @@ void SettingsActivity::onEnter() { } // Append device-only ACTION items - controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action("Remap Front Buttons")); - systemSettings.push_back(SettingInfo::Action("KOReader Sync")); - systemSettings.push_back(SettingInfo::Action("OPDS Browser")); - systemSettings.push_back(SettingInfo::Action("Clear Cache")); - systemSettings.push_back(SettingInfo::Action("Check for updates")); + controlsSettings.insert(controlsSettings.begin(), + SettingInfo::Action("Remap Front Buttons", SettingAction::RemapFrontButtons)); + systemSettings.push_back(SettingInfo::Action("Network", SettingAction::Network)); + systemSettings.push_back(SettingInfo::Action("KOReader Sync", SettingAction::KOReaderSync)); + systemSettings.push_back(SettingInfo::Action("OPDS Browser", SettingAction::OPDSBrowser)); + systemSettings.push_back(SettingInfo::Action("Clear Cache", SettingAction::ClearCache)); + systemSettings.push_back(SettingInfo::Action("Check for updates", SettingAction::CheckForUpdates)); // Reset selection to first category selectedCategoryIndex = 0; @@ -178,46 +181,45 @@ void SettingsActivity::toggleCurrentSetting() { SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; } } else if (setting.type == SettingType::ACTION) { - if (strcmp(setting.name, "Remap Front Buttons") == 0) { + auto enterSubActivity = [this](Activity* activity) { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); - enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); + enterNewActivity(activity); xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "KOReader Sync") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); + }; + + auto onComplete = [this] { exitActivity(); - enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "OPDS Browser") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); + updateRequired = true; + }; + + auto onCompleteBool = [this](bool) { exitActivity(); - enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Clear Cache") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Check for updates") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); + updateRequired = true; + }; + + switch (setting.action) { + case SettingAction::RemapFrontButtons: + enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::KOReaderSync: + enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::OPDSBrowser: + enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::Network: + enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false)); + break; + case SettingAction::ClearCache: + enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::CheckForUpdates: + enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::None: + // Do nothing + break; } } else { return; @@ -289,4 +291,4 @@ void SettingsActivity::render() const { // Always use standard refresh for settings screen renderer.displayBuffer(); -} +} \ No newline at end of file diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 04ead1e0..1417c17d 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -14,11 +14,22 @@ class CrossPointSettings; enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; +enum class SettingAction { + None, + RemapFrontButtons, + KOReaderSync, + OPDSBrowser, + Network, + ClearCache, + CheckForUpdates, +}; + struct SettingInfo { const char* name; SettingType type; uint8_t CrossPointSettings::* valuePtr = nullptr; std::vector enumValues; + SettingAction action = SettingAction::None; struct ValueRange { uint8_t min; @@ -63,10 +74,11 @@ struct SettingInfo { return s; } - static SettingInfo Action(const char* name) { + static SettingInfo Action(const char* name, SettingAction action) { SettingInfo s; s.name = name; s.type = SettingType::ACTION; + s.action = action; return s; } @@ -156,4 +168,4 @@ class SettingsActivity final : public ActivityWithSubactivity { void onEnter() override; void onExit() override; void loop() override; -}; +}; \ No newline at end of file