mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 15:47:39 +03:00
Compare commits
12 Commits
233978859a
...
06f9784b5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06f9784b5c | ||
|
|
21277e03eb | ||
|
|
4eef2b5793 | ||
|
|
5a55fa1c6e | ||
|
|
c98ba142e8 | ||
|
|
c1c94c0112 | ||
|
|
eb84bcee7c | ||
|
|
d45f355e87 | ||
|
|
e4bfcf0d77 | ||
|
|
a2d4e6936f | ||
|
|
145c7338a9 | ||
|
|
cd1a441d2b |
@ -96,6 +96,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- Left, Right, Back, Confirm
|
||||
- Left, Back, Confirm, Right
|
||||
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
|
||||
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
|
||||
- "Page Scroll" - Long-pressing scrolls a page up/down
|
||||
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
|
||||
- **Reader Font Family**: Choose the font used for reading:
|
||||
- "Bookerly" (default) - Amazon's reading font
|
||||
- "Noto Sans" - Google's sans-serif font
|
||||
@ -144,6 +148,9 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
|
||||
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
|
||||
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
|
||||
|
||||
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
|
||||
|
||||
|
||||
### System Navigation
|
||||
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
|
||||
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
|
||||
@ -25,7 +25,7 @@ constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
||||
const char* IMAGE_TAGS[] = {"img"};
|
||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
|
||||
const char* SKIP_TAGS[] = {"head", "table"};
|
||||
const char* SKIP_TAGS[] = {"head"};
|
||||
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
|
||||
|
||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||
@ -63,13 +63,44 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
// Special handling for tables - show placeholder text instead of dropping silently
|
||||
if (strcmp(name, "table") == 0) {
|
||||
// Add placeholder text
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
if (self->currentTextBlock) {
|
||||
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
|
||||
}
|
||||
|
||||
// Skip table contents
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
std::string alt;
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "alt") == 0) {
|
||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
||||
}
|
||||
}
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
|
||||
} else {
|
||||
// Skip for now
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||
// start skip
|
||||
self->skipUntilDepth = self->depth;
|
||||
|
||||
@ -468,7 +468,10 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
}
|
||||
|
||||
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const {
|
||||
const char* btn4) {
|
||||
const Orientation orig_orientation = getOrientation();
|
||||
setOrientation(Orientation::Portrait);
|
||||
|
||||
const int pageHeight = getScreenHeight();
|
||||
constexpr int buttonWidth = 106;
|
||||
constexpr int buttonHeight = 40;
|
||||
@ -481,12 +484,15 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
|
||||
// Only draw if the label is non-empty
|
||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||
const int x = buttonPositions[i];
|
||||
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
|
||||
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
|
||||
const int textWidth = getTextWidth(fontId, labels[i]);
|
||||
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
|
||||
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
setOrientation(orig_orientation);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {
|
||||
|
||||
@ -84,7 +84,7 @@ class GfxRenderer {
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
|
||||
// UI Components
|
||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
|
||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
|
||||
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
|
||||
|
||||
private:
|
||||
|
||||
@ -45,9 +45,9 @@ lib_deps =
|
||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
||||
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
||||
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
||||
ArduinoJson @ 7.4.2
|
||||
QRCode @ 0.0.1
|
||||
links2004/WebSockets @ ^2.4.1
|
||||
bblanchon/ArduinoJson @ 7.4.2
|
||||
ricmoo/QRCode @ 0.0.1
|
||||
links2004/WebSockets @ 2.7.3
|
||||
|
||||
[env:default]
|
||||
extends = base
|
||||
|
||||
@ -3,6 +3,11 @@
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
#include <WiFi.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
|
||||
// Initialize the static instance
|
||||
WifiCredentialStore WifiCredentialStore::instance;
|
||||
@ -53,6 +58,10 @@ bool WifiCredentialStore::saveToFile() const {
|
||||
serialization::writeString(file, obfuscatedPwd);
|
||||
}
|
||||
|
||||
// Write default SSID
|
||||
serialization::writeString(file, defaultSSID);
|
||||
Serial.printf("[%lu] [WCS] Saving default SSID: %s\n", millis(), defaultSSID.c_str());
|
||||
|
||||
file.close();
|
||||
Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size());
|
||||
return true;
|
||||
@ -95,6 +104,27 @@ bool WifiCredentialStore::loadFromFile() {
|
||||
credentials.push_back(cred);
|
||||
}
|
||||
|
||||
// Try to read default SSID if it exists
|
||||
defaultSSID.clear();
|
||||
if (file.available() >= 4) {
|
||||
const uint32_t posBefore = file.position();
|
||||
uint32_t len = 0;
|
||||
serialization::readPod(file, len);
|
||||
|
||||
if (file.available() >= len && len <= 64) {
|
||||
defaultSSID.resize(len);
|
||||
const size_t bytesRead = file.read(reinterpret_cast<uint8_t*>(&defaultSSID[0]), len);
|
||||
if (bytesRead == len) {
|
||||
Serial.printf("[%lu] [WCS] Loaded default SSID: %s\n", millis(), defaultSSID.c_str());
|
||||
} else {
|
||||
file.seek(posBefore);
|
||||
defaultSSID.clear();
|
||||
}
|
||||
} else {
|
||||
file.seek(posBefore);
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size());
|
||||
return true;
|
||||
@ -127,6 +157,9 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) {
|
||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||
if (cred != credentials.end()) {
|
||||
credentials.erase(cred);
|
||||
if (defaultSSID == ssid) {
|
||||
defaultSSID.clear();
|
||||
}
|
||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
@ -148,6 +181,107 @@ bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { re
|
||||
|
||||
void WifiCredentialStore::clearAll() {
|
||||
credentials.clear();
|
||||
defaultSSID.clear();
|
||||
saveToFile();
|
||||
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
|
||||
}
|
||||
|
||||
void WifiCredentialStore::setDefaultSSID(const std::string& ssid) {
|
||||
defaultSSID = ssid;
|
||||
saveToFile();
|
||||
Serial.printf("[%lu] [WCS] Set default SSID: %s\n", millis(), ssid.c_str());
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::connectToDefaultWifi(int timeoutMs) const {
|
||||
if (defaultSSID.empty()) {
|
||||
Serial.printf("[%lu] [WCS] No default SSID set\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* cred = findCredential(defaultSSID);
|
||||
if (!cred) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick check: scan to see if the SSID is available before attempting connection
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect(false);
|
||||
delay(100);
|
||||
|
||||
Serial.printf("[%lu] [WCS] Scanning for SSID: %s\n", millis(), defaultSSID.c_str());
|
||||
WiFi.scanNetworks(false);
|
||||
|
||||
const unsigned long scanStart = millis();
|
||||
int16_t scanResult = WiFi.scanComplete();
|
||||
while (scanResult == WIFI_SCAN_RUNNING && millis() - scanStart < 3000) {
|
||||
delay(100);
|
||||
scanResult = WiFi.scanComplete();
|
||||
}
|
||||
|
||||
if (scanResult > 0) {
|
||||
bool ssidFound = false;
|
||||
for (int i = 0; i < scanResult; i++) {
|
||||
std::string scannedSSID = WiFi.SSID(i).c_str();
|
||||
if (scannedSSID == defaultSSID) {
|
||||
ssidFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WiFi.scanDelete();
|
||||
|
||||
if (!ssidFound) {
|
||||
Serial.printf("[%lu] [WCS] SSID not found in scan results, skipping connection attempt\n", millis());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
WiFi.scanDelete();
|
||||
return false;
|
||||
}
|
||||
|
||||
WiFi.begin(defaultSSID.c_str(), cred->password.c_str());
|
||||
|
||||
Serial.printf("[%lu] [WCS] Connecting to default WiFi: %s\n", millis(), defaultSSID.c_str());
|
||||
const unsigned long startTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && millis() - startTime < static_cast<unsigned long>(timeoutMs)) {
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("[%lu] [WCS] Connected to default WiFi: %s (IP: %s)\n", millis(), defaultSSID.c_str(),
|
||||
WiFi.localIP().toString().c_str());
|
||||
return true;
|
||||
} else {
|
||||
Serial.printf("[%lu] [WCS] Failed to connect to default WiFi: %s\n", millis(), defaultSSID.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void WifiCredentialStore::ensureWifiConnected(ActivityWithSubactivity& activity, GfxRenderer& renderer,
|
||||
MappedInputManager& mappedInput, const std::function<void()>& onSuccess,
|
||||
const std::function<void()>& onCancel, int timeoutMs) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to connect using default WiFi
|
||||
WifiCredentialStore::getInstance().loadFromFile();
|
||||
if (WifiCredentialStore::getInstance().connectToDefaultWifi(timeoutMs)) {
|
||||
Serial.printf("[%lu] [WCS] Auto-connected to WiFi\n", millis());
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-connect failed - show WiFi selection list
|
||||
Serial.printf("[%lu] [WCS] Auto-connect failed, showing WiFi selection\n", millis());
|
||||
activity.enterNewActivity(
|
||||
new WifiSelectionActivity(renderer, mappedInput, [&activity, onSuccess, onCancel](bool connected) {
|
||||
activity.exitActivity();
|
||||
if (connected) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ActivityWithSubactivity;
|
||||
class GfxRenderer;
|
||||
class MappedInputManager;
|
||||
|
||||
struct WifiCredential {
|
||||
std::string ssid;
|
||||
std::string password; // Stored obfuscated in file
|
||||
@ -16,6 +21,7 @@ class WifiCredentialStore {
|
||||
private:
|
||||
static WifiCredentialStore instance;
|
||||
std::vector<WifiCredential> credentials;
|
||||
std::string defaultSSID;
|
||||
|
||||
static constexpr size_t MAX_NETWORKS = 8;
|
||||
|
||||
@ -50,6 +56,16 @@ class WifiCredentialStore {
|
||||
|
||||
// Clear all credentials
|
||||
void clearAll();
|
||||
|
||||
// Default network management
|
||||
void setDefaultSSID(const std::string& ssid);
|
||||
const std::string& getDefaultSSID() const { return defaultSSID; }
|
||||
bool connectToDefaultWifi(int timeoutMs = 5000) const;
|
||||
|
||||
// Helper function to try connecting to default WiFi, or show WiFi selection if it fails
|
||||
static void ensureWifiConnected(ActivityWithSubactivity& activity, GfxRenderer& renderer,
|
||||
MappedInputManager& mappedInput, const std::function<void()>& onSuccess,
|
||||
const std::function<void()>& onCancel, int timeoutMs = 10000);
|
||||
};
|
||||
|
||||
// Helper macro to access credentials store
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
class ActivityWithSubactivity : public Activity {
|
||||
protected:
|
||||
std::unique_ptr<Activity> subActivity = nullptr;
|
||||
void exitActivity();
|
||||
void enterNewActivity(Activity* activity);
|
||||
|
||||
public:
|
||||
void exitActivity();
|
||||
void enterNewActivity(Activity* activity);
|
||||
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity(std::move(name), renderer, mappedInput) {}
|
||||
void loop() override;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "ScreenComponents.h"
|
||||
#include "WifiCredentialStore.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "fontIds.h"
|
||||
#include "network/HttpDownloader.h"
|
||||
@ -154,6 +155,12 @@ void OpdsBookBrowserActivity::loop() {
|
||||
|
||||
void OpdsBookBrowserActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
// If a subactivity is active, yield CPU time but don't render
|
||||
if (subActivity) {
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
@ -374,8 +381,27 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Not connected - launch WiFi selection screen directly
|
||||
launchWifiSelection();
|
||||
// Try to connect to default WiFi if available
|
||||
WIFI_STORE.loadFromFile();
|
||||
const bool hasDefaultSSID = !WIFI_STORE.getDefaultSSID().empty();
|
||||
|
||||
if (hasDefaultSSID) {
|
||||
statusMessage = "Connecting to WiFi...";
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
WIFI_STORE.ensureWifiConnected(
|
||||
*this, renderer, mappedInput,
|
||||
[this]() {
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = "Loading...";
|
||||
updateRequired = true;
|
||||
fetchFeed(currentPath);
|
||||
},
|
||||
[this]() {
|
||||
// User cancelled WiFi selection - go home
|
||||
onGoHome();
|
||||
});
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::launchWifiSelection() {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "NetworkModeSelectionActivity.h"
|
||||
#include "WifiSelectionActivity.h"
|
||||
#include "WifiCredentialStore.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
@ -135,14 +135,24 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
||||
exitActivity();
|
||||
|
||||
if (mode == NetworkMode::JOIN_NETWORK) {
|
||||
// STA mode - launch WiFi selection
|
||||
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
||||
// STA mode - use ensureWifiConnected helper
|
||||
Serial.printf("[%lu] [WEBACT] Checking WiFi connection...\n", millis());
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
state = WebServerActivityState::WIFI_SELECTION;
|
||||
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
WIFI_STORE.ensureWifiConnected(
|
||||
*this, renderer, mappedInput,
|
||||
[this]() {
|
||||
// WiFi connected - start web server
|
||||
connectedIP = WiFi.localIP().toString().c_str();
|
||||
connectedSSID = WiFi.SSID().c_str();
|
||||
isApMode = false;
|
||||
onWifiSelectionComplete(true);
|
||||
},
|
||||
[this]() {
|
||||
// WiFi connection cancelled - go back to mode selection
|
||||
onWifiSelectionComplete(false);
|
||||
});
|
||||
} else {
|
||||
// AP mode - start access point
|
||||
state = WebServerActivityState::AP_STARTING;
|
||||
@ -152,15 +162,15 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
||||
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
||||
Serial.printf("[%lu] [WEBACT] WiFi connection completed, connected=%d\n", millis(), connected);
|
||||
|
||||
if (connected) {
|
||||
// Get connection info before exiting subactivity
|
||||
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
||||
connectedSSID = WiFi.SSID().c_str();
|
||||
isApMode = false;
|
||||
|
||||
exitActivity();
|
||||
// Exit any subactivity if present
|
||||
if (subActivity) {
|
||||
exitActivity();
|
||||
}
|
||||
|
||||
// Start mDNS for hostname resolution
|
||||
if (MDNS.begin(AP_HOSTNAME)) {
|
||||
|
||||
@ -24,7 +24,7 @@ enum class WebServerActivityState {
|
||||
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
||||
* It:
|
||||
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
|
||||
* - For STA mode: Launches WifiSelectionActivity to connect to an existing network
|
||||
* - For STA mode: Uses ensureWifiConnected to auto-connect to default WiFi or show WiFi selection
|
||||
* - For AP mode: Creates an Access Point that clients can connect to
|
||||
* - Starts the CrossPointWebServer when connected
|
||||
* - Handles client requests in its loop() function
|
||||
|
||||
@ -36,6 +36,15 @@ void WifiSelectionActivity::onEnter() {
|
||||
usedSavedPassword = false;
|
||||
savePromptSelection = 0;
|
||||
forgetPromptSelection = 0;
|
||||
setDefaultPromptSelection = 0;
|
||||
|
||||
// Cache MAC address for display
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
char macStr[32];
|
||||
snprintf(macStr, sizeof(macStr), "MAC address: %02x-%02x-%02x-%02x-%02x-%02x", mac[0], mac[1], mac[2], mac[3], mac[4],
|
||||
mac[5]);
|
||||
cachedMacAddress = std::string(macStr);
|
||||
|
||||
// Trigger first update to show scanning message
|
||||
updateRequired = true;
|
||||
@ -229,6 +238,28 @@ void WifiSelectionActivity::attemptConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::savePassword() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::displaySetDefaultPrompt() {
|
||||
WIFI_STORE.loadFromFile();
|
||||
const std::string currentDefault = WIFI_STORE.getDefaultSSID();
|
||||
if (selectedSSID != currentDefault) {
|
||||
state = WifiSelectionState::SET_DEFAULT_PROMPT;
|
||||
setDefaultPromptSelection = 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::setDefaultNetwork() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
WIFI_STORE.setDefaultSSID(selectedSSID);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::checkConnectionStatus() {
|
||||
if (state != WifiSelectionState::CONNECTING) {
|
||||
return;
|
||||
@ -243,16 +274,19 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
||||
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||
connectedIP = ipStr;
|
||||
|
||||
// If we entered a new password, ask if user wants to save it
|
||||
// Otherwise, immediately complete so parent can start web server
|
||||
// If we entered a new password, save it (with or without prompt based on fromSettingsScreen)
|
||||
if (!usedSavedPassword && !enteredPassword.empty()) {
|
||||
state = WifiSelectionState::SAVE_PROMPT;
|
||||
savePromptSelection = 0; // Default to "Yes"
|
||||
updateRequired = true;
|
||||
} else {
|
||||
// Using saved password or open network - complete immediately
|
||||
Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis());
|
||||
onComplete(true);
|
||||
if (fromSettingsScreen) {
|
||||
// Always save without prompting
|
||||
savePassword();
|
||||
displaySetDefaultPrompt();
|
||||
|
||||
return;
|
||||
} else {
|
||||
state = WifiSelectionState::SAVE_PROMPT;
|
||||
savePromptSelection = 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -318,11 +352,9 @@ void WifiSelectionActivity::loop() {
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (savePromptSelection == 0) {
|
||||
// User chose "Yes" - save the password
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
savePassword();
|
||||
displaySetDefaultPrompt();
|
||||
}
|
||||
// Complete - parent will start web server
|
||||
onComplete(true);
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
// Skip saving, complete anyway
|
||||
@ -331,6 +363,44 @@ void WifiSelectionActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle set default prompt state
|
||||
if (state == WifiSelectionState::SET_DEFAULT_PROMPT) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
if (setDefaultPromptSelection > 0) {
|
||||
setDefaultPromptSelection--;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
if (setDefaultPromptSelection < 1) {
|
||||
setDefaultPromptSelection++;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (setDefaultPromptSelection == 0) {
|
||||
// User chose "Yes" - set as default
|
||||
setDefaultNetwork();
|
||||
}
|
||||
// Disconnect if from settings screen before completing
|
||||
if (fromSettingsScreen) {
|
||||
WiFi.disconnect(false);
|
||||
delay(100);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
onComplete(true);
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
// Disconnect if from settings screen before completing
|
||||
if (fromSettingsScreen) {
|
||||
WiFi.disconnect(false);
|
||||
delay(100);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
onComplete(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle forget prompt state (connection failed with saved credentials)
|
||||
if (state == WifiSelectionState::FORGET_PROMPT) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
@ -489,6 +559,9 @@ void WifiSelectionActivity::render() const {
|
||||
case WifiSelectionState::SAVE_PROMPT:
|
||||
renderSavePrompt();
|
||||
break;
|
||||
case WifiSelectionState::SET_DEFAULT_PROMPT:
|
||||
renderSetDefaultPrompt();
|
||||
break;
|
||||
case WifiSelectionState::CONNECTION_FAILED:
|
||||
renderConnectionFailed();
|
||||
break;
|
||||
@ -572,6 +645,9 @@ void WifiSelectionActivity::renderNetworkList() const {
|
||||
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
|
||||
}
|
||||
|
||||
// Show MAC address above the network count and legend
|
||||
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
|
||||
|
||||
// Draw help text
|
||||
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");
|
||||
@ -655,6 +731,46 @@ void WifiSelectionActivity::renderSavePrompt() const {
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderSetDefaultPrompt() const {
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
const auto top = (pageHeight - height * 3) / 2;
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connected!", true, EpdFontFamily::BOLD);
|
||||
|
||||
std::string ssidInfo = "Network: " + selectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
|
||||
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Set as default WiFi?");
|
||||
|
||||
// Draw Yes/No buttons
|
||||
const int buttonY = top + 80;
|
||||
constexpr int buttonWidth = 60;
|
||||
constexpr int buttonSpacing = 30;
|
||||
constexpr int totalWidth = buttonWidth * 2 + buttonSpacing;
|
||||
const int startX = (pageWidth - totalWidth) / 2;
|
||||
|
||||
// Draw "Yes" button
|
||||
if (setDefaultPromptSelection == 0) {
|
||||
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]");
|
||||
} else {
|
||||
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes");
|
||||
}
|
||||
|
||||
// Draw "No" button
|
||||
if (setDefaultPromptSelection == 1) {
|
||||
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]");
|
||||
} else {
|
||||
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No");
|
||||
}
|
||||
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm");
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderConnectionFailed() const {
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
|
||||
@ -21,14 +21,15 @@ struct WifiNetworkInfo {
|
||||
|
||||
// WiFi selection states
|
||||
enum class WifiSelectionState {
|
||||
SCANNING, // Scanning for networks
|
||||
NETWORK_LIST, // Displaying available networks
|
||||
PASSWORD_ENTRY, // Entering password for selected network
|
||||
CONNECTING, // Attempting to connect
|
||||
CONNECTED, // Successfully connected
|
||||
SAVE_PROMPT, // Asking user if they want to save the password
|
||||
CONNECTION_FAILED, // Connection failed
|
||||
FORGET_PROMPT // Asking user if they want to forget the network
|
||||
SCANNING, // Scanning for networks
|
||||
NETWORK_LIST, // Displaying available networks
|
||||
PASSWORD_ENTRY, // Entering password for selected network
|
||||
CONNECTING, // Attempting to connect
|
||||
CONNECTED, // Successfully connected
|
||||
SAVE_PROMPT, // Asking user if they want to save the password
|
||||
SET_DEFAULT_PROMPT, // Asking user if they want to set as default
|
||||
CONNECTION_FAILED, // Connection failed
|
||||
FORGET_PROMPT // Asking user if they want to forget the network
|
||||
};
|
||||
|
||||
/**
|
||||
@ -62,12 +63,19 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
||||
// Password to potentially save (from keyboard or saved credentials)
|
||||
std::string enteredPassword;
|
||||
|
||||
// Cached MAC address string for display
|
||||
std::string cachedMacAddress;
|
||||
|
||||
// Whether network was connected using a saved password (skip save prompt)
|
||||
bool usedSavedPassword = false;
|
||||
|
||||
// Save/forget prompt selection (0 = Yes, 1 = No)
|
||||
// Whether launched from settings screen (affects save behavior and disconnection)
|
||||
bool fromSettingsScreen = false;
|
||||
|
||||
// Save/forget/set default prompt selection (0 = Yes, 1 = No)
|
||||
int savePromptSelection = 0;
|
||||
int forgetPromptSelection = 0;
|
||||
int setDefaultPromptSelection = 0;
|
||||
|
||||
// Connection timeout
|
||||
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
|
||||
@ -81,6 +89,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
||||
void renderConnecting() const;
|
||||
void renderConnected() const;
|
||||
void renderSavePrompt() const;
|
||||
void renderSetDefaultPrompt() const;
|
||||
void renderConnectionFailed() const;
|
||||
void renderForgetPrompt() const;
|
||||
|
||||
@ -91,10 +100,17 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
||||
void checkConnectionStatus();
|
||||
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
||||
|
||||
// Helper methods
|
||||
void savePassword();
|
||||
void displaySetDefaultPrompt();
|
||||
void setDefaultNetwork();
|
||||
|
||||
public:
|
||||
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(bool connected)>& onComplete)
|
||||
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
|
||||
const std::function<void(bool connected)>& onComplete, bool fromSettingsScreen = false)
|
||||
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput),
|
||||
onComplete(onComplete),
|
||||
fromSettingsScreen(fromSettingsScreen) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
@ -127,7 +127,7 @@ void XtcReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool skipPages = mappedInput.getHeldTime() > skipPageMs;
|
||||
const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs;
|
||||
const int skipAmount = skipPages ? 10 : 1;
|
||||
|
||||
if (prevReleased) {
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "WifiConnectionsActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
// Define the static settings list
|
||||
@ -42,6 +43,7 @@ const SettingInfo settingsList[settingsCount] = {
|
||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
||||
SettingInfo::Action("Calibre Settings"),
|
||||
SettingInfo::Action("WiFi Connections"),
|
||||
SettingInfo::Action("Check for updates")};
|
||||
} // namespace
|
||||
|
||||
@ -147,6 +149,14 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "WiFi Connections") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new WifiConnectionsActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "Check for updates") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
|
||||
267
src/activities/settings/WifiConnectionsActivity.cpp
Normal file
267
src/activities/settings/WifiConnectionsActivity.cpp
Normal file
@ -0,0 +1,267 @@
|
||||
#include "WifiConnectionsActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "WifiCredentialStore.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr int PAGE_ITEMS = 23;
|
||||
constexpr int SKIP_PAGE_MS = 700;
|
||||
constexpr unsigned long IGNORE_INPUT_MS = 300;
|
||||
} // namespace
|
||||
|
||||
void WifiConnectionsActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<WifiConnectionsActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
state = State::LIST;
|
||||
selectorIndex = 0;
|
||||
settingsSelection = 0;
|
||||
selectedNetwork.clear();
|
||||
enterTime = millis();
|
||||
updateRequired = true;
|
||||
|
||||
WIFI_STORE.loadFromFile();
|
||||
|
||||
xTaskCreate(&WifiConnectionsActivity::taskTrampoline, "WifiConnectionsTask", 4096, this, 1, &displayTaskHandle);
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned long timeSinceEnter = millis() - enterTime;
|
||||
if (timeSinceEnter < IGNORE_INPUT_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::SETTINGS_MENU) {
|
||||
// Check if this network is already the default
|
||||
WIFI_STORE.loadFromFile();
|
||||
const std::string currentDefault = WIFI_STORE.getDefaultSSID();
|
||||
const bool isDefault = (selectedNetwork == currentDefault);
|
||||
|
||||
// Handle settings menu
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
if (settingsSelection > 0) {
|
||||
settingsSelection--;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
if (settingsSelection < 1) {
|
||||
settingsSelection++;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (settingsSelection == 0) {
|
||||
if (isDefault) {
|
||||
removeDefault();
|
||||
} else {
|
||||
setDefault();
|
||||
}
|
||||
} else {
|
||||
deleteNetwork();
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
cancelSettings();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle list navigation
|
||||
const auto& credentials = WIFI_STORE.getCredentials();
|
||||
const size_t totalItems = credentials.size() + 1; // +1 for "Add new connection"
|
||||
|
||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectorIndex == 0) {
|
||||
// "Add new connection" selected - launch WiFi selection
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new WifiSelectionActivity(
|
||||
renderer, mappedInput,
|
||||
[this](bool connected) {
|
||||
// Reload credentials after WiFi selection
|
||||
WIFI_STORE.loadFromFile();
|
||||
exitActivity();
|
||||
enterTime = millis(); // Reset enter time to ignore input after subactivity exits
|
||||
updateRequired = true;
|
||||
},
|
||||
true)); // true = fromSettingsScreen (always save password and disconnect after)
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else {
|
||||
// Regular credential selected
|
||||
handleSettings();
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onBack();
|
||||
} else if (prevReleased) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + totalItems) % totalItems;
|
||||
} else {
|
||||
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
|
||||
}
|
||||
updateRequired = true;
|
||||
} else if (nextReleased) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % totalItems;
|
||||
} else {
|
||||
selectorIndex = (selectorIndex + 1) % totalItems;
|
||||
}
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::handleSettings() {
|
||||
const auto& credentials = WIFI_STORE.getCredentials();
|
||||
// selectorIndex 0 is "Add new connection", so credentials start at index 1
|
||||
if (selectorIndex == 0 || selectorIndex > credentials.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedNetwork = credentials[selectorIndex - 1].ssid;
|
||||
state = State::SETTINGS_MENU;
|
||||
settingsSelection = 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::setDefault() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
WIFI_STORE.setDefaultSSID(selectedNetwork);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
|
||||
state = State::LIST;
|
||||
selectedNetwork.clear();
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::removeDefault() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
WIFI_STORE.setDefaultSSID("");
|
||||
xSemaphoreGive(renderingMutex);
|
||||
|
||||
state = State::LIST;
|
||||
selectedNetwork.clear();
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::deleteNetwork() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
WIFI_STORE.removeCredential(selectedNetwork);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
|
||||
// Reload to get updated list
|
||||
WIFI_STORE.loadFromFile();
|
||||
|
||||
const auto& credentials = WIFI_STORE.getCredentials();
|
||||
const size_t totalItems = credentials.size() + 1;
|
||||
if (selectorIndex >= totalItems) {
|
||||
selectorIndex = totalItems - 1;
|
||||
}
|
||||
|
||||
state = State::LIST;
|
||||
selectedNetwork.clear();
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::cancelSettings() {
|
||||
state = State::LIST;
|
||||
selectedNetwork.clear();
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired && !subActivity) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void WifiConnectionsActivity::render() {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Connections", true, EpdFontFamily::BOLD);
|
||||
|
||||
if (state == State::SETTINGS_MENU) {
|
||||
const int centerY = pageHeight / 2;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Settings", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 20, selectedNetwork.c_str());
|
||||
|
||||
// Check if this network is already the default
|
||||
const std::string currentDefault = WIFI_STORE.getDefaultSSID();
|
||||
const bool isDefault = (selectedNetwork == currentDefault);
|
||||
|
||||
const char* defaultText = settingsSelection == 0 ? (isDefault ? "> Remove Default" : "> Set Default")
|
||||
: (isDefault ? " Remove Default" : " Set Default");
|
||||
const char* deleteText = settingsSelection == 1 ? "> Delete" : " Delete";
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 20, defaultText);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 40, deleteText);
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Cancel", "Confirm", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
} else {
|
||||
const auto& credentials = WIFI_STORE.getCredentials();
|
||||
|
||||
const auto labels = mappedInput.mapLabels("« Back", selectorIndex == 0 ? "Add" : "Settings", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
const size_t totalItems = credentials.size() + 1;
|
||||
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
||||
const std::string& defaultSSID = WIFI_STORE.getDefaultSSID();
|
||||
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
|
||||
|
||||
for (size_t i = pageStartIndex; i < totalItems && i < pageStartIndex + PAGE_ITEMS; i++) {
|
||||
std::string displayText;
|
||||
if (i == 0) {
|
||||
displayText = "+ Add new connection";
|
||||
} else {
|
||||
displayText = credentials[i - 1].ssid;
|
||||
if (credentials[i - 1].ssid == defaultSSID) {
|
||||
displayText += " [Default]";
|
||||
}
|
||||
}
|
||||
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40);
|
||||
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
46
src/activities/settings/WifiConnectionsActivity.h
Normal file
46
src/activities/settings/WifiConnectionsActivity.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
/**
|
||||
* Activity for managing saved WiFi connections.
|
||||
* Shows a list of saved WiFi networks and allows deletion with confirmation.
|
||||
*/
|
||||
class WifiConnectionsActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
explicit WifiConnectionsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack)
|
||||
: ActivityWithSubactivity("WifiConnections", renderer, mappedInput), onBack(onBack) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
private:
|
||||
enum class State { LIST, SETTINGS_MENU };
|
||||
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
State state = State::LIST;
|
||||
size_t selectorIndex = 0;
|
||||
int settingsSelection = 0;
|
||||
std::string selectedNetwork;
|
||||
unsigned long enterTime = 0;
|
||||
const std::function<void()> onBack;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
void handleSettings();
|
||||
void setDefault();
|
||||
void removeDefault();
|
||||
void deleteNetwork();
|
||||
void cancelSettings();
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user