mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 16:17:38 +03:00
feature: adding a default wifi option
This commit is contained in:
parent
d4ae108d9b
commit
cd1a441d2b
@ -3,6 +3,11 @@
|
|||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
WifiCredentialStore WifiCredentialStore::instance;
|
WifiCredentialStore WifiCredentialStore::instance;
|
||||||
@ -53,6 +58,10 @@ bool WifiCredentialStore::saveToFile() const {
|
|||||||
serialization::writeString(file, obfuscatedPwd);
|
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();
|
file.close();
|
||||||
Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size());
|
Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size());
|
||||||
return true;
|
return true;
|
||||||
@ -95,6 +104,27 @@ bool WifiCredentialStore::loadFromFile() {
|
|||||||
credentials.push_back(cred);
|
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();
|
file.close();
|
||||||
Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size());
|
Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size());
|
||||||
return true;
|
return true;
|
||||||
@ -127,6 +157,9 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) {
|
|||||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||||
if (cred != credentials.end()) {
|
if (cred != credentials.end()) {
|
||||||
credentials.erase(cred);
|
credentials.erase(cred);
|
||||||
|
if (defaultSSID == ssid) {
|
||||||
|
defaultSSID.clear();
|
||||||
|
}
|
||||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
@ -148,6 +181,107 @@ bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { re
|
|||||||
|
|
||||||
void WifiCredentialStore::clearAll() {
|
void WifiCredentialStore::clearAll() {
|
||||||
credentials.clear();
|
credentials.clear();
|
||||||
|
defaultSSID.clear();
|
||||||
saveToFile();
|
saveToFile();
|
||||||
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
|
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();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
class ActivityWithSubactivity;
|
||||||
|
class GfxRenderer;
|
||||||
|
class MappedInputManager;
|
||||||
|
|
||||||
struct WifiCredential {
|
struct WifiCredential {
|
||||||
std::string ssid;
|
std::string ssid;
|
||||||
std::string password; // Stored obfuscated in file
|
std::string password; // Stored obfuscated in file
|
||||||
@ -16,6 +21,7 @@ class WifiCredentialStore {
|
|||||||
private:
|
private:
|
||||||
static WifiCredentialStore instance;
|
static WifiCredentialStore instance;
|
||||||
std::vector<WifiCredential> credentials;
|
std::vector<WifiCredential> credentials;
|
||||||
|
std::string defaultSSID;
|
||||||
|
|
||||||
static constexpr size_t MAX_NETWORKS = 8;
|
static constexpr size_t MAX_NETWORKS = 8;
|
||||||
|
|
||||||
@ -50,6 +56,16 @@ class WifiCredentialStore {
|
|||||||
|
|
||||||
// Clear all credentials
|
// Clear all credentials
|
||||||
void clearAll();
|
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
|
// Helper macro to access credentials store
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
class ActivityWithSubactivity : public Activity {
|
class ActivityWithSubactivity : public Activity {
|
||||||
protected:
|
protected:
|
||||||
std::unique_ptr<Activity> subActivity = nullptr;
|
std::unique_ptr<Activity> subActivity = nullptr;
|
||||||
void exitActivity();
|
|
||||||
void enterNewActivity(Activity* activity);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
void exitActivity();
|
||||||
|
void enterNewActivity(Activity* activity);
|
||||||
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
: Activity(std::move(name), renderer, mappedInput) {}
|
: Activity(std::move(name), renderer, mappedInput) {}
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "ScreenComponents.h"
|
#include "ScreenComponents.h"
|
||||||
#include "WifiCredentialStore.h"
|
#include "WifiCredentialStore.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
#include "network/HttpDownloader.h"
|
#include "network/HttpDownloader.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
@ -66,6 +67,11 @@ void OpdsBookBrowserActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::loop() {
|
void OpdsBookBrowserActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle error state - Confirm retries, Back goes back or home
|
// Handle error state - Confirm retries, Back goes back or home
|
||||||
if (state == BrowserState::ERROR) {
|
if (state == BrowserState::ERROR) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
@ -82,6 +88,9 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
// Handle WiFi check state - only Back works
|
// Handle WiFi check state - only Back works
|
||||||
if (state == BrowserState::CHECK_WIFI) {
|
if (state == BrowserState::CHECK_WIFI) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
if (subActivity) {
|
||||||
|
exitActivity();
|
||||||
|
}
|
||||||
onGoHome();
|
onGoHome();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -139,6 +148,12 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
|
|
||||||
void OpdsBookBrowserActivity::displayTaskLoop() {
|
void OpdsBookBrowserActivity::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
// If a subactivity is active, yield CPU time but don't render
|
||||||
|
if (subActivity) {
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
@ -350,49 +365,24 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
||||||
// Already connected?
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
state = BrowserState::LOADING;
|
|
||||||
statusMessage = "Loading...";
|
|
||||||
updateRequired = true;
|
|
||||||
fetchFeed(currentPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to connect using saved credentials
|
|
||||||
statusMessage = "Connecting to WiFi...";
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
WIFI_STORE.loadFromFile();
|
WIFI_STORE.loadFromFile();
|
||||||
const auto& credentials = WIFI_STORE.getCredentials();
|
const bool hasDefaultSSID = !WIFI_STORE.getDefaultSSID().empty();
|
||||||
if (credentials.empty()) {
|
const bool alreadyConnected = (WiFi.status() == WL_CONNECTED);
|
||||||
state = BrowserState::ERROR;
|
|
||||||
errorMessage = "No WiFi credentials saved";
|
if (hasDefaultSSID && !alreadyConnected) {
|
||||||
|
statusMessage = "Connecting to WiFi...";
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first saved credential
|
WIFI_STORE.ensureWifiConnected(
|
||||||
const auto& cred = credentials[0];
|
*this, renderer, mappedInput,
|
||||||
WiFi.mode(WIFI_STA);
|
[this]() {
|
||||||
WiFi.begin(cred.ssid.c_str(), cred.password.c_str());
|
|
||||||
|
|
||||||
// Wait for connection with timeout
|
|
||||||
constexpr int WIFI_TIMEOUT_MS = 10000;
|
|
||||||
const unsigned long startTime = millis();
|
|
||||||
while (WiFi.status() != WL_CONNECTED && millis() - startTime < WIFI_TIMEOUT_MS) {
|
|
||||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
Serial.printf("[%lu] [OPDS] WiFi connected: %s\n", millis(), WiFi.localIP().toString().c_str());
|
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = "Loading...";
|
statusMessage = "Loading...";
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
} else {
|
},
|
||||||
state = BrowserState::ERROR;
|
[this]() {
|
||||||
errorMessage = "WiFi connection failed";
|
onGoHome();
|
||||||
updateRequired = true;
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,13 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for browsing and downloading books from an OPDS server.
|
* Activity for browsing and downloading books from an OPDS server.
|
||||||
* Supports navigation through catalog hierarchy and downloading EPUBs.
|
* Supports navigation through catalog hierarchy and downloading EPUBs.
|
||||||
*/
|
*/
|
||||||
class OpdsBookBrowserActivity final : public Activity {
|
class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
enum class BrowserState {
|
enum class BrowserState {
|
||||||
CHECK_WIFI, // Checking WiFi connection
|
CHECK_WIFI, // Checking WiFi connection
|
||||||
@ -26,7 +26,7 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
|
|
||||||
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void()>& onGoHome)
|
const std::function<void()>& onGoHome)
|
||||||
: Activity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome) {}
|
: ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome) {}
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "NetworkModeSelectionActivity.h"
|
#include "NetworkModeSelectionActivity.h"
|
||||||
#include "WifiSelectionActivity.h"
|
#include "WifiCredentialStore.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -135,14 +135,24 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
if (mode == NetworkMode::JOIN_NETWORK) {
|
if (mode == NetworkMode::JOIN_NETWORK) {
|
||||||
// STA mode - launch WiFi selection
|
// STA mode - use ensureWifiConnected helper
|
||||||
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Checking WiFi connection...\n", millis());
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
state = WebServerActivityState::WIFI_SELECTION;
|
state = WebServerActivityState::WIFI_SELECTION;
|
||||||
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
WIFI_STORE.ensureWifiConnected(
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
*this, renderer, mappedInput,
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
[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 {
|
} else {
|
||||||
// AP mode - start access point
|
// AP mode - start access point
|
||||||
state = WebServerActivityState::AP_STARTING;
|
state = WebServerActivityState::AP_STARTING;
|
||||||
@ -152,15 +162,15 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
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) {
|
if (connected) {
|
||||||
// Get connection info before exiting subactivity
|
|
||||||
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
|
||||||
connectedSSID = WiFi.SSID().c_str();
|
|
||||||
isApMode = false;
|
isApMode = false;
|
||||||
|
|
||||||
exitActivity();
|
// Exit any subactivity if present
|
||||||
|
if (subActivity) {
|
||||||
|
exitActivity();
|
||||||
|
}
|
||||||
|
|
||||||
// Start mDNS for hostname resolution
|
// Start mDNS for hostname resolution
|
||||||
if (MDNS.begin(AP_HOSTNAME)) {
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ enum class WebServerActivityState {
|
|||||||
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
||||||
* It:
|
* It:
|
||||||
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
|
* - 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
|
* - For AP mode: Creates an Access Point that clients can connect to
|
||||||
* - Starts the CrossPointWebServer when connected
|
* - Starts the CrossPointWebServer when connected
|
||||||
* - Handles client requests in its loop() function
|
* - Handles client requests in its loop() function
|
||||||
|
|||||||
@ -36,6 +36,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
usedSavedPassword = false;
|
usedSavedPassword = false;
|
||||||
savePromptSelection = 0;
|
savePromptSelection = 0;
|
||||||
forgetPromptSelection = 0;
|
forgetPromptSelection = 0;
|
||||||
|
setDefaultPromptSelection = 0;
|
||||||
|
|
||||||
// Trigger first update to show scanning message
|
// Trigger first update to show scanning message
|
||||||
updateRequired = true;
|
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() {
|
void WifiSelectionActivity::checkConnectionStatus() {
|
||||||
if (state != WifiSelectionState::CONNECTING) {
|
if (state != WifiSelectionState::CONNECTING) {
|
||||||
return;
|
return;
|
||||||
@ -243,16 +266,19 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||||
connectedIP = ipStr;
|
connectedIP = ipStr;
|
||||||
|
|
||||||
// If we entered a new password, ask if user wants to save it
|
// If we entered a new password, save it (with or without prompt based on fromSettingsScreen)
|
||||||
// Otherwise, immediately complete so parent can start web server
|
|
||||||
if (!usedSavedPassword && !enteredPassword.empty()) {
|
if (!usedSavedPassword && !enteredPassword.empty()) {
|
||||||
state = WifiSelectionState::SAVE_PROMPT;
|
if (fromSettingsScreen) {
|
||||||
savePromptSelection = 0; // Default to "Yes"
|
// Always save without prompting
|
||||||
updateRequired = true;
|
savePassword();
|
||||||
} else {
|
displaySetDefaultPrompt();
|
||||||
// Using saved password or open network - complete immediately
|
|
||||||
Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis());
|
return;
|
||||||
onComplete(true);
|
} else {
|
||||||
|
state = WifiSelectionState::SAVE_PROMPT;
|
||||||
|
savePromptSelection = 0;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -318,14 +344,50 @@ void WifiSelectionActivity::loop() {
|
|||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
if (savePromptSelection == 0) {
|
if (savePromptSelection == 0) {
|
||||||
// User chose "Yes" - save the password
|
// User chose "Yes" - save the password
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
savePassword();
|
||||||
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
|
displaySetDefaultPrompt();
|
||||||
xSemaphoreGive(renderingMutex);
|
}
|
||||||
|
onComplete(true);
|
||||||
|
}else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
// Skip saving, complete anyway
|
||||||
|
onComplete(true);
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
// Complete - parent will start web server
|
|
||||||
onComplete(true);
|
onComplete(true);
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
// Skip saving, complete anyway
|
// Disconnect if from settings screen before completing
|
||||||
|
if (fromSettingsScreen) {
|
||||||
|
WiFi.disconnect(false);
|
||||||
|
delay(100);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
}
|
||||||
onComplete(true);
|
onComplete(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -489,6 +551,9 @@ void WifiSelectionActivity::render() const {
|
|||||||
case WifiSelectionState::SAVE_PROMPT:
|
case WifiSelectionState::SAVE_PROMPT:
|
||||||
renderSavePrompt();
|
renderSavePrompt();
|
||||||
break;
|
break;
|
||||||
|
case WifiSelectionState::SET_DEFAULT_PROMPT:
|
||||||
|
renderSetDefaultPrompt();
|
||||||
|
break;
|
||||||
case WifiSelectionState::CONNECTION_FAILED:
|
case WifiSelectionState::CONNECTION_FAILED:
|
||||||
renderConnectionFailed();
|
renderConnectionFailed();
|
||||||
break;
|
break;
|
||||||
@ -655,6 +720,46 @@ void WifiSelectionActivity::renderSavePrompt() const {
|
|||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
|
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 {
|
void WifiSelectionActivity::renderConnectionFailed() const {
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
|
|||||||
@ -21,14 +21,15 @@ struct WifiNetworkInfo {
|
|||||||
|
|
||||||
// WiFi selection states
|
// WiFi selection states
|
||||||
enum class WifiSelectionState {
|
enum class WifiSelectionState {
|
||||||
SCANNING, // Scanning for networks
|
SCANNING, // Scanning for networks
|
||||||
NETWORK_LIST, // Displaying available networks
|
NETWORK_LIST, // Displaying available networks
|
||||||
PASSWORD_ENTRY, // Entering password for selected network
|
PASSWORD_ENTRY, // Entering password for selected network
|
||||||
CONNECTING, // Attempting to connect
|
CONNECTING, // Attempting to connect
|
||||||
CONNECTED, // Successfully connected
|
CONNECTED, // Successfully connected
|
||||||
SAVE_PROMPT, // Asking user if they want to save the password
|
SAVE_PROMPT, // Asking user if they want to save the password
|
||||||
CONNECTION_FAILED, // Connection failed
|
SET_DEFAULT_PROMPT, // Asking user if they want to set as default
|
||||||
FORGET_PROMPT // Asking user if they want to forget the network
|
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)
|
// Whether network was connected using a saved password (skip save prompt)
|
||||||
bool usedSavedPassword = false;
|
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 savePromptSelection = 0;
|
||||||
int forgetPromptSelection = 0;
|
int forgetPromptSelection = 0;
|
||||||
|
int setDefaultPromptSelection = 0;
|
||||||
|
|
||||||
// Connection timeout
|
// Connection timeout
|
||||||
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
|
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
|
||||||
@ -81,6 +86,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
|||||||
void renderConnecting() const;
|
void renderConnecting() const;
|
||||||
void renderConnected() const;
|
void renderConnected() const;
|
||||||
void renderSavePrompt() const;
|
void renderSavePrompt() const;
|
||||||
|
void renderSetDefaultPrompt() const;
|
||||||
void renderConnectionFailed() const;
|
void renderConnectionFailed() const;
|
||||||
void renderForgetPrompt() const;
|
void renderForgetPrompt() const;
|
||||||
|
|
||||||
@ -91,10 +97,17 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
|||||||
void checkConnectionStatus();
|
void checkConnectionStatus();
|
||||||
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
void savePassword();
|
||||||
|
void displaySetDefaultPrompt();
|
||||||
|
void setDefaultNetwork();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void(bool connected)>& onComplete)
|
const std::function<void(bool connected)>& onComplete,
|
||||||
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
|
bool fromSettingsScreen = false)
|
||||||
|
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete),
|
||||||
|
fromSettingsScreen(fromSettingsScreen) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -9,11 +9,12 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
|
#include "WifiConnectionsActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int settingsCount = 18;
|
constexpr int settingsCount = 19;
|
||||||
const SettingInfo settingsList[settingsCount] = {
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
|
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
|
||||||
@ -40,6 +41,7 @@ const SettingInfo settingsList[settingsCount] = {
|
|||||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
||||||
SettingInfo::Action("Calibre Settings"),
|
SettingInfo::Action("Calibre Settings"),
|
||||||
|
SettingInfo::Action("WiFi Connections"),
|
||||||
SettingInfo::Action("Check for updates")};
|
SettingInfo::Action("Check for updates")};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -145,6 +147,14 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
xSemaphoreGive(renderingMutex);
|
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) {
|
} else if (strcmp(setting.name, "Check for updates") == 0) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|||||||
271
src/activities/settings/WifiConnectionsActivity.cpp
Normal file
271
src/activities/settings/WifiConnectionsActivity.cpp
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 > credentials.size()) {
|
||||||
|
selectorIndex = credentials.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
50
src/activities/settings/WifiConnectionsActivity.h
Normal file
50
src/activities/settings/WifiConnectionsActivity.h
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#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();
|
||||||
|
};
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user