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" #include "config.h"
namespace { namespace {
constexpr int menuItemCount = 2; constexpr int menuItemCount = 3;
} }
void HomeActivity::taskTrampoline(void* param) { void HomeActivity::taskTrampoline(void* param) {
@ -51,6 +51,8 @@ void HomeActivity::loop() {
if (selectorIndex == 0) { if (selectorIndex == 0) {
onReaderOpen(); onReaderOpen();
} else if (selectorIndex == 1) { } else if (selectorIndex == 1) {
onFileTransferOpen();
} else if (selectorIndex == 2) {
onSettingsOpen(); onSettingsOpen();
} }
} else if (prevPressed) { } else if (prevPressed) {
@ -84,7 +86,8 @@ void HomeActivity::render() const {
// Draw selection // Draw selection
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30); 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, 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.drawRect(25, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back"); 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; bool updateRequired = false;
const std::function<void()> onReaderOpen; const std::function<void()> onReaderOpen;
const std::function<void()> onSettingsOpen; const std::function<void()> onSettingsOpen;
const std::function<void()> onFileTransferOpen;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
@ -21,8 +22,8 @@ class HomeActivity final : public Activity {
public: public:
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen, explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen) const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen)
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {} : Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() 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 <GfxRenderer.h>
#include <WiFi.h> #include <WiFi.h>
#include <map> #include <map>
#include "CrossPointWebServer.h"
#include "WifiCredentialStore.h" #include "WifiCredentialStore.h"
#include "config.h" #include "config.h"
void WifiScreen::taskTrampoline(void* param) { void WifiSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<WifiScreen*>(param); auto* self = static_cast<WifiSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void WifiScreen::onEnter() { void WifiSelectionActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
// Load saved WiFi credentials // Load saved WiFi credentials
@ -23,7 +22,7 @@ void WifiScreen::onEnter() {
// Reset state // Reset state
selectedNetworkIndex = 0; selectedNetworkIndex = 0;
networks.clear(); networks.clear();
state = WifiScreenState::SCANNING; state = WifiSelectionState::SCANNING;
selectedSSID.clear(); selectedSSID.clear();
connectedIP.clear(); connectedIP.clear();
connectionError.clear(); connectionError.clear();
@ -36,7 +35,7 @@ void WifiScreen::onEnter() {
// Trigger first update to show scanning message // Trigger first update to show scanning message
updateRequired = true; updateRequired = true;
xTaskCreate(&WifiScreen::taskTrampoline, "WifiScreenTask", xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
4096, // Stack size (larger for WiFi operations) 4096, // Stack size (larger for WiFi operations)
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -47,8 +46,8 @@ void WifiScreen::onEnter() {
startWifiScan(); startWifiScan();
} }
void WifiScreen::onExit() { void WifiSelectionActivity::onExit() {
Serial.printf("[%lu] [WIFI] ========== onExit START ==========\n", millis()); 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()); Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
// Stop any ongoing WiFi scan // Stop any ongoing WiFi scan
@ -56,28 +55,8 @@ void WifiScreen::onExit() {
WiFi.scanDelete(); WiFi.scanDelete();
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap()); 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 // Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity)
Serial.printf("[%lu] [WIFI] Stopping web server...\n", millis()); // manages WiFi connection state. We just clean up the scan and task.
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());
// Acquire mutex before deleting task to ensure task isn't using it // Acquire mutex before deleting task to ensure task isn't using it
// This prevents hangs/crashes if the task holds the mutex when deleted // 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] Mutex deleted\n", millis());
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap()); 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() { void WifiSelectionActivity::startWifiScan() {
state = WifiScreenState::SCANNING; state = WifiSelectionState::SCANNING;
networks.clear(); networks.clear();
updateRequired = true; updateRequired = true;
@ -116,7 +95,7 @@ void WifiScreen::startWifiScan() {
WiFi.scanNetworks(true); // true = async scan WiFi.scanNetworks(true); // true = async scan
} }
void WifiScreen::processWifiScanResults() { void WifiSelectionActivity::processWifiScanResults() {
int16_t scanResult = WiFi.scanComplete(); int16_t scanResult = WiFi.scanComplete();
if (scanResult == WIFI_SCAN_RUNNING) { if (scanResult == WIFI_SCAN_RUNNING) {
@ -125,7 +104,7 @@ void WifiScreen::processWifiScanResults() {
} }
if (scanResult == WIFI_SCAN_FAILED) { if (scanResult == WIFI_SCAN_FAILED) {
state = WifiScreenState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true; updateRequired = true;
return; return;
} }
@ -167,12 +146,12 @@ void WifiScreen::processWifiScanResults() {
[](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; }); [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; });
WiFi.scanDelete(); WiFi.scanDelete();
state = WifiScreenState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
selectedNetworkIndex = 0; selectedNetworkIndex = 0;
updateRequired = true; updateRequired = true;
} }
void WifiScreen::selectNetwork(int index) { void WifiSelectionActivity::selectNetwork(int index) {
if (index < 0 || index >= static_cast<int>(networks.size())) { if (index < 0 || index >= static_cast<int>(networks.size())) {
return; return;
} }
@ -197,7 +176,7 @@ void WifiScreen::selectNetwork(int index) {
if (selectedRequiresPassword) { if (selectedRequiresPassword) {
// Show password entry // Show password entry
state = WifiScreenState::PASSWORD_ENTRY; state = WifiSelectionState::PASSWORD_ENTRY;
keyboard.reset(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password", keyboard.reset(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password",
"", // No initial text "", // No initial text
64, // Max password length 64, // Max password length
@ -210,8 +189,8 @@ void WifiScreen::selectNetwork(int index) {
} }
} }
void WifiScreen::attemptConnection() { void WifiSelectionActivity::attemptConnection() {
state = WifiScreenState::CONNECTING; state = WifiSelectionState::CONNECTING;
connectionStartTime = millis(); connectionStartTime = millis();
connectedIP.clear(); connectedIP.clear();
connectionError.clear(); connectionError.clear();
@ -231,8 +210,8 @@ void WifiScreen::attemptConnection() {
} }
} }
void WifiScreen::checkConnectionStatus() { void WifiSelectionActivity::checkConnectionStatus() {
if (state != WifiScreenState::CONNECTING) { if (state != WifiSelectionState::CONNECTING) {
return; return;
} }
@ -245,18 +224,17 @@ void WifiScreen::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;
// 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 we entered a new password, ask if user wants to save it
if (usedSavedPassword || enteredPassword.empty()) { // Otherwise, immediately complete so parent can start web server
state = WifiScreenState::CONNECTED; if (!usedSavedPassword && !enteredPassword.empty()) {
} else { state = WifiSelectionState::SAVE_PROMPT;
state = WifiScreenState::SAVE_PROMPT;
savePromptSelection = 0; // Default to "Yes" 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; return;
} }
@ -265,7 +243,7 @@ void WifiScreen::checkConnectionStatus() {
if (status == WL_NO_SSID_AVAIL) { if (status == WL_NO_SSID_AVAIL) {
connectionError = "Network not found"; connectionError = "Network not found";
} }
state = WifiScreenState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; updateRequired = true;
return; return;
} }
@ -274,27 +252,27 @@ void WifiScreen::checkConnectionStatus() {
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
WiFi.disconnect(); WiFi.disconnect();
connectionError = "Connection timeout"; connectionError = "Connection timeout";
state = WifiScreenState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; updateRequired = true;
return; return;
} }
} }
void WifiScreen::loop() { void WifiSelectionActivity::loop() {
// Check scan progress // Check scan progress
if (state == WifiScreenState::SCANNING) { if (state == WifiSelectionState::SCANNING) {
processWifiScanResults(); processWifiScanResults();
return; return;
} }
// Check connection progress // Check connection progress
if (state == WifiScreenState::CONNECTING) { if (state == WifiSelectionState::CONNECTING) {
checkConnectionStatus(); checkConnectionStatus();
return; return;
} }
// Handle password entry state // Handle password entry state
if (state == WifiScreenState::PASSWORD_ENTRY && keyboard) { if (state == WifiSelectionState::PASSWORD_ENTRY && keyboard) {
keyboard->handleInput(); keyboard->handleInput();
if (keyboard->isComplete()) { if (keyboard->isComplete()) {
@ -303,7 +281,7 @@ void WifiScreen::loop() {
} }
if (keyboard->isCancelled()) { if (keyboard->isCancelled()) {
state = WifiScreenState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
keyboard.reset(); keyboard.reset();
updateRequired = true; updateRequired = true;
return; return;
@ -314,7 +292,7 @@ void WifiScreen::loop() {
} }
// Handle save prompt state // 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 (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
if (savePromptSelection > 0) { if (savePromptSelection > 0) {
savePromptSelection--; savePromptSelection--;
@ -330,19 +308,17 @@ void WifiScreen::loop() {
// User chose "Yes" - save the password // User chose "Yes" - save the password
WIFI_STORE.addCredential(selectedSSID, enteredPassword); WIFI_STORE.addCredential(selectedSSID, enteredPassword);
} }
// Move to connected screen // Complete - parent will start web server
state = WifiScreenState::CONNECTED; onComplete(true);
updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) { } else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip saving, go to connected screen // Skip saving, complete anyway
state = WifiScreenState::CONNECTED; onComplete(true);
updateRequired = true;
} }
return; return;
} }
// Handle forget prompt state (connection failed with saved credentials) // 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 (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
if (forgetPromptSelection > 0) { if (forgetPromptSelection > 0) {
forgetPromptSelection--; forgetPromptSelection--;
@ -366,35 +342,33 @@ void WifiScreen::loop() {
} }
} }
// Go back to network list // Go back to network list
state = WifiScreenState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true; updateRequired = true;
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) { } else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip forgetting, go back to network list // Skip forgetting, go back to network list
state = WifiScreenState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true; updateRequired = true;
} }
return; return;
} }
// Handle connected state // Handle connected state (should not normally be reached - connection completes immediately)
if (state == WifiScreenState::CONNECTED) { if (state == WifiSelectionState::CONNECTED) {
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) { // Safety fallback - immediately complete
// Exit screen on success onComplete(true);
onGoBack(); return;
return;
}
} }
// Handle connection failed state // 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 (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// If we used saved credentials, offer to forget the network // If we used saved credentials, offer to forget the network
if (usedSavedPassword) { if (usedSavedPassword) {
state = WifiScreenState::FORGET_PROMPT; state = WifiSelectionState::FORGET_PROMPT;
forgetPromptSelection = 0; // Default to "Yes" forgetPromptSelection = 0; // Default to "Yes"
} else { } else {
// Go back to network list on failure // Go back to network list on failure
state = WifiScreenState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
} }
updateRequired = true; updateRequired = true;
return; return;
@ -402,10 +376,10 @@ void WifiScreen::loop() {
} }
// Handle network list state // Handle network list state
if (state == WifiScreenState::NETWORK_LIST) { if (state == WifiSelectionState::NETWORK_LIST) {
// Check for Back button to exit // Check for Back button to exit (cancel)
if (inputManager.wasPressed(InputManager::BTN_BACK)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack(); onComplete(false);
return; 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 // Convert RSSI to signal bars representation
if (rssi >= -50) { if (rssi >= -50) {
return "||||"; // Excellent return "||||"; // Excellent
@ -448,7 +422,7 @@ std::string WifiScreen::getSignalStrengthIndicator(int32_t rssi) const {
return " "; // Very weak return " "; // Very weak
} }
void WifiScreen::displayTaskLoop() { void WifiSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -460,32 +434,32 @@ void WifiScreen::displayTaskLoop() {
} }
} }
void WifiScreen::render() const { void WifiSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
switch (state) { switch (state) {
case WifiScreenState::SCANNING: case WifiSelectionState::SCANNING:
renderConnecting(); // Reuse connecting screen with different message renderConnecting(); // Reuse connecting screen with different message
break; break;
case WifiScreenState::NETWORK_LIST: case WifiSelectionState::NETWORK_LIST:
renderNetworkList(); renderNetworkList();
break; break;
case WifiScreenState::PASSWORD_ENTRY: case WifiSelectionState::PASSWORD_ENTRY:
renderPasswordEntry(); renderPasswordEntry();
break; break;
case WifiScreenState::CONNECTING: case WifiSelectionState::CONNECTING:
renderConnecting(); renderConnecting();
break; break;
case WifiScreenState::CONNECTED: case WifiSelectionState::CONNECTED:
renderConnected(); renderConnected();
break; break;
case WifiScreenState::SAVE_PROMPT: case WifiSelectionState::SAVE_PROMPT:
renderSavePrompt(); renderSavePrompt();
break; break;
case WifiScreenState::CONNECTION_FAILED: case WifiSelectionState::CONNECTION_FAILED:
renderConnectionFailed(); renderConnectionFailed();
break; break;
case WifiScreenState::FORGET_PROMPT: case WifiSelectionState::FORGET_PROMPT:
renderForgetPrompt(); renderForgetPrompt();
break; break;
} }
@ -493,7 +467,7 @@ void WifiScreen::render() const {
renderer.displayBuffer(); renderer.displayBuffer();
} }
void WifiScreen::renderNetworkList() const { void WifiSelectionActivity::renderNetworkList() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); 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"); 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(); const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header // 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 pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
if (state == WifiScreenState::SCANNING) { if (state == WifiSelectionState::SCANNING) {
renderer.drawCenteredText(UI_FONT_ID, top, "Scanning...", true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, "Scanning...", true, REGULAR);
} else { } else {
renderer.drawCenteredText(READER_FONT_ID, top - 30, "Connecting...", true, BOLD); 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 pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
@ -623,14 +597,10 @@ void WifiScreen::renderConnected() const {
std::string ipInfo = "IP Address: " + connectedIP; std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR);
// Show web server info renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", true, REGULAR);
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);
} }
void WifiScreen::renderSavePrompt() const { void WifiSelectionActivity::renderSavePrompt() const {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); 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); 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 pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 2) / 2; 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); 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 pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); 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 bool hasSavedPassword; // Whether we have saved credentials for this network
}; };
// WiFi screen states // WiFi selection states
enum class WifiScreenState { 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, showing IP 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 CONNECTION_FAILED, // Connection failed
FORGET_PROMPT // Asking user if they want to forget the network 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; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
WifiScreenState state = WifiScreenState::SCANNING; WifiSelectionState state = WifiSelectionState::SCANNING;
int selectedNetworkIndex = 0; int selectedNetworkIndex = 0;
std::vector<WifiNetworkInfo> networks; std::vector<WifiNetworkInfo> networks;
const std::function<void()> onGoBack; const std::function<void(bool connected)> onComplete;
// Selected network for connection // Selected network for connection
std::string selectedSSID; std::string selectedSSID;
@ -85,9 +96,13 @@ class WifiScreen final : public Activity {
std::string getSignalStrengthIndicator(int32_t rssi) const; std::string getSignalStrengthIndicator(int32_t rssi) const;
public: public:
explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoBack) explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
: Activity(renderer, inputManager), onGoBack(onGoBack) {} const std::function<void(bool connected)>& onComplete)
: Activity(renderer, inputManager), onComplete(onComplete) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() 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] = { const SettingInfo SettingsActivity::settingsList[settingsCount] = {
{"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen}, {"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen},
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing}, {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing}};
{"WiFi", SettingType::ACTION, nullptr}};
void SettingsActivity::taskTrampoline(void* param) { void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param); auto* self = static_cast<SettingsActivity*>(param);
@ -48,7 +47,7 @@ void SettingsActivity::onExit() {
void SettingsActivity::loop() { void SettingsActivity::loop() {
// Handle actions with early return // Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
activateCurrentSetting(); toggleCurrentSetting();
updateRequired = true; updateRequired = true;
return; 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() { void SettingsActivity::toggleCurrentSetting() {
// Validate index // Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
@ -135,8 +114,6 @@ void SettingsActivity::render() const {
// Draw header // Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD);
// We always have at least one setting
// Draw all settings // Draw all settings
for (int i = 0; i < settingsCount; i++) { for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings 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) { if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
bool value = SETTINGS.*(settingsList[i].valuePtr); bool value = SETTINGS.*(settingsList[i].valuePtr);
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); 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 // 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 // Always use standard refresh for settings screen
renderer.displayBuffer(); renderer.displayBuffer();

View File

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

View File

@ -21,7 +21,7 @@
#include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h" #include "activities/boot_sleep/SleepActivity.h"
#include "activities/home/HomeActivity.h" #include "activities/home/HomeActivity.h"
#include "activities/network/WifiScreen.h" #include "activities/network/CrossPointWebServerActivity.h"
#include "activities/reader/ReaderActivity.h" #include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h" #include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h" #include "activities/util/FullScreenMessageActivity.h"
@ -143,21 +143,19 @@ void onGoToReader(const std::string& initialEpubPath) {
} }
void onGoToReaderHome() { onGoToReader(std::string()); } void onGoToReaderHome() { onGoToReader(std::string()); }
void onGoToSettings(); void onGoToFileTransfer() {
void onGoToWifi() {
exitActivity(); exitActivity();
enterNewActivity(new WifiScreen(renderer, inputManager, onGoToSettings)); enterNewActivity(new CrossPointWebServerActivity(renderer, inputManager, onGoHome));
} }
void onGoToSettings() { void onGoToSettings() {
exitActivity(); exitActivity();
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome, onGoToWifi)); enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
} }
void onGoHome() { void onGoHome() {
exitActivity(); exitActivity();
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings)); enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer));
} }
void setup() { void setup() {
@ -204,10 +202,8 @@ void setup() {
void loop() { void loop() {
static unsigned long lastLoopTime = 0; static unsigned long lastLoopTime = 0;
static unsigned long maxLoopDuration = 0; static unsigned long maxLoopDuration = 0;
static unsigned long lastHandleClientTime = 0;
unsigned long loopStartTime = millis(); unsigned long loopStartTime = millis();
unsigned long timeSinceLastLoop = loopStartTime - lastLoopTime;
// Reduce delay when webserver is running to allow faster handleClient() calls // Reduce delay when webserver is running to allow faster handleClient() calls
// This is critical for upload performance and preventing TCP timeouts // This is critical for upload performance and preventing TCP timeouts
@ -251,20 +247,6 @@ void loop() {
} }
unsigned long activityDuration = millis() - activityStartTime; 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; unsigned long loopDuration = millis() - loopStartTime;
if (loopDuration > maxLoopDuration) { if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration; maxLoopDuration = loopDuration;