diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 19b30c0..82d57cc 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -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"); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 7f6ac4d..6439204 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -14,6 +14,7 @@ class HomeActivity final : public Activity { bool updateRequired = false; const std::function onReaderOpen; const std::function onSettingsOpen; + const std::function 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& onReaderOpen, - const std::function& onSettingsOpen) - : Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {} + const std::function& onSettingsOpen, const std::function& onFileTransferOpen) + : Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp new file mode 100644 index 0000000..fe572c9 --- /dev/null +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -0,0 +1,234 @@ +#include "CrossPointWebServerActivity.h" + +#include +#include + +#include "CrossPointWebServer.h" +#include "config.h" + +void CrossPointWebServerActivity::taskTrampoline(void* param) { + auto* self = static_cast(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); +} diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h new file mode 100644 index 0000000..f9e4509 --- /dev/null +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#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 onGoBack; + + // WiFi selection subactivity + std::unique_ptr 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& onGoBack) + : Activity(renderer, inputManager), onGoBack(onGoBack) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/activities/network/WifiScreen.cpp b/src/activities/network/WifiSelectionActivity.cpp similarity index 80% rename from src/activities/network/WifiScreen.cpp rename to src/activities/network/WifiSelectionActivity.cpp index c631028..a48891e 100644 --- a/src/activities/network/WifiScreen.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -1,20 +1,19 @@ -#include "WifiScreen.h" +#include "WifiSelectionActivity.h" #include #include #include -#include "CrossPointWebServer.h" #include "WifiCredentialStore.h" #include "config.h" -void WifiScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +void WifiSelectionActivity::taskTrampoline(void* param) { + auto* self = static_cast(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(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); diff --git a/src/activities/network/WifiScreen.h b/src/activities/network/WifiSelectionActivity.h similarity index 69% rename from src/activities/network/WifiScreen.h rename to src/activities/network/WifiSelectionActivity.h index 173b51f..a6b3c25 100644 --- a/src/activities/network/WifiScreen.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -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 networks; - const std::function onGoBack; + const std::function 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& onGoBack) - : Activity(renderer, inputManager), onGoBack(onGoBack) {} + explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager, + const std::function& 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; } }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index a510e8f..f341af7 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -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(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(); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index b68ace8..e831b03 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -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 onGoHome; - const std::function 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& onGoHome, - const std::function& onGoWifi) - : Activity(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {} + explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoHome) + : Activity(renderer, inputManager), onGoHome(onGoHome) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/main.cpp b/src/main.cpp index d729d6c..e13b92c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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;