A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Connect to last wifi by default (#752)

## Summary

* **What is the goal of this PR?**

Use last connected network as default

* **What changes are included?**

- Refactor how an action type of Settings are handled
- Add a new System Settings option → Network
- Add the ability to forget a network in the Network Selection Screen
- Add the ability to Refresh network list
- Save the last connected network SSID
- Use the last connection whenever network is needed (OPDS, Koreader
sync, update etc)

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).


![IMG_6504](https://github.com/user-attachments/assets/e48fb013-b5c3-45c0-b284-e183e6fd5a68)

![IMG_6503](https://github.com/user-attachments/assets/78c4b6b6-4e7b-4656-b356-19d65ff6aa12)




https://github.com/user-attachments/assets/95bf34a8-44ce-4279-8cd8-f78524ce745b





---

### AI Usage

Did you use AI tools to help write this code? _** PARTIALLY: I wrote
most of it but I also used Gemini as assist.

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

authored by

Eliz
Eliz Kilic
Copilot
and committed by
GitHub
98e67896 b5d28a3a

+183 -70
+29 -2
src/WifiCredentialStore.cpp
··· 9 9 10 10 namespace { 11 11 // File format version 12 - constexpr uint8_t WIFI_FILE_VERSION = 1; 12 + constexpr uint8_t WIFI_FILE_VERSION = 2; // Increased version 13 13 14 14 // WiFi credentials file path 15 15 constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin"; ··· 38 38 39 39 // Write header 40 40 serialization::writePod(file, WIFI_FILE_VERSION); 41 + serialization::writeString(file, lastConnectedSsid); // Save last connected SSID 41 42 serialization::writePod(file, static_cast<uint8_t>(credentials.size())); 42 43 43 44 // Write each credential ··· 67 68 // Read and verify version 68 69 uint8_t version; 69 70 serialization::readPod(file, version); 70 - if (version != WIFI_FILE_VERSION) { 71 + if (version > WIFI_FILE_VERSION) { 71 72 Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version); 72 73 file.close(); 73 74 return false; 74 75 } 75 76 77 + if (version >= 2) { 78 + serialization::readString(file, lastConnectedSsid); 79 + } else { 80 + lastConnectedSsid.clear(); 81 + } 82 + 76 83 // Read credential count 77 84 uint8_t count; 78 85 serialization::readPod(file, count); ··· 128 135 if (cred != credentials.end()) { 129 136 credentials.erase(cred); 130 137 Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str()); 138 + if (ssid == lastConnectedSsid) { 139 + clearLastConnectedSsid(); 140 + } 131 141 return saveToFile(); 132 142 } 133 143 return false; // Not found ··· 146 156 147 157 bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; } 148 158 159 + void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) { 160 + if (lastConnectedSsid != ssid) { 161 + lastConnectedSsid = ssid; 162 + saveToFile(); 163 + } 164 + } 165 + 166 + const std::string& WifiCredentialStore::getLastConnectedSsid() const { return lastConnectedSsid; } 167 + 168 + void WifiCredentialStore::clearLastConnectedSsid() { 169 + if (!lastConnectedSsid.empty()) { 170 + lastConnectedSsid.clear(); 171 + saveToFile(); 172 + } 173 + } 174 + 149 175 void WifiCredentialStore::clearAll() { 150 176 credentials.clear(); 177 + lastConnectedSsid.clear(); 151 178 saveToFile(); 152 179 Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis()); 153 180 }
+6
src/WifiCredentialStore.h
··· 16 16 private: 17 17 static WifiCredentialStore instance; 18 18 std::vector<WifiCredential> credentials; 19 + std::string lastConnectedSsid; 19 20 20 21 static constexpr size_t MAX_NETWORKS = 8; 21 22 ··· 47 48 48 49 // Check if a network is saved 49 50 bool hasSavedCredential(const std::string& ssid) const; 51 + 52 + // Last connected network 53 + void setLastConnectedSsid(const std::string& ssid); 54 + const std::string& getLastConnectedSsid() const; 55 + void clearLastConnectedSsid(); 50 56 51 57 // Clear all credentials 52 58 void clearAll();
+80 -23
src/activities/network/WifiSelectionActivity.cpp
··· 21 21 22 22 renderingMutex = xSemaphoreCreateMutex(); 23 23 24 - // Load saved WiFi credentials - SD card operations need lock as we use SPI for both 24 + // Load saved WiFi credentials - SD card operations need lock as we use SPI 25 + // for both 25 26 xSemaphoreTake(renderingMutex, portMAX_DELAY); 26 27 WIFI_STORE.loadFromFile(); 27 28 xSemaphoreGive(renderingMutex); ··· 37 38 usedSavedPassword = false; 38 39 savePromptSelection = 0; 39 40 forgetPromptSelection = 0; 41 + autoConnecting = false; 40 42 41 43 // Cache MAC address for display 42 44 uint8_t mac[6]; ··· 46 48 mac[5]); 47 49 cachedMacAddress = std::string(macStr); 48 50 49 - // Trigger first update to show scanning message 50 - updateRequired = true; 51 - 51 + // Task creation 52 52 xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask", 53 53 4096, // Stack size (larger for WiFi operations) 54 54 this, // Parameters ··· 56 56 &displayTaskHandle // Task handle 57 57 ); 58 58 59 - // Start WiFi scan 59 + // Attempt to auto-connect to the last network 60 + if (allowAutoConnect) { 61 + const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); 62 + if (!lastSsid.empty()) { 63 + const auto* cred = WIFI_STORE.findCredential(lastSsid); 64 + if (cred) { 65 + Serial.printf("[%lu] [WIFI] Attempting to auto-connect to %s\n", millis(), lastSsid.c_str()); 66 + selectedSSID = cred->ssid; 67 + enteredPassword = cred->password; 68 + selectedRequiresPassword = !cred->password.empty(); 69 + usedSavedPassword = true; 70 + autoConnecting = true; 71 + attemptConnection(); 72 + updateRequired = true; 73 + return; 74 + } 75 + } 76 + } 77 + 78 + // Fallback to scanning 60 79 startWifiScan(); 61 80 } 62 81 ··· 70 89 WiFi.scanDelete(); 71 90 Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap()); 72 91 73 - // Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity) 74 - // manages WiFi connection state. We just clean up the scan and task. 92 + // Note: We do NOT disconnect WiFi here - the parent activity 93 + // (CrossPointWebServerActivity) manages WiFi connection state. We just clean 94 + // up the scan and task. 75 95 76 96 // Acquire mutex before deleting task to ensure task isn't using it 77 97 // This prevents hangs/crashes if the task holds the mutex when deleted 78 98 Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis()); 79 99 xSemaphoreTake(renderingMutex, portMAX_DELAY); 80 100 81 - // Delete the display task (we now hold the mutex, so task is blocked if it needs it) 101 + // Delete the display task (we now hold the mutex, so task is blocked if it 102 + // needs it) 82 103 Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis()); 83 104 if (displayTaskHandle) { 84 105 vTaskDelete(displayTaskHandle); ··· 96 117 } 97 118 98 119 void WifiSelectionActivity::startWifiScan() { 120 + autoConnecting = false; 99 121 state = WifiSelectionState::SCANNING; 100 122 networks.clear(); 101 123 updateRequired = true; ··· 181 203 selectedRequiresPassword = network.isEncrypted; 182 204 usedSavedPassword = false; 183 205 enteredPassword.clear(); 206 + autoConnecting = false; 184 207 185 208 // Check if we have saved credentials for this network 186 209 const auto* savedCred = WIFI_STORE.findCredential(selectedSSID); ··· 223 246 } 224 247 225 248 void WifiSelectionActivity::attemptConnection() { 226 - state = WifiSelectionState::CONNECTING; 249 + state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; 227 250 connectionStartTime = millis(); 228 251 connectedIP.clear(); 229 252 connectionError.clear(); ··· 239 262 } 240 263 241 264 void WifiSelectionActivity::checkConnectionStatus() { 242 - if (state != WifiSelectionState::CONNECTING) { 265 + if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) { 243 266 return; 244 267 } 245 268 ··· 251 274 char ipStr[16]; 252 275 snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); 253 276 connectedIP = ipStr; 277 + autoConnecting = false; 278 + 279 + // Save this as the last connected network - SD card operations need lock as 280 + // we use SPI for both 281 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 282 + WIFI_STORE.setLastConnectedSsid(selectedSSID); 283 + xSemaphoreGive(renderingMutex); 254 284 255 285 // If we entered a new password, ask if user wants to save it 256 286 // Otherwise, immediately complete so parent can start web server ··· 260 290 updateRequired = true; 261 291 } else { 262 292 // Using saved password or open network - complete immediately 263 - Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis()); 293 + Serial.printf( 294 + "[%lu] [WIFI] Connected with saved/open credentials, " 295 + "completing immediately\n", 296 + millis()); 264 297 onComplete(true); 265 298 } 266 299 return; ··· 299 332 } 300 333 301 334 // Check connection progress 302 - if (state == WifiSelectionState::CONNECTING) { 335 + if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { 303 336 checkConnectionStatus(); 304 337 return; 305 338 } ··· 368 401 } 369 402 } 370 403 // Go back to network list (whether Cancel or Forget network was selected) 371 - state = WifiSelectionState::NETWORK_LIST; 372 - updateRequired = true; 404 + startWifiScan(); 373 405 } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 374 406 // Skip forgetting, go back to network list 375 - state = WifiSelectionState::NETWORK_LIST; 376 - updateRequired = true; 407 + startWifiScan(); 377 408 } 378 409 return; 379 410 } 380 411 381 - // Handle connected state (should not normally be reached - connection completes immediately) 412 + // Handle connected state (should not normally be reached - connection 413 + // completes immediately) 382 414 if (state == WifiSelectionState::CONNECTED) { 383 415 // Safety fallback - immediately complete 384 416 onComplete(true); ··· 389 421 if (state == WifiSelectionState::CONNECTION_FAILED) { 390 422 if (mappedInput.wasPressed(MappedInputManager::Button::Back) || 391 423 mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 392 - // If we used saved credentials, offer to forget the network 393 - if (usedSavedPassword) { 424 + // If we were auto-connecting or using a saved credential, offer to forget 425 + // the network 426 + if (autoConnecting || usedSavedPassword) { 427 + autoConnecting = false; 394 428 state = WifiSelectionState::FORGET_PROMPT; 395 429 forgetPromptSelection = 0; // Default to "Cancel" 396 430 } else { 397 - // Go back to network list on failure 431 + // Go back to network list on failure for non-saved credentials 398 432 state = WifiSelectionState::NETWORK_LIST; 399 433 } 400 434 updateRequired = true; ··· 420 454 return; 421 455 } 422 456 457 + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { 458 + startWifiScan(); 459 + return; 460 + } 461 + 462 + const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left); 463 + if (leftPressed) { 464 + const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; 465 + if (hasSavedPassword) { 466 + selectedSSID = networks[selectedNetworkIndex].ssid; 467 + state = WifiSelectionState::FORGET_PROMPT; 468 + forgetPromptSelection = 0; // Default to "Cancel" 469 + updateRequired = true; 470 + return; 471 + } 472 + } 473 + 423 474 // Handle navigation 424 475 buttonNavigator.onNext([this] { 425 476 selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); ··· 479 530 renderer.clearScreen(); 480 531 481 532 switch (state) { 533 + case WifiSelectionState::AUTO_CONNECTING: 534 + renderConnecting(); 535 + break; 482 536 case WifiSelectionState::SCANNING: 483 537 renderConnecting(); // Reuse connecting screen with different message 484 538 break; ··· 582 636 583 637 // Draw help text 584 638 renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); 585 - const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); 639 + 640 + const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; 641 + const char* forgetLabel = hasSavedPassword ? "Forget" : ""; 642 + 643 + const auto labels = mappedInput.mapLabels("« Back", "Connect", forgetLabel, "Refresh"); 586 644 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 587 645 } 588 646 ··· 686 744 const auto height = renderer.getLineHeight(UI_10_FONT_ID); 687 745 const auto top = (pageHeight - height * 3) / 2; 688 746 689 - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD); 690 - 747 + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network", true, EpdFontFamily::BOLD); 691 748 std::string ssidInfo = "Network: " + selectedSSID; 692 749 if (ssidInfo.length() > 28) { 693 750 ssidInfo.replace(25, ssidInfo.length() - 25, "...");
+11 -2
src/activities/network/WifiSelectionActivity.h
··· 22 22 23 23 // WiFi selection states 24 24 enum class WifiSelectionState { 25 + AUTO_CONNECTING, // Trying to connect to the last known network 25 26 SCANNING, // Scanning for networks 26 27 NETWORK_LIST, // Displaying available networks 27 28 PASSWORD_ENTRY, // Entering password for selected network ··· 70 71 // Whether network was connected using a saved password (skip save prompt) 71 72 bool usedSavedPassword = false; 72 73 74 + // Whether to attempt auto-connect on entry 75 + const bool allowAutoConnect; 76 + 77 + // Whether we are attempting to auto-connect 78 + bool autoConnecting = false; 79 + 73 80 // Save/forget prompt selection (0 = Yes, 1 = No) 74 81 int savePromptSelection = 0; 75 82 int forgetPromptSelection = 0; ··· 98 105 99 106 public: 100 107 explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 101 - const std::function<void(bool connected)>& onComplete) 102 - : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {} 108 + const std::function<void(bool connected)>& onComplete, bool autoConnect = true) 109 + : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), 110 + onComplete(onComplete), 111 + allowAutoConnect(autoConnect) {} 103 112 void onEnter() override; 104 113 void onExit() override; 105 114 void loop() override;
+43 -41
src/activities/settings/SettingsActivity.cpp
··· 11 11 #include "MappedInputManager.h" 12 12 #include "OtaUpdateActivity.h" 13 13 #include "SettingsList.h" 14 + #include "activities/network/WifiSelectionActivity.h" 14 15 #include "components/UITheme.h" 15 16 #include "fontIds.h" 16 17 ··· 46 47 } 47 48 48 49 // Append device-only ACTION items 49 - controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action("Remap Front Buttons")); 50 - systemSettings.push_back(SettingInfo::Action("KOReader Sync")); 51 - systemSettings.push_back(SettingInfo::Action("OPDS Browser")); 52 - systemSettings.push_back(SettingInfo::Action("Clear Cache")); 53 - systemSettings.push_back(SettingInfo::Action("Check for updates")); 50 + controlsSettings.insert(controlsSettings.begin(), 51 + SettingInfo::Action("Remap Front Buttons", SettingAction::RemapFrontButtons)); 52 + systemSettings.push_back(SettingInfo::Action("Network", SettingAction::Network)); 53 + systemSettings.push_back(SettingInfo::Action("KOReader Sync", SettingAction::KOReaderSync)); 54 + systemSettings.push_back(SettingInfo::Action("OPDS Browser", SettingAction::OPDSBrowser)); 55 + systemSettings.push_back(SettingInfo::Action("Clear Cache", SettingAction::ClearCache)); 56 + systemSettings.push_back(SettingInfo::Action("Check for updates", SettingAction::CheckForUpdates)); 54 57 55 58 // Reset selection to first category 56 59 selectedCategoryIndex = 0; ··· 178 181 SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; 179 182 } 180 183 } else if (setting.type == SettingType::ACTION) { 181 - if (strcmp(setting.name, "Remap Front Buttons") == 0) { 184 + auto enterSubActivity = [this](Activity* activity) { 182 185 xSemaphoreTake(renderingMutex, portMAX_DELAY); 183 186 exitActivity(); 184 - enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] { 185 - exitActivity(); 186 - updateRequired = true; 187 - })); 187 + enterNewActivity(activity); 188 188 xSemaphoreGive(renderingMutex); 189 - } else if (strcmp(setting.name, "KOReader Sync") == 0) { 190 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 189 + }; 190 + 191 + auto onComplete = [this] { 191 192 exitActivity(); 192 - enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { 193 - exitActivity(); 194 - updateRequired = true; 195 - })); 196 - xSemaphoreGive(renderingMutex); 197 - } else if (strcmp(setting.name, "OPDS Browser") == 0) { 198 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 199 - exitActivity(); 200 - enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { 201 - exitActivity(); 202 - updateRequired = true; 203 - })); 204 - xSemaphoreGive(renderingMutex); 205 - } else if (strcmp(setting.name, "Clear Cache") == 0) { 206 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 207 - exitActivity(); 208 - enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { 209 - exitActivity(); 210 - updateRequired = true; 211 - })); 212 - xSemaphoreGive(renderingMutex); 213 - } else if (strcmp(setting.name, "Check for updates") == 0) { 214 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 193 + updateRequired = true; 194 + }; 195 + 196 + auto onCompleteBool = [this](bool) { 215 197 exitActivity(); 216 - enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { 217 - exitActivity(); 218 - updateRequired = true; 219 - })); 220 - xSemaphoreGive(renderingMutex); 198 + updateRequired = true; 199 + }; 200 + 201 + switch (setting.action) { 202 + case SettingAction::RemapFrontButtons: 203 + enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete)); 204 + break; 205 + case SettingAction::KOReaderSync: 206 + enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete)); 207 + break; 208 + case SettingAction::OPDSBrowser: 209 + enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete)); 210 + break; 211 + case SettingAction::Network: 212 + enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false)); 213 + break; 214 + case SettingAction::ClearCache: 215 + enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete)); 216 + break; 217 + case SettingAction::CheckForUpdates: 218 + enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete)); 219 + break; 220 + case SettingAction::None: 221 + // Do nothing 222 + break; 221 223 } 222 224 } else { 223 225 return; ··· 289 291 290 292 // Always use standard refresh for settings screen 291 293 renderer.displayBuffer(); 292 - } 294 + }
+14 -2
src/activities/settings/SettingsActivity.h
··· 14 14 15 15 enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; 16 16 17 + enum class SettingAction { 18 + None, 19 + RemapFrontButtons, 20 + KOReaderSync, 21 + OPDSBrowser, 22 + Network, 23 + ClearCache, 24 + CheckForUpdates, 25 + }; 26 + 17 27 struct SettingInfo { 18 28 const char* name; 19 29 SettingType type; 20 30 uint8_t CrossPointSettings::* valuePtr = nullptr; 21 31 std::vector<std::string> enumValues; 32 + SettingAction action = SettingAction::None; 22 33 23 34 struct ValueRange { 24 35 uint8_t min; ··· 63 74 return s; 64 75 } 65 76 66 - static SettingInfo Action(const char* name) { 77 + static SettingInfo Action(const char* name, SettingAction action) { 67 78 SettingInfo s; 68 79 s.name = name; 69 80 s.type = SettingType::ACTION; 81 + s.action = action; 70 82 return s; 71 83 } 72 84 ··· 156 168 void onEnter() override; 157 169 void onExit() override; 158 170 void loop() override; 159 - }; 171 + };