Refactor layout of activities

This commit is contained in:
Brendan O'Leary 2025-12-18 21:59:38 -05:00
parent e2dab0d811
commit 804db098d1
9 changed files with 414 additions and 176 deletions

View File

@ -6,7 +6,7 @@
#include "config.h"
namespace {
constexpr int menuItemCount = 2;
constexpr int menuItemCount = 3;
}
void HomeActivity::taskTrampoline(void* param) {
@ -51,6 +51,8 @@ void HomeActivity::loop() {
if (selectorIndex == 0) {
onReaderOpen();
} else if (selectorIndex == 1) {
onFileTransferOpen();
} else if (selectorIndex == 2) {
onSettingsOpen();
}
} else if (prevPressed) {
@ -84,7 +86,8 @@ void HomeActivity::render() const {
// Draw selection
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0);
renderer.drawText(UI_FONT_ID, 20, 90, "Settings", selectorIndex != 1);
renderer.drawText(UI_FONT_ID, 20, 90, "File transfer", selectorIndex != 1);
renderer.drawText(UI_FONT_ID, 20, 120, "Settings", selectorIndex != 2);
renderer.drawRect(25, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back");

View File

@ -14,6 +14,7 @@ class HomeActivity final : public Activity {
bool updateRequired = false;
const std::function<void()> onReaderOpen;
const std::function<void()> onSettingsOpen;
const std::function<void()> onFileTransferOpen;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
@ -21,8 +22,8 @@ class HomeActivity final : public Activity {
public:
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen)
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {}
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen)
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@ -0,0 +1,234 @@
#include "CrossPointWebServerActivity.h"
#include <GfxRenderer.h>
#include <WiFi.h>
#include "CrossPointWebServer.h"
#include "config.h"
void CrossPointWebServerActivity::taskTrampoline(void* param) {
auto* self = static_cast<CrossPointWebServerActivity*>(param);
self->displayTaskLoop();
}
void CrossPointWebServerActivity::onEnter() {
Serial.printf("[%lu] [WEBACT] ========== CrossPointWebServerActivity onEnter ==========\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
renderingMutex = xSemaphoreCreateMutex();
// Reset state
state = WebServerActivityState::WIFI_SELECTION;
connectedIP.clear();
connectedSSID.clear();
lastHandleClientTime = 0;
updateRequired = true;
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Turn on WiFi immediately
Serial.printf("[%lu] [WEBACT] Turning on WiFi...\n", millis());
WiFi.mode(WIFI_STA);
// Launch WiFi selection subactivity
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
wifiSelection.reset(new WifiSelectionActivity(renderer, inputManager, [this](bool connected) {
onWifiSelectionComplete(connected);
}));
wifiSelection->onEnter();
}
void CrossPointWebServerActivity::onExit() {
Serial.printf("[%lu] [WEBACT] ========== CrossPointWebServerActivity onExit START ==========\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
state = WebServerActivityState::SHUTTING_DOWN;
// Stop the web server first (before disconnecting WiFi)
stopWebServer();
// Exit WiFi selection subactivity if still active
if (wifiSelection) {
Serial.printf("[%lu] [WEBACT] Exiting WifiSelectionActivity...\n", millis());
wifiSelection->onExit();
wifiSelection.reset();
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity exited\n", millis());
}
// CRITICAL: Wait for LWIP stack to flush any pending packets
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
delay(500);
// Disconnect WiFi gracefully
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
delay(100); // Allow disconnect frame to be sent
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
WiFi.mode(WIFI_OFF);
delay(100); // Allow WiFi hardware to fully power down
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap());
// Acquire mutex before deleting task
Serial.printf("[%lu] [WEBACT] Acquiring rendering mutex before task deletion...\n", millis());
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// Delete the display task
Serial.printf("[%lu] [WEBACT] Deleting display task...\n", millis());
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
Serial.printf("[%lu] [WEBACT] Display task deleted\n", millis());
}
// Delete the mutex
Serial.printf("[%lu] [WEBACT] Deleting mutex...\n", millis());
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
Serial.printf("[%lu] [WEBACT] Mutex deleted\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
Serial.printf("[%lu] [WEBACT] ========== CrossPointWebServerActivity onExit COMPLETE ==========\n", millis());
}
void CrossPointWebServerActivity::onWifiSelectionComplete(bool connected) {
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
if (connected) {
// Get connection info before exiting subactivity
connectedIP = wifiSelection->getConnectedIP();
connectedSSID = WiFi.SSID().c_str();
// Exit the wifi selection subactivity
wifiSelection->onExit();
wifiSelection.reset();
// Start the web server
startWebServer();
} else {
// User cancelled - go back
onGoBack();
}
}
void CrossPointWebServerActivity::startWebServer() {
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
crossPointWebServer.begin();
if (crossPointWebServer.isRunning()) {
state = WebServerActivityState::SERVER_RUNNING;
Serial.printf("[%lu] [WEBACT] Web server started successfully\n", millis());
// Force an immediate render since we're transitioning from a subactivity
// that had its own rendering task. We need to make sure our display is shown.
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
Serial.printf("[%lu] [WEBACT] Rendered File Transfer screen\n", millis());
} else {
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start web server!\n", millis());
// Go back on error
onGoBack();
}
}
void CrossPointWebServerActivity::stopWebServer() {
if (crossPointWebServer.isRunning()) {
Serial.printf("[%lu] [WEBACT] Stopping web server...\n", millis());
crossPointWebServer.stop();
Serial.printf("[%lu] [WEBACT] Web server stopped\n", millis());
}
}
void CrossPointWebServerActivity::loop() {
// Handle different states
switch (state) {
case WebServerActivityState::WIFI_SELECTION:
// Forward loop to WiFi selection subactivity
if (wifiSelection) {
wifiSelection->loop();
}
break;
case WebServerActivityState::SERVER_RUNNING:
// Handle web server requests
if (crossPointWebServer.isRunning()) {
unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
// Log if there's a significant gap between handleClient calls (>100ms)
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
Serial.printf("[%lu] [WEBACT] WARNING: %lu ms gap since last handleClient\n", millis(),
timeSinceLastHandleClient);
}
crossPointWebServer.handleClient();
lastHandleClientTime = millis();
}
// Handle exit on Back button
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
return;
}
break;
case WebServerActivityState::SHUTTING_DOWN:
// Do nothing - waiting for cleanup
break;
}
}
void CrossPointWebServerActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void CrossPointWebServerActivity::render() const {
// Only render our own UI when server is running
// WiFi selection handles its own rendering
if (state == WebServerActivityState::SERVER_RUNNING) {
renderer.clearScreen();
renderServerRunning();
renderer.displayBuffer();
}
}
void CrossPointWebServerActivity::renderServerRunning() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 5) / 2;
renderer.drawCenteredText(READER_FONT_ID, top - 30, "File Transfer", true, BOLD);
std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo = ssidInfo.substr(0, 25) + "...";
}
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR);
// Show web server URL prominently
std::string webInfo = "http://" + connectedIP + "/";
renderer.drawCenteredText(UI_FONT_ID, top + 70, webInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, top + 110, "Open this URL in your browser", true, REGULAR);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press BACK to exit", true, REGULAR);
}

