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>
This commit is contained in:
Eliz
2026-02-10 09:41:44 +00:00
committed by GitHub
parent b5d28a3a9c
commit 98e6789626
6 changed files with 183 additions and 70 deletions

View File

@@ -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, "...");

View File

@@ -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<void(bool connected)>& onComplete)
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
const std::function<void(bool connected)>& onComplete, bool autoConnect = true)
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput),
onComplete(onComplete),
allowAutoConnect(autoConnect) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@@ -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();
}
}

View File

@@ -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<std::string> 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;
};
};