mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-08 08:37:38 +03:00
Compare commits
No commits in common. "2dfecbc8c4ae0b6484cbb1c2c021cb72afead9b0" and "71fc35845ba83fd88a2510500e498f261fd36251" have entirely different histories.
2dfecbc8c4
...
71fc35845b
@ -10,15 +10,16 @@
|
|||||||
#include "html/FilesPageHeaderHtml.generated.h"
|
#include "html/FilesPageHeaderHtml.generated.h"
|
||||||
#include "html/HomePageHtml.generated.h"
|
#include "html/HomePageHtml.generated.h"
|
||||||
|
|
||||||
namespace {
|
// Global instance
|
||||||
|
CrossPointWebServer crossPointWebServer;
|
||||||
|
|
||||||
// Folders/files to hide from the web interface file browser
|
// Folders/files to hide from the web interface file browser
|
||||||
// Note: Items starting with "." are automatically hidden
|
// Note: Items starting with "." are automatically hidden
|
||||||
const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
|
static const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
|
||||||
const size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
|
static const size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
|
||||||
|
|
||||||
// Helper function to escape HTML special characters to prevent XSS
|
// Helper function to escape HTML special characters to prevent XSS
|
||||||
String escapeHtml(const String& input) {
|
static String escapeHtml(const String& input) {
|
||||||
String output;
|
String output;
|
||||||
output.reserve(input.length() * 1.1); // Pre-allocate with some extra space
|
output.reserve(input.length() * 1.1); // Pre-allocate with some extra space
|
||||||
|
|
||||||
@ -48,8 +49,6 @@ String escapeHtml(const String& input) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// File listing page template - now using generated headers:
|
// File listing page template - now using generated headers:
|
||||||
// - HomePageHtml (from html/HomePage.html)
|
// - HomePageHtml (from html/HomePage.html)
|
||||||
// - FilesPageHeaderHtml (from html/FilesPageHeader.html)
|
// - FilesPageHeaderHtml (from html/FilesPageHeader.html)
|
||||||
@ -108,30 +107,19 @@ void CrossPointWebServer::begin() {
|
|||||||
|
|
||||||
void CrossPointWebServer::stop() {
|
void CrossPointWebServer::stop() {
|
||||||
if (!running || !server) {
|
if (!running || !server) {
|
||||||
Serial.printf("[%lu] [WEB] stop() called but already stopped (running=%d, server=%p)\n", millis(), running, server);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] STOP INITIATED - setting running=false first\n", millis());
|
|
||||||
running = false; // Set this FIRST to prevent handleClient from using server
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap before stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap before stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Add delay to allow any in-flight handleClient() calls to complete
|
|
||||||
delay(100);
|
|
||||||
Serial.printf("[%lu] [WEB] Waited 100ms for handleClient to finish\n", millis());
|
|
||||||
|
|
||||||
server->stop();
|
server->stop();
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after server->stop(): %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after server->stop(): %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Add another delay before deletion to ensure server->stop() completes
|
|
||||||
delay(50);
|
|
||||||
Serial.printf("[%lu] [WEB] Waited 50ms before deleting server\n", millis());
|
|
||||||
|
|
||||||
delete server;
|
delete server;
|
||||||
server = nullptr;
|
server = nullptr;
|
||||||
|
running = false;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Web server stopped and deleted\n", millis());
|
Serial.printf("[%lu] [WEB] Web server stopped\n", millis());
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
||||||
@ -141,25 +129,14 @@ void CrossPointWebServer::stop() {
|
|||||||
|
|
||||||
void CrossPointWebServer::handleClient() {
|
void CrossPointWebServer::handleClient() {
|
||||||
static unsigned long lastDebugPrint = 0;
|
static unsigned long lastDebugPrint = 0;
|
||||||
|
if (running && server) {
|
||||||
// Check running flag FIRST before accessing server
|
// Print debug every 10 seconds to confirm handleClient is being called
|
||||||
if (!running) {
|
if (millis() - lastDebugPrint > 10000) {
|
||||||
return;
|
Serial.printf("[%lu] [WEB] handleClient active, server running on port %d\n", millis(), port);
|
||||||
|
lastDebugPrint = millis();
|
||||||
|
}
|
||||||
|
server->handleClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check server pointer is valid
|
|
||||||
if (!server) {
|
|
||||||
Serial.printf("[%lu] [WEB] WARNING: handleClient called with null server!\n", millis());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print debug every 10 seconds to confirm handleClient is being called
|
|
||||||
if (millis() - lastDebugPrint > 10000) {
|
|
||||||
Serial.printf("[%lu] [WEB] handleClient active, server running on port %d\n", millis(), port);
|
|
||||||
lastDebugPrint = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
server->handleClient();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleRoot() {
|
void CrossPointWebServer::handleRoot() {
|
||||||
@ -476,12 +453,6 @@ void CrossPointWebServer::handleUpload() {
|
|||||||
static unsigned long uploadStartTime = 0;
|
static unsigned long uploadStartTime = 0;
|
||||||
static size_t lastLoggedSize = 0;
|
static size_t lastLoggedSize = 0;
|
||||||
|
|
||||||
// Safety check: ensure server is still valid
|
|
||||||
if (!running || !server) {
|
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] ERROR: handleUpload called but server not running!\n", millis());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTPUpload& upload = server->upload();
|
HTTPUpload& upload = server->upload();
|
||||||
|
|
||||||
if (upload.status == UPLOAD_FILE_START) {
|
if (upload.status == UPLOAD_FILE_START) {
|
||||||
@ -54,3 +54,6 @@ class CrossPointWebServer {
|
|||||||
void handleCreateFolder();
|
void handleCreateFolder();
|
||||||
void handleDelete();
|
void handleDelete();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Global instance
|
||||||
|
extern CrossPointWebServer crossPointWebServer;
|
||||||
@ -9,7 +9,6 @@
|
|||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
WifiCredentialStore WifiCredentialStore::instance;
|
WifiCredentialStore WifiCredentialStore::instance;
|
||||||
|
|
||||||
namespace {
|
|
||||||
// File format version
|
// File format version
|
||||||
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ constexpr char WIFI_FILE[] = "/sd/.crosspoint/wifi.bin";
|
|||||||
// This is NOT cryptographic security, just prevents casual file reading
|
// This is NOT cryptographic security, just prevents casual file reading
|
||||||
constexpr uint8_t OBFUSCATION_KEY[] = {0x43, 0x72, 0x6F, 0x73, 0x73, 0x50, 0x6F, 0x69, 0x6E, 0x74};
|
constexpr uint8_t OBFUSCATION_KEY[] = {0x43, 0x72, 0x6F, 0x73, 0x73, 0x50, 0x6F, 0x69, 0x6E, 0x74};
|
||||||
constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
|
constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void WifiCredentialStore::obfuscate(std::string& data) const {
|
void WifiCredentialStore::obfuscate(std::string& data) const {
|
||||||
Serial.printf("[%lu] [WCS] Obfuscating/deobfuscating %zu bytes\n", millis(), data.size());
|
Serial.printf("[%lu] [WCS] Obfuscating/deobfuscating %zu bytes\n", millis(), data.size());
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int menuItemCount = 3;
|
constexpr int menuItemCount = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::taskTrampoline(void* param) {
|
void HomeActivity::taskTrampoline(void* param) {
|
||||||
@ -51,8 +51,6 @@ 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) {
|
||||||
@ -86,8 +84,7 @@ 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, "File transfer", selectorIndex != 1);
|
renderer.drawText(UI_FONT_ID, 20, 90, "Settings", 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");
|
||||||
|
|||||||
@ -14,7 +14,6 @@ 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();
|
||||||
@ -22,11 +21,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()>& onFileTransferOpen)
|
const std::function<void()>& onSettingsOpen)
|
||||||
: Activity(renderer, inputManager),
|
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {}
|
||||||
onReaderOpen(onReaderOpen),
|
|
||||||
onSettingsOpen(onSettingsOpen),
|
|
||||||
onFileTransferOpen(onFileTransferOpen) {}
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -1,243 +0,0 @@
|
|||||||
#include "CrossPointWebServerActivity.h"
|
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
|
||||||
#include <WiFi.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());
|
|
||||||
|
|
||||||
// Create the web server instance
|
|
||||||
webServer.reset(new CrossPointWebServer());
|
|
||||||
webServer->begin();
|
|
||||||
|
|
||||||
if (webServer->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());
|
|
||||||
webServer.reset();
|
|
||||||
// Go back on error
|
|
||||||
onGoBack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CrossPointWebServerActivity::stopWebServer() {
|
|
||||||
if (webServer && webServer->isRunning()) {
|
|
||||||
Serial.printf("[%lu] [WEBACT] Stopping web server...\n", millis());
|
|
||||||
webServer->stop();
|
|
||||||
Serial.printf("[%lu] [WEBACT] Web server stopped\n", millis());
|
|
||||||
}
|
|
||||||
webServer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 - call handleClient multiple times per loop
|
|
||||||
// to improve responsiveness and upload throughput
|
|
||||||
if (webServer && webServer->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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call handleClient multiple times to process pending requests faster
|
|
||||||
// This is critical for upload performance - HTTP file uploads send data
|
|
||||||
// in chunks and each handleClient() call processes incoming data
|
|
||||||
constexpr int HANDLE_CLIENT_ITERATIONS = 10;
|
|
||||||
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
|
|
||||||
webServer->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);
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
#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"
|
|
||||||
#include "server/CrossPointWebServer.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;
|
|
||||||
|
|
||||||
// Web server - owned by this activity
|
|
||||||
std::unique_ptr<CrossPointWebServer> webServer;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Check if web server is running (used by main loop for timing optimization)
|
|
||||||
bool isWebServerRunning() const { return webServer && webServer->isRunning(); }
|
|
||||||
};
|
|
||||||
@ -1,30 +1,34 @@
|
|||||||
#include "KeyboardEntryActivity.h"
|
#include "OnScreenKeyboard.h"
|
||||||
|
|
||||||
#include "../../config.h"
|
#include "config.h"
|
||||||
|
|
||||||
// Keyboard layouts - lowercase
|
// Keyboard layouts - lowercase
|
||||||
const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = {
|
const char* const OnScreenKeyboard::keyboard[NUM_ROWS] = {
|
||||||
"`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./",
|
"`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./",
|
||||||
"^ _____<OK" // ^ = shift, _ = space, < = backspace, OK = done
|
"^ _____<OK" // ^ = shift, _ = space, < = backspace, OK = done
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keyboard layouts - uppercase/symbols
|
// Keyboard layouts - uppercase/symbols
|
||||||
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
|
const char* const OnScreenKeyboard::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
|
||||||
"ZXCVBNM<>?", "^ _____<OK"};
|
"ZXCVBNM<>?", "^ _____<OK"};
|
||||||
|
|
||||||
KeyboardEntryActivity::KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager,
|
OnScreenKeyboard::OnScreenKeyboard(GfxRenderer& renderer, InputManager& inputManager, const std::string& title,
|
||||||
const std::string& title, const std::string& initialText, size_t maxLength,
|
const std::string& initialText, size_t maxLength, bool isPassword)
|
||||||
bool isPassword)
|
: renderer(renderer),
|
||||||
: Activity(renderer, inputManager), title(title), text(initialText), maxLength(maxLength), isPassword(isPassword) {}
|
inputManager(inputManager),
|
||||||
|
title(title),
|
||||||
|
text(initialText),
|
||||||
|
maxLength(maxLength),
|
||||||
|
isPassword(isPassword) {}
|
||||||
|
|
||||||
void KeyboardEntryActivity::setText(const std::string& newText) {
|
void OnScreenKeyboard::setText(const std::string& newText) {
|
||||||
text = newText;
|
text = newText;
|
||||||
if (maxLength > 0 && text.length() > maxLength) {
|
if (maxLength > 0 && text.length() > maxLength) {
|
||||||
text = text.substr(0, maxLength);
|
text = text.substr(0, maxLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string& newInitialText) {
|
void OnScreenKeyboard::reset(const std::string& newTitle, const std::string& newInitialText) {
|
||||||
if (!newTitle.empty()) {
|
if (!newTitle.empty()) {
|
||||||
title = newTitle;
|
title = newTitle;
|
||||||
}
|
}
|
||||||
@ -36,22 +40,7 @@ void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string
|
|||||||
cancelled = false;
|
cancelled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::onEnter() {
|
int OnScreenKeyboard::getRowLength(int row) const {
|
||||||
// Reset state when entering the activity
|
|
||||||
complete = false;
|
|
||||||
cancelled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KeyboardEntryActivity::onExit() {
|
|
||||||
// Clean up if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
void KeyboardEntryActivity::loop() {
|
|
||||||
handleInput();
|
|
||||||
render(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
int KeyboardEntryActivity::getRowLength(int row) const {
|
|
||||||
if (row < 0 || row >= NUM_ROWS) return 0;
|
if (row < 0 || row >= NUM_ROWS) return 0;
|
||||||
|
|
||||||
// Return actual length of each row based on keyboard layout
|
// Return actual length of each row based on keyboard layout
|
||||||
@ -71,7 +60,7 @@ int KeyboardEntryActivity::getRowLength(int row) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
char KeyboardEntryActivity::getSelectedChar() const {
|
char OnScreenKeyboard::getSelectedChar() const {
|
||||||
const char* const* layout = shiftActive ? keyboardShift : keyboard;
|
const char* const* layout = shiftActive ? keyboardShift : keyboard;
|
||||||
|
|
||||||
if (selectedRow < 0 || selectedRow >= NUM_ROWS) return '\0';
|
if (selectedRow < 0 || selectedRow >= NUM_ROWS) return '\0';
|
||||||
@ -80,7 +69,7 @@ char KeyboardEntryActivity::getSelectedChar() const {
|
|||||||
return layout[selectedRow][selectedCol];
|
return layout[selectedRow][selectedCol];
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::handleKeyPress() {
|
void OnScreenKeyboard::handleKeyPress() {
|
||||||
// Handle special row (bottom row with shift, space, backspace, done)
|
// Handle special row (bottom row with shift, space, backspace, done)
|
||||||
if (selectedRow == SHIFT_ROW) {
|
if (selectedRow == SHIFT_ROW) {
|
||||||
if (selectedCol == SHIFT_COL) {
|
if (selectedCol == SHIFT_COL) {
|
||||||
@ -128,7 +117,7 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KeyboardEntryActivity::handleInput() {
|
bool OnScreenKeyboard::handleInput() {
|
||||||
if (complete || cancelled) {
|
if (complete || cancelled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -190,7 +179,7 @@ bool KeyboardEntryActivity::handleInput() {
|
|||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::render(int startY) const {
|
void OnScreenKeyboard::render(int startY) const {
|
||||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
|
|
||||||
// Draw title
|
// Draw title
|
||||||
@ -5,20 +5,18 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "../Activity.h"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable keyboard entry activity for text input.
|
* Reusable on-screen keyboard component for text input.
|
||||||
* Can be started from any activity that needs text entry.
|
* Can be embedded in any screen that needs text entry.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* 1. Create a KeyboardEntryActivity instance
|
* 1. Create an OnScreenKeyboard instance
|
||||||
* 2. Set callbacks with setOnComplete() and setOnCancel()
|
* 2. Call render() to draw the keyboard
|
||||||
* 3. Call onEnter() to start the activity
|
* 3. Call handleInput() to process button presses
|
||||||
* 4. Call loop() in your main loop
|
* 4. When isComplete() returns true, get the result from getText()
|
||||||
* 5. When complete or cancelled, callbacks will be invoked
|
* 5. Call isCancelled() to check if user cancelled input
|
||||||
*/
|
*/
|
||||||
class KeyboardEntryActivity : public Activity {
|
class OnScreenKeyboard {
|
||||||
public:
|
public:
|
||||||
// Callback types
|
// Callback types
|
||||||
using OnCompleteCallback = std::function<void(const std::string&)>;
|
using OnCompleteCallback = std::function<void(const std::string&)>;
|
||||||
@ -33,20 +31,20 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
* @param maxLength Maximum length of input text (0 for unlimited)
|
* @param maxLength Maximum length of input text (0 for unlimited)
|
||||||
* @param isPassword If true, display asterisks instead of actual characters
|
* @param isPassword If true, display asterisks instead of actual characters
|
||||||
*/
|
*/
|
||||||
KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text",
|
OnScreenKeyboard(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text",
|
||||||
const std::string& initialText = "", size_t maxLength = 0, bool isPassword = false);
|
const std::string& initialText = "", size_t maxLength = 0, bool isPassword = false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle button input. Call this in your main loop.
|
* Handle button input. Call this in your screen's handleInput().
|
||||||
* @return true if input was handled, false otherwise
|
* @return true if input was handled, false otherwise
|
||||||
*/
|
*/
|
||||||
bool handleInput();
|
bool handleInput();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the keyboard at the specified Y position.
|
* Render the keyboard at the specified Y position.
|
||||||
* @param startY Y-coordinate where keyboard rendering starts (default 10)
|
* @param startY Y-coordinate where keyboard rendering starts
|
||||||
*/
|
*/
|
||||||
void render(int startY = 10) const;
|
void render(int startY) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current text entered by the user.
|
* Get the current text entered by the user.
|
||||||
@ -83,12 +81,10 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
*/
|
*/
|
||||||
void setOnCancel(OnCancelCallback callback) { onCancel = callback; }
|
void setOnCancel(OnCancelCallback callback) { onCancel = callback; }
|
||||||
|
|
||||||
// Activity overrides
|
|
||||||
void onEnter() override;
|
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
GfxRenderer& renderer;
|
||||||
|
InputManager& inputManager;
|
||||||
|
|
||||||
std::string title;
|
std::string title;
|
||||||
std::string text;
|
std::string text;
|
||||||
size_t maxLength;
|
size_t maxLength;
|
||||||
@ -1,19 +1,20 @@
|
|||||||
#include "WifiSelectionActivity.h"
|
#include "WifiScreen.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 WifiSelectionActivity::taskTrampoline(void* param) {
|
void WifiScreen::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<WifiSelectionActivity*>(param);
|
auto* self = static_cast<WifiScreen*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::onEnter() {
|
void WifiScreen::onEnter() {
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Load saved WiFi credentials
|
// Load saved WiFi credentials
|
||||||
@ -22,7 +23,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
// Reset state
|
// Reset state
|
||||||
selectedNetworkIndex = 0;
|
selectedNetworkIndex = 0;
|
||||||
networks.clear();
|
networks.clear();
|
||||||
state = WifiSelectionState::SCANNING;
|
state = WifiScreenState::SCANNING;
|
||||||
selectedSSID.clear();
|
selectedSSID.clear();
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
connectionError.clear();
|
connectionError.clear();
|
||||||
@ -35,7 +36,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
// Trigger first update to show scanning message
|
// Trigger first update to show scanning message
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
|
xTaskCreate(&WifiScreen::taskTrampoline, "WifiScreenTask",
|
||||||
4096, // Stack size (larger for WiFi operations)
|
4096, // Stack size (larger for WiFi operations)
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
@ -46,43 +47,42 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
startWifiScan();
|
startWifiScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::onExit() {
|
void WifiScreen::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());
|
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
|
||||||
Serial.printf("[%lu] [WIFI] Deleting WiFi scan...\n", millis());
|
|
||||||
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());
|
||||||
|
|
||||||
// Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity)
|
// Stop the web server to free memory
|
||||||
// manages WiFi connection state. We just clean up the scan and task.
|
crossPointWebServer.stop();
|
||||||
|
Serial.printf("[%lu] [WIFI] [MEM] Free heap after webserver stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Acquire mutex before deleting task to ensure task isn't using it
|
// Disconnect WiFi to free memory
|
||||||
// This prevents hangs/crashes if the task holds the mutex when deleted
|
WiFi.disconnect(true);
|
||||||
Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis());
|
WiFi.mode(WIFI_OFF);
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
Serial.printf("[%lu] [WIFI] [MEM] Free heap after WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Delete the display task (we now hold the mutex, so task is blocked if it needs it)
|
// Delete the display task
|
||||||
Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis());
|
|
||||||
if (displayTaskHandle) {
|
if (displayTaskHandle) {
|
||||||
vTaskDelete(displayTaskHandle);
|
vTaskDelete(displayTaskHandle);
|
||||||
displayTaskHandle = nullptr;
|
displayTaskHandle = nullptr;
|
||||||
Serial.printf("[%lu] [WIFI] Display task deleted\n", millis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now safe to delete the mutex since we own it
|
// Small delay to ensure task is fully deleted before cleaning up mutex
|
||||||
Serial.printf("[%lu] [WIFI] Deleting mutex...\n", millis());
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
// Now safe to delete the mutex
|
||||||
Serial.printf("[%lu] [WIFI] Mutex deleted\n", millis());
|
if (renderingMutex) {
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
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] ========== WifiSelectionActivity onExit COMPLETE ==========\n", millis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::startWifiScan() {
|
void WifiScreen::startWifiScan() {
|
||||||
state = WifiSelectionState::SCANNING;
|
state = WifiScreenState::SCANNING;
|
||||||
networks.clear();
|
networks.clear();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ void WifiSelectionActivity::startWifiScan() {
|
|||||||
WiFi.scanNetworks(true); // true = async scan
|
WiFi.scanNetworks(true); // true = async scan
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::processWifiScanResults() {
|
void WifiScreen::processWifiScanResults() {
|
||||||
int16_t scanResult = WiFi.scanComplete();
|
int16_t scanResult = WiFi.scanComplete();
|
||||||
|
|
||||||
if (scanResult == WIFI_SCAN_RUNNING) {
|
if (scanResult == WIFI_SCAN_RUNNING) {
|
||||||
@ -104,7 +104,7 @@ void WifiSelectionActivity::processWifiScanResults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scanResult == WIFI_SCAN_FAILED) {
|
if (scanResult == WIFI_SCAN_FAILED) {
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiScreenState::NETWORK_LIST;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -146,12 +146,12 @@ void WifiSelectionActivity::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 = WifiSelectionState::NETWORK_LIST;
|
state = WifiScreenState::NETWORK_LIST;
|
||||||
selectedNetworkIndex = 0;
|
selectedNetworkIndex = 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::selectNetwork(int index) {
|
void WifiScreen::selectNetwork(int index) {
|
||||||
if (index < 0 || index >= static_cast<int>(networks.size())) {
|
if (index < 0 || index >= static_cast<int>(networks.size())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -176,12 +176,12 @@ void WifiSelectionActivity::selectNetwork(int index) {
|
|||||||
|
|
||||||
if (selectedRequiresPassword) {
|
if (selectedRequiresPassword) {
|
||||||
// Show password entry
|
// Show password entry
|
||||||
state = WifiSelectionState::PASSWORD_ENTRY;
|
state = WifiScreenState::PASSWORD_ENTRY;
|
||||||
keyboard.reset(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password",
|
keyboard.reset(new OnScreenKeyboard(renderer, inputManager, "Enter WiFi Password",
|
||||||
"", // No initial text
|
"", // No initial text
|
||||||
64, // Max password length
|
64, // Max password length
|
||||||
false // Show password by default (hard keyboard to use)
|
false // Show password by default (hard keyboard to use)
|
||||||
));
|
));
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
// Connect directly for open networks
|
// Connect directly for open networks
|
||||||
@ -189,8 +189,8 @@ void WifiSelectionActivity::selectNetwork(int index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::attemptConnection() {
|
void WifiScreen::attemptConnection() {
|
||||||
state = WifiSelectionState::CONNECTING;
|
state = WifiScreenState::CONNECTING;
|
||||||
connectionStartTime = millis();
|
connectionStartTime = millis();
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
connectionError.clear();
|
connectionError.clear();
|
||||||
@ -210,8 +210,8 @@ void WifiSelectionActivity::attemptConnection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::checkConnectionStatus() {
|
void WifiScreen::checkConnectionStatus() {
|
||||||
if (state != WifiSelectionState::CONNECTING) {
|
if (state != WifiScreenState::CONNECTING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,17 +224,18 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||||
connectedIP = ipStr;
|
connectedIP = ipStr;
|
||||||
|
|
||||||
|
// 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
|
||||||
// Otherwise, immediately complete so parent can start web server
|
if (usedSavedPassword || enteredPassword.empty()) {
|
||||||
if (!usedSavedPassword && !enteredPassword.empty()) {
|
state = WifiScreenState::CONNECTED;
|
||||||
state = WifiSelectionState::SAVE_PROMPT;
|
|
||||||
savePromptSelection = 0; // Default to "Yes"
|
|
||||||
updateRequired = true;
|
|
||||||
} else {
|
} else {
|
||||||
// Using saved password or open network - complete immediately
|
state = WifiScreenState::SAVE_PROMPT;
|
||||||
Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis());
|
savePromptSelection = 0; // Default to "Yes"
|
||||||
onComplete(true);
|
|
||||||
}
|
}
|
||||||
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +244,7 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
if (status == WL_NO_SSID_AVAIL) {
|
if (status == WL_NO_SSID_AVAIL) {
|
||||||
connectionError = "Network not found";
|
connectionError = "Network not found";
|
||||||
}
|
}
|
||||||
state = WifiSelectionState::CONNECTION_FAILED;
|
state = WifiScreenState::CONNECTION_FAILED;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -252,27 +253,27 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
|
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
|
||||||
WiFi.disconnect();
|
WiFi.disconnect();
|
||||||
connectionError = "Connection timeout";
|
connectionError = "Connection timeout";
|
||||||
state = WifiSelectionState::CONNECTION_FAILED;
|
state = WifiScreenState::CONNECTION_FAILED;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::loop() {
|
void WifiScreen::loop() {
|
||||||
// Check scan progress
|
// Check scan progress
|
||||||
if (state == WifiSelectionState::SCANNING) {
|
if (state == WifiScreenState::SCANNING) {
|
||||||
processWifiScanResults();
|
processWifiScanResults();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connection progress
|
// Check connection progress
|
||||||
if (state == WifiSelectionState::CONNECTING) {
|
if (state == WifiScreenState::CONNECTING) {
|
||||||
checkConnectionStatus();
|
checkConnectionStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle password entry state
|
// Handle password entry state
|
||||||
if (state == WifiSelectionState::PASSWORD_ENTRY && keyboard) {
|
if (state == WifiScreenState::PASSWORD_ENTRY && keyboard) {
|
||||||
keyboard->handleInput();
|
keyboard->handleInput();
|
||||||
|
|
||||||
if (keyboard->isComplete()) {
|
if (keyboard->isComplete()) {
|
||||||
@ -281,7 +282,7 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keyboard->isCancelled()) {
|
if (keyboard->isCancelled()) {
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiScreenState::NETWORK_LIST;
|
||||||
keyboard.reset();
|
keyboard.reset();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
@ -292,7 +293,7 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle save prompt state
|
// Handle save prompt state
|
||||||
if (state == WifiSelectionState::SAVE_PROMPT) {
|
if (state == WifiScreenState::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--;
|
||||||
@ -308,17 +309,19 @@ void WifiSelectionActivity::loop() {
|
|||||||
// User chose "Yes" - save the password
|
// User chose "Yes" - save the password
|
||||||
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
|
WIFI_STORE.addCredential(selectedSSID, enteredPassword);
|
||||||
}
|
}
|
||||||
// Complete - parent will start web server
|
// Move to connected screen
|
||||||
onComplete(true);
|
state = WifiScreenState::CONNECTED;
|
||||||
|
updateRequired = true;
|
||||||
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
// Skip saving, complete anyway
|
// Skip saving, go to connected screen
|
||||||
onComplete(true);
|
state = WifiScreenState::CONNECTED;
|
||||||
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle forget prompt state (connection failed with saved credentials)
|
// Handle forget prompt state (connection failed with saved credentials)
|
||||||
if (state == WifiSelectionState::FORGET_PROMPT) {
|
if (state == WifiScreenState::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--;
|
||||||
@ -342,33 +345,35 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Go back to network list
|
// Go back to network list
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiScreenState::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 = WifiSelectionState::NETWORK_LIST;
|
state = WifiScreenState::NETWORK_LIST;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle connected state (should not normally be reached - connection completes immediately)
|
// Handle connected state
|
||||||
if (state == WifiSelectionState::CONNECTED) {
|
if (state == WifiScreenState::CONNECTED) {
|
||||||
// Safety fallback - immediately complete
|
if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
onComplete(true);
|
// Exit screen on success
|
||||||
return;
|
onGoBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle connection failed state
|
// Handle connection failed state
|
||||||
if (state == WifiSelectionState::CONNECTION_FAILED) {
|
if (state == WifiScreenState::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 = WifiSelectionState::FORGET_PROMPT;
|
state = WifiScreenState::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 = WifiSelectionState::NETWORK_LIST;
|
state = WifiScreenState::NETWORK_LIST;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
@ -376,10 +381,10 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle network list state
|
// Handle network list state
|
||||||
if (state == WifiSelectionState::NETWORK_LIST) {
|
if (state == WifiScreenState::NETWORK_LIST) {
|
||||||
// Check for Back button to exit (cancel)
|
// Check for Back button to exit
|
||||||
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
onComplete(false);
|
onGoBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +413,7 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string WifiSelectionActivity::getSignalStrengthIndicator(int32_t rssi) const {
|
std::string WifiScreen::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
|
||||||
@ -422,7 +427,7 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(int32_t rssi) cons
|
|||||||
return " "; // Very weak
|
return " "; // Very weak
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::displayTaskLoop() {
|
void WifiScreen::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
@ -434,32 +439,32 @@ void WifiSelectionActivity::displayTaskLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::render() const {
|
void WifiScreen::render() const {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case WifiSelectionState::SCANNING:
|
case WifiScreenState::SCANNING:
|
||||||
renderConnecting(); // Reuse connecting screen with different message
|
renderConnecting(); // Reuse connecting screen with different message
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::NETWORK_LIST:
|
case WifiScreenState::NETWORK_LIST:
|
||||||
renderNetworkList();
|
renderNetworkList();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::PASSWORD_ENTRY:
|
case WifiScreenState::PASSWORD_ENTRY:
|
||||||
renderPasswordEntry();
|
renderPasswordEntry();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::CONNECTING:
|
case WifiScreenState::CONNECTING:
|
||||||
renderConnecting();
|
renderConnecting();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::CONNECTED:
|
case WifiScreenState::CONNECTED:
|
||||||
renderConnected();
|
renderConnected();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::SAVE_PROMPT:
|
case WifiScreenState::SAVE_PROMPT:
|
||||||
renderSavePrompt();
|
renderSavePrompt();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::CONNECTION_FAILED:
|
case WifiScreenState::CONNECTION_FAILED:
|
||||||
renderConnectionFailed();
|
renderConnectionFailed();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::FORGET_PROMPT:
|
case WifiScreenState::FORGET_PROMPT:
|
||||||
renderForgetPrompt();
|
renderForgetPrompt();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -467,7 +472,7 @@ void WifiSelectionActivity::render() const {
|
|||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::renderNetworkList() const {
|
void WifiScreen::renderNetworkList() const {
|
||||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||||
|
|
||||||
@ -543,7 +548,7 @@ void WifiSelectionActivity::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 WifiSelectionActivity::renderPasswordEntry() const {
|
void WifiScreen::renderPasswordEntry() const {
|
||||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||||
|
|
||||||
// Draw header
|
// Draw header
|
||||||
@ -562,12 +567,12 @@ void WifiSelectionActivity::renderPasswordEntry() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::renderConnecting() const {
|
void WifiScreen::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 == WifiSelectionState::SCANNING) {
|
if (state == WifiScreenState::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);
|
||||||
@ -580,7 +585,7 @@ void WifiSelectionActivity::renderConnecting() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::renderConnected() const {
|
void WifiScreen::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);
|
||||||
@ -597,10 +602,14 @@ void WifiSelectionActivity::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);
|
||||||
|
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue", 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::renderSavePrompt() const {
|
void WifiScreen::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);
|
||||||
@ -640,7 +649,7 @@ void WifiSelectionActivity::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 WifiSelectionActivity::renderConnectionFailed() const {
|
void WifiScreen::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;
|
||||||
@ -650,7 +659,7 @@ void WifiSelectionActivity::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 WifiSelectionActivity::renderForgetPrompt() const {
|
void WifiScreen::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);
|
||||||
@ -10,7 +10,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "../util/KeyboardEntryActivity.h"
|
#include "OnScreenKeyboard.h"
|
||||||
|
|
||||||
// Structure to hold WiFi network information
|
// Structure to hold WiFi network information
|
||||||
struct WifiNetworkInfo {
|
struct WifiNetworkInfo {
|
||||||
@ -20,44 +20,33 @@ struct WifiNetworkInfo {
|
|||||||
bool hasSavedPassword; // Whether we have saved credentials for this network
|
bool hasSavedPassword; // Whether we have saved credentials for this network
|
||||||
};
|
};
|
||||||
|
|
||||||
// WiFi selection states
|
// WiFi screen states
|
||||||
enum class WifiSelectionState {
|
enum class WifiScreenState {
|
||||||
SCANNING, // Scanning for networks
|
SCANNING, // Scanning for networks
|
||||||
NETWORK_LIST, // Displaying available networks
|
NETWORK_LIST, // Displaying available networks
|
||||||
PASSWORD_ENTRY, // Entering password for selected network
|
PASSWORD_ENTRY, // Entering password for selected network
|
||||||
CONNECTING, // Attempting to connect
|
CONNECTING, // Attempting to connect
|
||||||
CONNECTED, // Successfully connected
|
CONNECTED, // Successfully connected, showing IP
|
||||||
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;
|
||||||
WifiSelectionState state = WifiSelectionState::SCANNING;
|
WifiScreenState state = WifiScreenState::SCANNING;
|
||||||
int selectedNetworkIndex = 0;
|
int selectedNetworkIndex = 0;
|
||||||
std::vector<WifiNetworkInfo> networks;
|
std::vector<WifiNetworkInfo> networks;
|
||||||
const std::function<void(bool connected)> onComplete;
|
const std::function<void()> onGoBack;
|
||||||
|
|
||||||
// Selected network for connection
|
// Selected network for connection
|
||||||
std::string selectedSSID;
|
std::string selectedSSID;
|
||||||
bool selectedRequiresPassword = false;
|
bool selectedRequiresPassword = false;
|
||||||
|
|
||||||
// On-screen keyboard for password entry
|
// On-screen keyboard for password entry
|
||||||
std::unique_ptr<KeyboardEntryActivity> keyboard;
|
std::unique_ptr<OnScreenKeyboard> keyboard;
|
||||||
|
|
||||||
// Connection result
|
// Connection result
|
||||||
std::string connectedIP;
|
std::string connectedIP;
|
||||||
@ -96,13 +85,9 @@ class WifiSelectionActivity final : public Activity {
|
|||||||
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
|
explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoBack)
|
||||||
const std::function<void(bool connected)>& onComplete)
|
: Activity(renderer, inputManager), onGoBack(onGoBack) {}
|
||||||
: 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; }
|
|
||||||
};
|
};
|
||||||
@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
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);
|
||||||
@ -47,7 +48,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)) {
|
||||||
toggleCurrentSetting();
|
activateCurrentSetting();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -72,6 +73,26 @@ 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) {
|
||||||
@ -114,6 +135,8 @@ 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
|
||||||
@ -130,11 +153,13 @@ 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 toggle, BACK to save & exit");
|
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to select, BACK to save & exit");
|
||||||
|
|
||||||
// Always use standard refresh for settings screen
|
// Always use standard refresh for settings screen
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE };
|
enum class SettingType { TOGGLE, ACTION };
|
||||||
|
|
||||||
// Structure to hold setting information
|
// Structure to hold setting information
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
@ -27,19 +27,22 @@ 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 = 2; // Number of settings
|
static constexpr int settingsCount = 3; // 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,
|
||||||
: Activity(renderer, inputManager), onGoHome(onGoHome) {}
|
const std::function<void()>& onGoWifi)
|
||||||
|
: 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;
|
||||||
|
|||||||
50
src/main.cpp
50
src/main.cpp
@ -17,10 +17,11 @@
|
|||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "CrossPointWebServer.h"
|
||||||
#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/CrossPointWebServerActivity.h"
|
#include "activities/network/WifiScreen.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"
|
||||||
@ -44,7 +45,6 @@ EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
|||||||
InputManager inputManager;
|
InputManager inputManager;
|
||||||
GfxRenderer renderer(einkDisplay);
|
GfxRenderer renderer(einkDisplay);
|
||||||
Activity* currentActivity;
|
Activity* currentActivity;
|
||||||
CrossPointWebServerActivity* webServerActivity = nullptr; // Track web server activity for loop timing
|
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
EpdFont bookerlyFont(&bookerly_2b);
|
EpdFont bookerlyFont(&bookerly_2b);
|
||||||
@ -72,8 +72,6 @@ void exitActivity() {
|
|||||||
if (currentActivity) {
|
if (currentActivity) {
|
||||||
currentActivity->onExit();
|
currentActivity->onExit();
|
||||||
delete currentActivity;
|
delete currentActivity;
|
||||||
currentActivity = nullptr;
|
|
||||||
webServerActivity = nullptr; // Clear web server activity pointer when exiting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,20 +143,21 @@ void onGoToReader(const std::string& initialEpubPath) {
|
|||||||
}
|
}
|
||||||
void onGoToReaderHome() { onGoToReader(std::string()); }
|
void onGoToReaderHome() { onGoToReader(std::string()); }
|
||||||
|
|
||||||
void onGoToFileTransfer() {
|
void onGoToSettings();
|
||||||
|
|
||||||
|
void onGoToWifi() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
webServerActivity = new CrossPointWebServerActivity(renderer, inputManager, onGoHome);
|
enterNewActivity(new WifiScreen(renderer, inputManager, onGoToSettings));
|
||||||
enterNewActivity(webServerActivity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToSettings() {
|
void onGoToSettings() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
|
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome, onGoToWifi));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoHome() {
|
void onGoHome() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer));
|
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@ -205,8 +204,18 @@ 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
|
||||||
|
// This is critical for upload performance and preventing TCP timeouts
|
||||||
|
if (crossPointWebServer.isRunning()) {
|
||||||
|
delay(1); // Minimal delay to prevent tight loop
|
||||||
|
} else {
|
||||||
|
delay(10); // Normal delay when webserver not active
|
||||||
|
}
|
||||||
|
|
||||||
static unsigned long lastMemPrint = 0;
|
static unsigned long lastMemPrint = 0;
|
||||||
if (Serial && millis() - lastMemPrint >= 10000) {
|
if (Serial && millis() - lastMemPrint >= 10000) {
|
||||||
@ -242,6 +251,20 @@ 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;
|
||||||
@ -252,13 +275,4 @@ void loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastLoopTime = loopStartTime;
|
lastLoopTime = loopStartTime;
|
||||||
|
|
||||||
// Add delay at the end of the loop to prevent tight spinning
|
|
||||||
// When webserver is running, use yield() instead of delay for faster response
|
|
||||||
// When webserver is not running, use longer delay to save power
|
|
||||||
if (webServerActivity && webServerActivity->isWebServerRunning()) {
|
|
||||||
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
|
||||||
} else {
|
|
||||||
delay(10); // Normal delay when webserver not active
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user