View File

@ -0,0 +1,61 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <memory>
#include <string>
#include "../Activity.h"
#include "WifiSelectionActivity.h"
// Web server activity states
enum class WebServerActivityState {
WIFI_SELECTION, // WiFi selection subactivity is active
SERVER_RUNNING, // Web server is running and handling requests
SHUTTING_DOWN // Shutting down server and WiFi
};
/**
* CrossPointWebServerActivity is the entry point for file transfer functionality.
* It:
* - Immediately turns on WiFi and launches WifiSelectionActivity on enter
* - When WifiSelectionActivity completes successfully, starts the CrossPointWebServer
* - Handles client requests in its loop() function
* - Cleans up the server and shuts down WiFi on exit
*/
class CrossPointWebServerActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
WebServerActivityState state = WebServerActivityState::WIFI_SELECTION;
const std::function<void()> onGoBack;
// WiFi selection subactivity
std::unique_ptr<WifiSelectionActivity> wifiSelection;
// Server status
std::string connectedIP;
std::string connectedSSID;
// Performance monitoring
unsigned long lastHandleClientTime = 0;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void renderServerRunning() const;
void onWifiSelectionComplete(bool connected);
void startWebServer();
void stopWebServer();
public:
explicit CrossPointWebServerActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onGoBack)
: Activity(renderer, inputManager), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@ -1,20 +1,19 @@
#include "WifiScreen.h"
#include "WifiSelectionActivity.h"
#include <GfxRenderer.h>
#include <WiFi.h>
#include <map>
#include "CrossPointWebServer.h"
#include "WifiCredentialStore.h"
#include "config.h"
void WifiScreen::taskTrampoline(void* param) {
auto* self = static_cast<WifiScreen*>(param);
void WifiSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<WifiSelectionActivity*>(param);
self->displayTaskLoop();
}
void WifiScreen::onEnter() {
void WifiSelectionActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
// Load saved WiFi credentials
@ -23,7 +22,7 @@ void WifiScreen::onEnter() {
// Reset state
selectedNetworkIndex = 0;
networks.clear();
state = WifiScreenState::SCANNING;
state = WifiSelectionState::SCANNING;
selectedSSID.clear();
connectedIP.clear();
connectionError.clear();
@ -36,7 +35,7 @@ void WifiScreen::onEnter() {
// Trigger first update to show scanning message
updateRequired = true;
xTaskCreate(&WifiScreen::taskTrampoline, "WifiScreenTask",
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
4096, // Stack size (larger for WiFi operations)
this, // Parameters
1, // Priority
@ -47,8 +46,8 @@ void WifiScreen::onEnter() {
startWifiScan();
}
void WifiScreen::onExit() {
Serial.printf("[%lu] [WIFI] ========== onExit START ==========\n", millis());
void WifiSelectionActivity::onExit() {
Serial.printf("[%lu] [WIFI] ========== WifiSelectionActivity onExit START ==========\n", millis());
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
// Stop any ongoing WiFi scan
@ -56,28 +55,8 @@ void WifiScreen::onExit() {
WiFi.scanDelete();
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap());
// CRITICAL: Stop the web server FIRST to prevent new packets from being queued
Serial.printf("[%lu] [WIFI] Stopping web server...\n", millis());
crossPointWebServer.stop();
Serial.printf("[%lu] [WIFI] Web server stopped successfully\n", millis());
Serial.printf("[%lu] [WIFI] [MEM] Free heap after webserver stop: %d bytes\n", millis());
// CRITICAL: Wait for LWIP stack to flush any pending packets
// The crash occurs because WiFi.disconnect() tears down the interface while
// packets are still queued in the LWIP stack (ethernet.c, etharp.c, wlanif.c)
Serial.printf("[%lu] [WIFI] Waiting 500ms for network stack to flush pending packets...\n", millis());
delay(500);
// Disconnect WiFi gracefully - use disconnect(false) first to send disconnect frame
Serial.printf("[%lu] [WIFI] Disconnecting WiFi (graceful)...\n", millis());
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
delay(100); // Allow disconnect frame to be sent
Serial.printf("[%lu] [WIFI] Setting WiFi mode OFF...\n", millis());
WiFi.mode(WIFI_OFF);
delay(100); // Allow WiFi hardware to fully power down
Serial.printf("[%lu] [WIFI] [MEM] Free heap after WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap());
// Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity)
// manages WiFi connection state. We just clean up the scan and task.
// Acquire mutex before deleting task to ensure task isn't using it
// This prevents hangs/crashes if the task holds the mutex when deleted
@ -99,11 +78,11 @@ void WifiScreen::onExit() {
Serial.printf("[%lu] [WIFI] Mutex deleted\n", millis());
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
Serial.printf("[%lu] [WIFI] ========== onExit COMPLETE ==========\n", millis());
Serial.printf("[%lu] [WIFI] ========== WifiSelectionActivity onExit COMPLETE ==========\n", millis());
}
void WifiScreen::startWifiScan() {
state = WifiScreenState::SCANNING;
void WifiSelectionActivity::startWifiScan() {
state = WifiSelectionState::SCANNING;
networks.clear();
updateRequired = true;
@ -116,7 +95,7 @@ void WifiScreen::startWifiScan() {
WiFi.scanNetworks(true); // true = async scan
}
void WifiScreen::processWifiScanResults() {
void WifiSelectionActivity::processWifiScanResults() {
int16_t scanResult = WiFi.scanComplete();
if (scanResult == WIFI_SCAN_RUNNING) {
@ -125,7 +104,7 @@ void WifiScreen::processWifiScanResults() {
}
if (scanResult == WIFI_SCAN_FAILED) {
state = WifiScreenState::NETWORK_LIST;
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
return;
}
@ -167,12 +146,12 @@ void WifiScreen::processWifiScanResults() {
[](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; });
WiFi.scanDelete();
state = WifiScreenState::NETWORK_LIST;
state = WifiSelectionState::NETWORK_LIST;
selectedNetworkIndex = 0;
updateRequired = true;
}
void WifiScreen::selectNetwork(int index) {
void WifiSelectionActivity::selectNetwork(int index) {
if (index < 0 || index >= static_cast<int>(networks.size())) {
return;
}
@ -197,7 +176,7 @@ void WifiScreen::selectNetwork(int index) {
if (selectedRequiresPassword) {
// Show password entry
state = WifiScreenState::PASSWORD_ENTRY;
state = WifiSelectionState::PASSWORD_ENTRY;
keyboard.reset(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password",
"", // No initial text
64, // Max password length
@ -210,8 +189,8 @@ void WifiScreen::selectNetwork(int index) {
}
}
void WifiScreen::attemptConnection() {
state = WifiScreenState::CONNECTING;
void WifiSelectionActivity::attemptConnection() {
state = WifiSelectionState::CONNECTING;
connectionStartTime = millis();
connectedIP.clear();
connectionError.clear();
@ -231,8 +210,8 @@ void WifiScreen::attemptConnection() {
}
}
void WifiScreen::checkConnectionStatus() {
if (state != WifiScreenState::CONNECTING) {
void WifiSelectionActivity::checkConnectionStatus() {
if (state != WifiSelectionState::CONNECTING) {
return;
}
@ -245,18 +224,17 @@ void WifiScreen::checkConnectionStatus() {
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
connectedIP = ipStr;
// Start the web server
crossPointWebServer.begin();
// If we used a saved password, go directly to connected screen
// If we entered a new password, ask if user wants to save it
if (usedSavedPassword || enteredPassword.empty()) {
state = WifiScreenState::CONNECTED;
} else {
state = WifiScreenState::SAVE_PROMPT;
// Otherwise, immediately complete so parent can start web server
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);
}
updateRequired = true;
return;
}
@ -265,7 +243,7 @@ void WifiScreen::checkConnectionStatus() {
if (status == WL_NO_SSID_AVAIL) {
connectionError = "Network not found";
}
state = WifiScreenState::CONNECTION_FAILED;
state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true;
return;
}
@ -274,27 +252,27 @@ void WifiScreen::checkConnectionStatus() {
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
WiFi.disconnect();
connectionError = "Connection timeout";
state = WifiScreenState::CONNECTION_FAILED;
state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true;
return;
}
}
void WifiScreen::loop() {
void WifiSelectionActivity::loop() {
// Check scan progress
if (state == WifiScreenState::SCANNING) {
if (state == WifiSelectionState::SCANNING) {
processWifiScanResults();
return;
}
// Check connection progress
if (state == WifiScreenState::CONNECTING) {
if (state == WifiSelectionState::CONNECTING) {
checkConnectionStatus();
return;
}
// Handle password entry state
if (state == WifiScreenState::PASSWORD_ENTRY && keyboard) {
if (state == WifiSelectionState::PASSWORD_ENTRY && keyboard) {
keyboard->handleInput();
if (keyboard->isComplete()) {
@ -303,7 +281,7 @@ void WifiScreen::loop() {
}
if (keyboard->isCancelled()) {
state = WifiScreenState::NETWORK_LIST;
state = WifiSelectionState::NETWORK_LIST;
keyboard.reset();
updateRequired = true;
return;
@ -314,7 +292,7 @@ void WifiScreen::loop() {
}
// Handle save prompt state
if (state == WifiScreenState::SAVE_PROMPT) {
if (state == WifiSelectionState::SAVE_PROMPT) {
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
if (savePromptSelection > 0) {
savePromptSelection--;
@ -330,19 +308,17 @@ void WifiScreen::loop() {
// User chose "Yes" - save the password
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
}
// Move to connected screen
state = WifiScreenState::CONNECTED;
updateRequired = true;
// Complete - parent will start web server
onComplete(true);
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip saving, go to connected screen
state = WifiScreenState::CONNECTED;
updateRequired = true;
// Skip saving, complete anyway
onComplete(true);
}
return;
}
// Handle forget prompt state (connection failed with saved credentials)
if (state == WifiScreenState::FORGET_PROMPT) {
if (state == WifiSelectionState::FORGET_PROMPT) {
if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
if (forgetPromptSelection > 0) {
forgetPromptSelection--;
@ -366,35 +342,33 @@ void WifiScreen::loop() {
}
}
// Go back to network list
state = WifiScreenState::NETWORK_LIST;
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip forgetting, go back to network list
state = WifiScreenState::NETWORK_LIST;
state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
}
return;
}
// Handle connected state
if (state == WifiScreenState::CONNECTED) {
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Exit screen on success
onGoBack();
return;
}
// Handle connected state (should not normally be reached - connection completes immediately)
if (state == WifiSelectionState::CONNECTED) {
// Safety fallback - immediately complete
onComplete(true);
return;
}
// Handle connection failed state
if (state == WifiScreenState::CONNECTION_FAILED) {
if (state == WifiSelectionState::CONNECTION_FAILED) {
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// If we used saved credentials, offer to forget the network
if (usedSavedPassword) {
state = WifiScreenState::FORGET_PROMPT;
state = WifiSelectionState::FORGET_PROMPT;
forgetPromptSelection = 0; // Default to "Yes"
} else {
// Go back to network list on failure
state = WifiScreenState::NETWORK_LIST;
state = WifiSelectionState::NETWORK_LIST;
}
updateRequired = true;
return;
@ -402,10 +376,10 @@ void WifiScreen::loop() {
}
// Handle network list state
if (state == WifiScreenState::NETWORK_LIST) {
// Check for Back button to exit
if (state == WifiSelectionState::NETWORK_LIST) {
// Check for Back button to exit (cancel)
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
onComplete(false);
return;
}
@ -434,7 +408,7 @@ void WifiScreen::loop() {
}
}
std::string WifiScreen::getSignalStrengthIndicator(int32_t rssi) const {
std::string WifiSelectionActivity::getSignalStrengthIndicator(int32_t rssi) const {
// Convert RSSI to signal bars representation
if (rssi >= -50) {
return "||||"; // Excellent
@ -448,7 +422,7 @@ std::string WifiScreen::getSignalStrengthIndicator(int32_t rssi) const {
return " "; // Very weak
}
void WifiScreen::displayTaskLoop() {
void WifiSelectionActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -460,32 +434,32 @@ void WifiScreen::displayTaskLoop() {
}
}
void WifiScreen::render() const {
void WifiSelectionActivity::render() const {
renderer.clearScreen();
switch (state) {
case WifiScreenState::SCANNING:
case WifiSelectionState::SCANNING:
renderConnecting(); // Reuse connecting screen with different message
break;
case WifiScreenState::NETWORK_LIST:
case WifiSelectionState::NETWORK_LIST:
renderNetworkList();
break;
case WifiScreenState::PASSWORD_ENTRY:
case WifiSelectionState::PASSWORD_ENTRY:
renderPasswordEntry();
break;
case WifiScreenState::CONNECTING:
case WifiSelectionState::CONNECTING:
renderConnecting();
break;
case WifiScreenState::CONNECTED:
case WifiSelectionState::CONNECTED:
renderConnected();
break;
case WifiScreenState::SAVE_PROMPT:
case WifiSelectionState::SAVE_PROMPT:
renderSavePrompt();
break;
case WifiScreenState::CONNECTION_FAILED:
case WifiSelectionState::CONNECTION_FAILED:
renderConnectionFailed();
break;
case WifiScreenState::FORGET_PROMPT:
case WifiSelectionState::FORGET_PROMPT:
renderForgetPrompt();
break;
}
@ -493,7 +467,7 @@ void WifiScreen::render() const {
renderer.displayBuffer();
}
void WifiScreen::renderNetworkList() const {
void WifiSelectionActivity::renderNetworkList() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
@ -569,7 +543,7 @@ void WifiScreen::renderNetworkList() const {
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "OK: Connect | * = Encrypted | + = Saved");
}
void WifiScreen::renderPasswordEntry() const {
void WifiSelectionActivity::renderPasswordEntry() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header
@ -588,12 +562,12 @@ void WifiScreen::renderPasswordEntry() const {
}
}
void WifiScreen::renderConnecting() const {
void WifiSelectionActivity::renderConnecting() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == WifiScreenState::SCANNING) {
if (state == WifiSelectionState::SCANNING) {
renderer.drawCenteredText(UI_FONT_ID, top, "Scanning...", true, REGULAR);
} else {
renderer.drawCenteredText(READER_FONT_ID, top - 30, "Connecting...", true, BOLD);
@ -606,7 +580,7 @@ void WifiScreen::renderConnecting() const {
}
}
void WifiScreen::renderConnected() const {
void WifiSelectionActivity::renderConnected() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
@ -623,14 +597,10 @@ void WifiScreen::renderConnected() const {
std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR);
// Show web server info
std::string webInfo = "Web: http://" + connectedIP + "/";
renderer.drawCenteredText(UI_FONT_ID, top + 70, webInfo.c_str(), true, REGULAR);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to exit", true, REGULAR);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", true, REGULAR);
}
void WifiScreen::renderSavePrompt() const {
void WifiSelectionActivity::renderSavePrompt() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
@ -670,7 +640,7 @@ void WifiScreen::renderSavePrompt() const {
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm", true, REGULAR);
}
void WifiScreen::renderConnectionFailed() const {
void WifiSelectionActivity::renderConnectionFailed() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 2) / 2;
@ -680,7 +650,7 @@ void WifiScreen::renderConnectionFailed() const {
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", true, REGULAR);
}
void WifiScreen::renderForgetPrompt() const {
void WifiSelectionActivity::renderForgetPrompt() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID);

View File

@ -20,26 +20,37 @@ struct WifiNetworkInfo {
bool hasSavedPassword; // Whether we have saved credentials for this network
};
// WiFi screen states
enum class WifiScreenState {
// 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, showing IP
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
};
class WifiScreen final : public Activity {
/**
* WifiSelectionActivity is responsible for scanning WiFi APs and connecting to them.
* It will:
* - Enter scanning mode on entry
* - List available WiFi networks
* - Allow selection and launch KeyboardEntryActivity for password if needed
* - Save the password if requested
* - Call onComplete callback when connected or cancelled
*
* The onComplete callback receives true if connected successfully, false if cancelled.
*/
class WifiSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
WifiScreenState state = WifiScreenState::SCANNING;
WifiSelectionState state = WifiSelectionState::SCANNING;
int selectedNetworkIndex = 0;
std::vector<WifiNetworkInfo> networks;
const std::function<void()> onGoBack;
const std::function<void(bool connected)> onComplete;
// Selected network for connection
std::string selectedSSID;
@ -85,9 +96,13 @@ class WifiScreen final : public Activity {
std::string getSignalStrengthIndicator(int32_t rssi) const;
public:
explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoBack)
: Activity(renderer, inputManager), onGoBack(onGoBack) {}
explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(bool connected)>& onComplete)
: Activity(renderer, inputManager), onComplete(onComplete) {}
void onEnter() override;
void onExit() override;
void loop() override;
// Get the IP address after successful connection
const std::string& getConnectedIP() const { return connectedIP; }
};

View File

@ -9,8 +9,7 @@
const SettingInfo SettingsActivity::settingsList[settingsCount] = {
{"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen},
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing},
{"WiFi", SettingType::ACTION, nullptr}};
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing}};
void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param);
@ -48,7 +47,7 @@ void SettingsActivity::onExit() {
void SettingsActivity::loop() {
// Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
activateCurrentSetting();
toggleCurrentSetting();
updateRequired = true;
return;
}
@ -73,26 +72,6 @@ void SettingsActivity::loop() {
}
}
void SettingsActivity::activateCurrentSetting() {
// Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return;
}
const auto& setting = settingsList[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE) {
toggleCurrentSetting();
// Trigger a redraw of the entire screen
updateRequired = true;
} else if (setting.type == SettingType::ACTION) {
// Handle action settings
if (std::string(setting.name) == "WiFi") {
onGoWifi();
}
}
}
void SettingsActivity::toggleCurrentSetting() {
// Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
@ -135,8 +114,6 @@ void SettingsActivity::render() const {
// Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD);
// We always have at least one setting
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
@ -153,13 +130,11 @@ void SettingsActivity::render() const {
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
bool value = SETTINGS.*(settingsList[i].valuePtr);
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
} else if (settingsList[i].type == SettingType::ACTION) {
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, ">");
}
}
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to select, BACK to save & exit");
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit");
// Always use standard refresh for settings screen
renderer.displayBuffer();

View File

@ -12,7 +12,7 @@
class CrossPointSettings;
enum class SettingType { TOGGLE, ACTION };
enum class SettingType { TOGGLE };
// Structure to hold setting information
struct SettingInfo {
@ -27,22 +27,19 @@ class SettingsActivity final : public Activity {
bool updateRequired = false;
int selectedSettingIndex = 0; // Currently selected setting
const std::function<void()> onGoHome;
const std::function<void()> onGoWifi;
// Static settings list
static constexpr int settingsCount = 3; // Number of settings
static constexpr int settingsCount = 2; // Number of settings
static const SettingInfo settingsList[settingsCount];
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void toggleCurrentSetting();
void activateCurrentSetting();
public:
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome,
const std::function<void()>& onGoWifi)
: Activity(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {}
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: Activity(renderer, inputManager), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@ -21,7 +21,7 @@
#include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h"
#include "activities/home/HomeActivity.h"
#include "activities/network/WifiScreen.h"
#include "activities/network/CrossPointWebServerActivity.h"
#include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
@ -143,21 +143,19 @@ void onGoToReader(const std::string& initialEpubPath) {
}
void onGoToReaderHome() { onGoToReader(std::string()); }
void onGoToSettings();
void onGoToWifi() {
void onGoToFileTransfer() {
exitActivity();
enterNewActivity(new WifiScreen(renderer, inputManager, onGoToSettings));
enterNewActivity(new CrossPointWebServerActivity(renderer, inputManager, onGoHome));
}
void onGoToSettings() {
exitActivity();
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome, onGoToWifi));
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
}
void onGoHome() {
exitActivity();
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer));
}
void setup() {
@ -204,10 +202,8 @@ void setup() {
void loop() {
static unsigned long lastLoopTime = 0;
static unsigned long maxLoopDuration = 0;
static unsigned long lastHandleClientTime = 0;
unsigned long loopStartTime = millis();
unsigned long timeSinceLastLoop = loopStartTime - lastLoopTime;
// Reduce delay when webserver is running to allow faster handleClient() calls
// This is critical for upload performance and preventing TCP timeouts
@ -251,20 +247,6 @@ void loop() {
}
unsigned long activityDuration = millis() - activityStartTime;
// Handle web server requests if running
if (crossPointWebServer.isRunning()) {
unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
// Log if there's a significant gap between handleClient calls (>100ms)
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
Serial.printf("[%lu] [LOOP] WARNING: %lu ms gap since last handleClient (activity took %lu ms)\n", millis(),
timeSinceLastHandleClient, activityDuration);
}
crossPointWebServer.handleClient();
lastHandleClientTime = millis();
}
unsigned long loopDuration = millis() - loopStartTime;
if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration;