From 5bf721ae42455750521dfb5d2613a2fd04caacff Mon Sep 17 00:00:00 2001 From: altsysrq Date: Mon, 5 Jan 2026 21:01:33 -0600 Subject: [PATCH 1/5] Add FTP server support with protocol selection and QR codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit integrates FTP server functionality alongside the existing HTTP server, allowing users to choose their preferred file transfer protocol. Key changes: - Added SimpleFTPServer library dependency (configured for SdFat2) - Created CrossPointFtpServer wrapper for FTP server management - Added network credentials to CrossPointSettings (ftpUsername, ftpPassword, httpUsername, httpPassword) - Created ProtocolSelectionActivity for HTTP/FTP choice - Created FileTransferActivity to replace CrossPointWebServerActivity - Supports both HTTP and FTP protocols - Shows QR codes for WiFi credentials (AP mode) and server URLs - Displays FTP credentials on screen for easy access - Updated main.cpp to use FileTransferActivity instead of CrossPointWebServerActivity The FTP server provides an alternative file transfer method that works well with dedicated FTP clients and offers better performance for large file transfers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- platformio.ini | 1 + src/CrossPointSettings.cpp | 14 +- src/CrossPointSettings.h | 7 + .../network/FileTransferActivity.cpp | 487 ++++++++++++++++++ src/activities/network/FileTransferActivity.h | 78 +++ .../network/ProtocolSelectionActivity.cpp | 123 +++++ .../network/ProtocolSelectionActivity.h | 30 ++ src/main.cpp | 4 +- src/network/CrossPointFtpServer.cpp | 65 +++ src/network/CrossPointFtpServer.h | 28 + 10 files changed, 834 insertions(+), 3 deletions(-) create mode 100644 src/activities/network/FileTransferActivity.cpp create mode 100644 src/activities/network/FileTransferActivity.h create mode 100644 src/activities/network/ProtocolSelectionActivity.cpp create mode 100644 src/activities/network/ProtocolSelectionActivity.h create mode 100644 src/network/CrossPointFtpServer.cpp create mode 100644 src/network/CrossPointFtpServer.h diff --git a/platformio.ini b/platformio.ini index 75d1a77b..afa720da 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,6 +45,7 @@ lib_deps = SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager ArduinoJson @ 7.4.2 QRCode @ 0.0.1 + xreef/SimpleFTPServer @ 3.0.1 [env:default] extends = base diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index d304c4e4..2ea2adfc 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 13; +constexpr uint8_t SETTINGS_COUNT = 17; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -40,6 +40,10 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, paragraphAlignment); serialization::writePod(outputFile, sleepTimeout); serialization::writePod(outputFile, refreshFrequency); + serialization::writeString(outputFile, ftpUsername); + serialization::writeString(outputFile, ftpPassword); + serialization::writeString(outputFile, httpUsername); + serialization::writeString(outputFile, httpPassword); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -92,6 +96,14 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, refreshFrequency); if (++settingsRead >= fileSettingsCount) break; + serialization::readString(inputFile, ftpUsername); + if (++settingsRead >= fileSettingsCount) break; + serialization::readString(inputFile, ftpPassword); + if (++settingsRead >= fileSettingsCount) break; + serialization::readString(inputFile, httpUsername); + if (++settingsRead >= fileSettingsCount) break; + serialization::readString(inputFile, httpPassword); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index bb38df68..2cad82ae 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include class CrossPointSettings { private: @@ -75,6 +76,12 @@ class CrossPointSettings { // E-ink refresh frequency (default 15 pages) uint8_t refreshFrequency = REFRESH_15; + // Network credentials for FTP and HTTP servers + std::string ftpUsername = "crosspoint"; + std::string ftpPassword = "reader"; + std::string httpUsername = "crosspoint"; + std::string httpPassword = "reader"; + ~CrossPointSettings() = default; // Get singleton instance diff --git a/src/activities/network/FileTransferActivity.cpp b/src/activities/network/FileTransferActivity.cpp new file mode 100644 index 00000000..142efe05 --- /dev/null +++ b/src/activities/network/FileTransferActivity.cpp @@ -0,0 +1,487 @@ +#include "FileTransferActivity.h" + +#include +#include +#include +#include +#include + +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "WifiSelectionActivity.h" +#include "fontIds.h" + +namespace { +// AP Mode configuration +constexpr const char* AP_SSID = "CrossPoint-Reader"; +constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use +constexpr const char* AP_HOSTNAME = "crosspoint"; +constexpr uint8_t AP_CHANNEL = 1; +constexpr uint8_t AP_MAX_CONNECTIONS = 4; + +// DNS server for captive portal (redirects all DNS queries to our IP) +DNSServer* dnsServer = nullptr; +constexpr uint16_t DNS_PORT = 53; + +void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data) { + // Implementation of QR code calculation + // The structure to manage the QR code + QRCode qrcode; + uint8_t qrcodeBytes[qrcode_getBufferSize(4)]; + Serial.printf("[%lu] [FTACT] QR Code (%lu): %s\n", millis(), data.length(), data.c_str()); + + qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str()); + const uint8_t px = 6; // pixels per module + for (uint8_t cy = 0; cy < qrcode.size; cy++) { + for (uint8_t cx = 0; cx < qrcode.size; cx++) { + if (qrcode_getModule(&qrcode, cx, cy)) { + renderer.fillRect(x + px * cx, y + px * cy, px, px, true); + } + } + } +} +} // namespace + +void FileTransferActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void FileTransferActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + + Serial.printf("[%lu] [FTACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap()); + + renderingMutex = xSemaphoreCreateMutex(); + + // Reset state + state = FileTransferActivityState::PROTOCOL_SELECTION; + selectedProtocol = FileTransferProtocol::HTTP; + networkMode = NetworkMode::JOIN_NETWORK; + isApMode = false; + connectedIP.clear(); + connectedSSID.clear(); + lastHandleClientTime = 0; + updateRequired = true; + + xTaskCreate(&FileTransferActivity::taskTrampoline, "FileTransferTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); + + // Launch protocol selection subactivity + Serial.printf("[%lu] [FTACT] Launching ProtocolSelectionActivity...\n", millis()); + enterNewActivity(new ProtocolSelectionActivity(renderer, mappedInput, + [this](const FileTransferProtocol protocol) { + onProtocolSelected(protocol); + })); +} + +void FileTransferActivity::onExit() { + ActivityWithSubactivity::onExit(); + + Serial.printf("[%lu] [FTACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap()); + + state = FileTransferActivityState::SHUTTING_DOWN; + + // Stop the server first (before disconnecting WiFi) + stopServer(); + + // Stop mDNS + MDNS.end(); + + // Stop DNS server if running (AP mode) + if (dnsServer) { + Serial.printf("[%lu] [FTACT] Stopping DNS server...\n", millis()); + dnsServer->stop(); + delete dnsServer; + dnsServer = nullptr; + } + + // CRITICAL: Wait for LWIP stack to flush any pending packets + Serial.printf("[%lu] [FTACT] Waiting 500ms for network stack to flush pending packets...\n", millis()); + delay(500); + + // Disconnect WiFi gracefully + if (isApMode) { + Serial.printf("[%lu] [FTACT] Stopping WiFi AP...\n", millis()); + WiFi.softAPdisconnect(true); + } else { + Serial.printf("[%lu] [FTACT] 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] [FTACT] Setting WiFi mode OFF...\n", millis()); + WiFi.mode(WIFI_OFF); + delay(100); // Allow WiFi hardware to fully power down + + Serial.printf("[%lu] [FTACT] [MEM] Free heap after WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap()); + + // Acquire mutex before deleting task + Serial.printf("[%lu] [FTACT] Acquiring rendering mutex before task deletion...\n", millis()); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + + // Delete the display task + Serial.printf("[%lu] [FTACT] Deleting display task...\n", millis()); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + Serial.printf("[%lu] [FTACT] Display task deleted\n", millis()); + } + + // Delete the mutex + Serial.printf("[%lu] [FTACT] Deleting mutex...\n", millis()); + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; + Serial.printf("[%lu] [FTACT] Mutex deleted\n", millis()); + + Serial.printf("[%lu] [FTACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap()); +} + +void FileTransferActivity::onProtocolSelected(const FileTransferProtocol protocol) { + Serial.printf("[%lu] [FTACT] Protocol selected: %s\n", millis(), protocol == FileTransferProtocol::HTTP ? "HTTP" : "FTP"); + + selectedProtocol = protocol; + + // Exit protocol selection subactivity + exitActivity(); + + // Launch network mode selection + state = FileTransferActivityState::MODE_SELECTION; + Serial.printf("[%lu] [FTACT] Launching NetworkModeSelectionActivity...\n", millis()); + enterNewActivity(new NetworkModeSelectionActivity( + renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, + [this]() { onGoBack(); } // Cancel goes back to home + )); +} + +void FileTransferActivity::onNetworkModeSelected(const NetworkMode mode) { + Serial.printf("[%lu] [FTACT] Network mode selected: %s\n", millis(), + mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot"); + + networkMode = mode; + isApMode = (mode == NetworkMode::CREATE_HOTSPOT); + + // Exit mode selection subactivity + exitActivity(); + + if (mode == NetworkMode::JOIN_NETWORK) { + // STA mode - launch WiFi selection + Serial.printf("[%lu] [FTACT] Turning on WiFi (STA mode)...\n", millis()); + WiFi.mode(WIFI_STA); + + state = FileTransferActivityState::WIFI_SELECTION; + Serial.printf("[%lu] [FTACT] Launching WifiSelectionActivity...\n", millis()); + enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, + [this](const bool connected) { onWifiSelectionComplete(connected); })); + } else { + // AP mode - start access point + state = FileTransferActivityState::AP_STARTING; + updateRequired = true; + startAccessPoint(); + } +} + +void FileTransferActivity::onWifiSelectionComplete(const bool connected) { + Serial.printf("[%lu] [FTACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected); + + if (connected) { + // Get connection info before exiting subactivity + connectedIP = static_cast(subActivity.get())->getConnectedIP(); + connectedSSID = WiFi.SSID().c_str(); + isApMode = false; + + exitActivity(); + + // Start mDNS for hostname resolution + if (MDNS.begin(AP_HOSTNAME)) { + Serial.printf("[%lu] [FTACT] mDNS started: %s.local\n", millis(), AP_HOSTNAME); + } + + // Start the server + startServer(); + } else { + // User cancelled - go back to mode selection + exitActivity(); + state = FileTransferActivityState::MODE_SELECTION; + enterNewActivity(new NetworkModeSelectionActivity( + renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, + [this]() { onGoBack(); })); + } +} + +void FileTransferActivity::startAccessPoint() { + Serial.printf("[%lu] [FTACT] Starting Access Point mode...\n", millis()); + Serial.printf("[%lu] [FTACT] [MEM] Free heap before AP start: %d bytes\n", millis(), ESP.getFreeHeap()); + + // Configure and start the AP + WiFi.mode(WIFI_AP); + delay(100); + + // Start soft AP + bool apStarted; + if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) { + apStarted = WiFi.softAP(AP_SSID, AP_PASSWORD, AP_CHANNEL, false, AP_MAX_CONNECTIONS); + } else { + // Open network (no password) + apStarted = WiFi.softAP(AP_SSID, nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS); + } + + if (!apStarted) { + Serial.printf("[%lu] [FTACT] ERROR: Failed to start Access Point!\n", millis()); + onGoBack(); + return; + } + + delay(100); // Wait for AP to fully initialize + + // Get AP IP address + const IPAddress apIP = WiFi.softAPIP(); + char ipStr[16]; + snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); + connectedIP = ipStr; + connectedSSID = AP_SSID; + + Serial.printf("[%lu] [FTACT] Access Point started!\n", millis()); + Serial.printf("[%lu] [FTACT] SSID: %s\n", millis(), AP_SSID); + Serial.printf("[%lu] [FTACT] IP: %s\n", millis(), connectedIP.c_str()); + + // Start mDNS for hostname resolution + if (MDNS.begin(AP_HOSTNAME)) { + Serial.printf("[%lu] [FTACT] mDNS started: %s.local\n", millis(), AP_HOSTNAME); + } else { + Serial.printf("[%lu] [FTACT] WARNING: mDNS failed to start\n", millis()); + } + + // Start DNS server for captive portal behavior + // This redirects all DNS queries to our IP, making any domain typed resolve to us + dnsServer = new DNSServer(); + dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + dnsServer->start(DNS_PORT, "*", apIP); + Serial.printf("[%lu] [FTACT] DNS server started for captive portal\n", millis()); + + Serial.printf("[%lu] [FTACT] [MEM] Free heap after AP start: %d bytes\n", millis(), ESP.getFreeHeap()); + + // Start the server + startServer(); +} + +void FileTransferActivity::startServer() { + Serial.printf("[%lu] [FTACT] Starting %s server...\n", millis(), + selectedProtocol == FileTransferProtocol::HTTP ? "HTTP" : "FTP"); + + if (selectedProtocol == FileTransferProtocol::HTTP) { + // Create and start HTTP server + webServer.reset(new CrossPointWebServer()); + webServer->begin(); + + if (webServer->isRunning()) { + state = FileTransferActivityState::SERVER_RUNNING; + Serial.printf("[%lu] [FTACT] HTTP server started successfully\n", millis()); + } else { + Serial.printf("[%lu] [FTACT] ERROR: Failed to start HTTP server!\n", millis()); + webServer.reset(); + onGoBack(); + return; + } + } else { + // Create and start FTP server + ftpServer.reset(new CrossPointFtpServer()); + if (ftpServer->begin()) { + state = FileTransferActivityState::SERVER_RUNNING; + Serial.printf("[%lu] [FTACT] FTP server started successfully\n", millis()); + } else { + Serial.printf("[%lu] [FTACT] ERROR: Failed to start FTP server!\n", millis()); + ftpServer.reset(); + onGoBack(); + return; + } + } + + // Force an immediate render since we're transitioning from a subactivity + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + Serial.printf("[%lu] [FTACT] Rendered File Transfer screen\n", millis()); +} + +void FileTransferActivity::stopServer() { + if (webServer && webServer->isRunning()) { + Serial.printf("[%lu] [FTACT] Stopping HTTP server...\n", millis()); + webServer->stop(); + Serial.printf("[%lu] [FTACT] HTTP server stopped\n", millis()); + } + webServer.reset(); + + if (ftpServer && ftpServer->running()) { + Serial.printf("[%lu] [FTACT] Stopping FTP server...\n", millis()); + ftpServer->stop(); + Serial.printf("[%lu] [FTACT] FTP server stopped\n", millis()); + } + ftpServer.reset(); +} + +void FileTransferActivity::loop() { + if (subActivity) { + // Forward loop to subactivity + subActivity->loop(); + return; + } + + // Handle different states + if (state == FileTransferActivityState::SERVER_RUNNING) { + // Handle DNS requests for captive portal (AP mode only) + if (isApMode && dnsServer) { + dnsServer->processNextRequest(); + } + + // Handle server requests + if (selectedProtocol == FileTransferProtocol::HTTP && webServer && webServer->isRunning()) { + const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime; + + // Log if there's a significant gap between handleClient calls (>100ms) + if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) { + Serial.printf("[%lu] [FTACT] WARNING: %lu ms gap since last handleClient\n", millis(), + timeSinceLastHandleClient); + } + + // Call handleClient multiple times to process pending requests faster + constexpr int HANDLE_CLIENT_ITERATIONS = 10; + for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) { + webServer->handleClient(); + } + lastHandleClientTime = millis(); + } else if (selectedProtocol == FileTransferProtocol::FTP && ftpServer && ftpServer->running()) { + ftpServer->handleClient(); + } + + // Handle exit on Back button + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + onGoBack(); + return; + } + } +} + +void FileTransferActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void FileTransferActivity::render() const { + // Only render our own UI when server is running + // Subactivities handle their own rendering + if (state == FileTransferActivityState::SERVER_RUNNING) { + renderer.clearScreen(); + renderServerRunning(); + renderer.displayBuffer(); + } else if (state == FileTransferActivityState::AP_STARTING) { + renderer.clearScreen(); + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + } +} + +void FileTransferActivity::renderServerRunning() const { + // Use consistent line spacing + constexpr int LINE_SPACING = 28; // Space between lines + + const char* protocolName = selectedProtocol == FileTransferProtocol::HTTP ? "HTTP" : "FTP"; + const std::string title = std::string("File Transfer (") + protocolName + ")"; + renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD); + + if (isApMode) { + // AP mode display - center the content block + int startY = 55; + + renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD); + + std::string ssidInfo = "Network: " + connectedSSID; + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); + + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, "Connect your device to this WiFi network"); + + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, + "or scan QR code with your phone to connect to WiFi:"); + // Show QR code for WiFi + const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; + drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); + + startY += 6 * 29 + 3 * LINE_SPACING; + + // Show URL based on protocol + std::string serverUrl; + if (selectedProtocol == FileTransferProtocol::HTTP) { + serverUrl = std::string("http://") + AP_HOSTNAME + ".local/"; + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, serverUrl.c_str(), true, EpdFontFamily::BOLD); + + std::string ipUrl = "or http://" + connectedIP + "/"; + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Open this URL in your browser"); + } else { + // FTP URL with credentials + serverUrl = std::string("ftp://") + SETTINGS.ftpUsername + ":" + SETTINGS.ftpPassword + "@" + connectedIP + "/"; + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, serverUrl.c_str(), true, EpdFontFamily::BOLD); + + std::string ftpInfo = "User: " + SETTINGS.ftpUsername + " | Pass: " + SETTINGS.ftpPassword; + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ftpInfo.c_str()); + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Use FTP client or scan QR code:"); + } + + // Show QR code for server URL + drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, serverUrl); + } else { + // STA mode display + const int startY = 65; + + std::string ssidInfo = "Network: " + connectedSSID; + if (ssidInfo.length() > 28) { + ssidInfo.replace(25, ssidInfo.length() - 25, "..."); + } + renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str()); + + std::string ipInfo = "IP Address: " + connectedIP; + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str()); + + // Show server URL based on protocol + std::string serverUrl; + if (selectedProtocol == FileTransferProtocol::HTTP) { + serverUrl = "http://" + connectedIP + "/"; + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, serverUrl.c_str(), true, EpdFontFamily::BOLD); + + std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/"; + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); + + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser"); + } else { + // FTP URL with credentials + serverUrl = std::string("ftp://") + SETTINGS.ftpUsername + ":" + SETTINGS.ftpPassword + "@" + connectedIP + "/"; + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, serverUrl.c_str(), true, EpdFontFamily::BOLD); + + std::string ftpInfo = "User: " + SETTINGS.ftpUsername + " | Pass: " + SETTINGS.ftpPassword; + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, ftpInfo.c_str()); + + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Use FTP client or scan QR code:"); + } + + // Show QR code for server URL + renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:"); + drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, serverUrl); + } + + const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); +} diff --git a/src/activities/network/FileTransferActivity.h b/src/activities/network/FileTransferActivity.h new file mode 100644 index 00000000..77004ff4 --- /dev/null +++ b/src/activities/network/FileTransferActivity.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "NetworkModeSelectionActivity.h" +#include "ProtocolSelectionActivity.h" +#include "activities/ActivityWithSubactivity.h" +#include "network/CrossPointFtpServer.h" +#include "network/CrossPointWebServer.h" + +// File transfer activity states +enum class FileTransferActivityState { + PROTOCOL_SELECTION, // Choosing between HTTP and FTP + MODE_SELECTION, // Choosing between Join Network and Create Hotspot + WIFI_SELECTION, // WiFi selection subactivity is active (for Join Network mode) + AP_STARTING, // Starting Access Point mode + SERVER_RUNNING, // Server is running and handling requests + SHUTTING_DOWN // Shutting down server and WiFi +}; + +/** + * FileTransferActivity is the entry point for file transfer functionality. + * It allows users to choose between HTTP and FTP protocols for file transfer. + */ +class FileTransferActivity final : public ActivityWithSubactivity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + FileTransferActivityState state = FileTransferActivityState::PROTOCOL_SELECTION; + const std::function onGoBack; + + // Selected protocol + FileTransferProtocol selectedProtocol = FileTransferProtocol::HTTP; + + // Network mode + NetworkMode networkMode = NetworkMode::JOIN_NETWORK; + bool isApMode = false; + + // Servers - owned by this activity + std::unique_ptr webServer; + std::unique_ptr ftpServer; + + // Server status + std::string connectedIP; + std::string connectedSSID; // For STA mode: network name, For AP mode: AP name + + // Performance monitoring + unsigned long lastHandleClientTime = 0; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void renderServerRunning() const; + + void onProtocolSelected(FileTransferProtocol protocol); + void onNetworkModeSelected(NetworkMode mode); + void onWifiSelectionComplete(bool connected); + void startAccessPoint(); + void startServer(); + void stopServer(); + + public: + explicit FileTransferActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onGoBack) + : ActivityWithSubactivity("FileTransfer", renderer, mappedInput), onGoBack(onGoBack) {} + void onEnter() override; + void onExit() override; + void loop() override; + bool skipLoopDelay() override { return (webServer && webServer->isRunning()) || (ftpServer && ftpServer->running()); } + bool preventAutoSleep() override { + return (webServer && webServer->isRunning()) || (ftpServer && ftpServer->running()); + } +}; diff --git a/src/activities/network/ProtocolSelectionActivity.cpp b/src/activities/network/ProtocolSelectionActivity.cpp new file mode 100644 index 00000000..d4dab5a6 --- /dev/null +++ b/src/activities/network/ProtocolSelectionActivity.cpp @@ -0,0 +1,123 @@ +#include "ProtocolSelectionActivity.h" + +#include + +#include "MappedInputManager.h" +#include "fontIds.h" + +namespace { +constexpr int MENU_ITEM_COUNT = 2; +const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"HTTP Server", "FTP Server"}; +const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {"Web-based file transfer via browser", + "FTP protocol for file transfer clients"}; +} // namespace + +void ProtocolSelectionActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void ProtocolSelectionActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + + // Reset selection + selectedIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&ProtocolSelectionActivity::taskTrampoline, "ProtocolSelTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void ProtocolSelectionActivity::onExit() { + Activity::onExit(); + + // Wait until not rendering to delete task + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void ProtocolSelectionActivity::loop() { + // Handle confirm button - select current option + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + const FileTransferProtocol protocol = (selectedIndex == 0) ? FileTransferProtocol::HTTP : FileTransferProtocol::FTP; + onProtocolSelected(protocol); + return; + } + + // Handle navigation + const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || + mappedInput.wasPressed(MappedInputManager::Button::Left); + const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || + mappedInput.wasPressed(MappedInputManager::Button::Right); + + if (prevPressed) { + selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; + updateRequired = true; + } else if (nextPressed) { + selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT; + updateRequired = true; + } +} + +void ProtocolSelectionActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void ProtocolSelectionActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + // Draw header + renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer Protocol", true, EpdFontFamily::BOLD); + + // Draw subtitle + renderer.drawCenteredText(UI_10_FONT_ID, 50, "Choose a protocol"); + + // Draw menu items centered on screen + constexpr int itemHeight = 50; // Height for each menu item (including description) + const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10; + + for (int i = 0; i < MENU_ITEM_COUNT; i++) { + const int itemY = startY + i * itemHeight; + const bool isSelected = (i == selectedIndex); + + // Draw selection highlight (black fill) for selected item + if (isSelected) { + renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6); + } + + // Draw text: black=false (white text) when selected (on black background) + // black=true (black text) when not selected (on white background) + renderer.drawText(UI_10_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected); + renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected); + } + + // Draw help text at bottom + const auto labels = mappedInput.mapLabels("", "Select", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/network/ProtocolSelectionActivity.h b/src/activities/network/ProtocolSelectionActivity.h new file mode 100644 index 00000000..4e526dd9 --- /dev/null +++ b/src/activities/network/ProtocolSelectionActivity.h @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +#include + +#include "../Activity.h" + +enum class FileTransferProtocol { HTTP, FTP }; + +class ProtocolSelectionActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + int selectedIndex = 0; + bool updateRequired = false; + const std::function onProtocolSelected; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + + public: + explicit ProtocolSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onProtocolSelected) + : Activity("ProtocolSelection", renderer, mappedInput), onProtocolSelected(onProtocolSelected) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/main.cpp b/src/main.cpp index e81448bd..86a2d4ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ #include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/SleepActivity.h" #include "activities/home/HomeActivity.h" -#include "activities/network/CrossPointWebServerActivity.h" +#include "activities/network/FileTransferActivity.h" #include "activities/reader/ReaderActivity.h" #include "activities/settings/SettingsActivity.h" #include "activities/util/FullScreenMessageActivity.h" @@ -214,7 +214,7 @@ void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onGoToFileTransfer() { exitActivity(); - enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); + enterNewActivity(new FileTransferActivity(renderer, mappedInputManager, onGoHome)); } void onGoToSettings() { diff --git a/src/network/CrossPointFtpServer.cpp b/src/network/CrossPointFtpServer.cpp new file mode 100644 index 00000000..31b96f6b --- /dev/null +++ b/src/network/CrossPointFtpServer.cpp @@ -0,0 +1,65 @@ +#include "CrossPointFtpServer.h" + +#include +#include +#include +#include + +#include "../CrossPointSettings.h" + +CrossPointFtpServer::~CrossPointFtpServer() { + stop(); +} + +bool CrossPointFtpServer::begin() { + if (isRunning) { + Serial.printf("[%lu] [FTP] Server already running\n", millis()); + return true; + } + + // Check WiFi connection + if (WiFi.status() != WL_CONNECTED && WiFi.getMode() != WIFI_AP) { + Serial.printf("[%lu] [FTP] WiFi not connected\n", millis()); + return false; + } + + // Disable WiFi sleep for better responsiveness + esp_wifi_set_ps(WIFI_PS_NONE); + + Serial.printf("[%lu] [FTP] Free heap before starting: %lu bytes\n", millis(), ESP.getFreeHeap()); + + // Create and start FTP server + ftpServer = new FtpServer(); + + // Start FTP server with credentials from settings + // The library automatically uses the global SdFat2 filesystem + ftpServer->begin(SETTINGS.ftpUsername.c_str(), SETTINGS.ftpPassword.c_str()); + + isRunning = true; + + Serial.printf("[%lu] [FTP] Server started on port 21\n", millis()); + Serial.printf("[%lu] [FTP] Username: %s\n", millis(), SETTINGS.ftpUsername.c_str()); + Serial.printf("[%lu] [FTP] Free heap after starting: %lu bytes\n", millis(), ESP.getFreeHeap()); + + return true; +} + +void CrossPointFtpServer::stop() { + if (!isRunning) { + return; + } + + if (ftpServer) { + delete ftpServer; + ftpServer = nullptr; + } + + isRunning = false; + Serial.printf("[%lu] [FTP] Server stopped\n", millis()); +} + +void CrossPointFtpServer::handleClient() { + if (isRunning && ftpServer) { + ftpServer->handleFTP(); + } +} diff --git a/src/network/CrossPointFtpServer.h b/src/network/CrossPointFtpServer.h new file mode 100644 index 00000000..fde1c778 --- /dev/null +++ b/src/network/CrossPointFtpServer.h @@ -0,0 +1,28 @@ +#pragma once + +// Configure SimpleFTPServer to use SdFat2 instead of SD library +#define DEFAULT_STORAGE_TYPE_ESP32 2 // STORAGE_SDFAT2 + +#include + +class CrossPointFtpServer { + private: + FtpServer* ftpServer = nullptr; + bool isRunning = false; + + public: + CrossPointFtpServer() = default; + ~CrossPointFtpServer(); + + // Initialize and start the FTP server + bool begin(); + + // Stop the FTP server + void stop(); + + // Handle FTP client requests (call this regularly in loop) + void handleClient(); + + // Check if server is running + bool running() const { return isRunning; } +}; From 1978739b43ac25c06f66913d099dfe62823c11e8 Mon Sep 17 00:00:00 2001 From: altsysrq Date: Mon, 5 Jan 2026 21:13:16 -0600 Subject: [PATCH 2/5] Add FTP username and password settings to configuration --- src/activities/settings/SettingsActivity.cpp | 31 +++++++++++++++----- src/activities/settings/SettingsActivity.h | 3 +- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index a242389d..69c2bd03 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -9,44 +9,53 @@ // Define the static settings list namespace { -constexpr int settingsCount = 14; +constexpr int settingsCount = 16; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE - {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, - {"Status Bar", SettingType::ENUM, &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}}, - {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}}, - {"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}}, + {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, nullptr, {"Dark", "Light", "Custom", "Cover"}}, + {"Status Bar", SettingType::ENUM, &CrossPointSettings::statusBar, nullptr, {"None", "No Progress", "Full"}}, + {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, nullptr, {}}, + {"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, nullptr, {}}, {"Reading Orientation", SettingType::ENUM, &CrossPointSettings::orientation, + nullptr, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}}, {"Front Button Layout", SettingType::ENUM, &CrossPointSettings::frontButtonLayout, + nullptr, {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}}, {"Side Button Layout (reader)", SettingType::ENUM, &CrossPointSettings::sideButtonLayout, + nullptr, {"Prev, Next", "Next, Prev"}}, {"Reader Font Family", SettingType::ENUM, &CrossPointSettings::fontFamily, + nullptr, {"Bookerly", "Noto Sans", "Open Dyslexic"}}, - {"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}}, - {"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}}, + {"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, nullptr, {"Small", "Medium", "Large", "X Large"}}, + {"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, nullptr, {"Tight", "Normal", "Wide"}}, {"Reader Paragraph Alignment", SettingType::ENUM, &CrossPointSettings::paragraphAlignment, + nullptr, {"Justify", "Left", "Center", "Right"}}, {"Time to Sleep", SettingType::ENUM, &CrossPointSettings::sleepTimeout, + nullptr, {"1 min", "5 min", "10 min", "15 min", "30 min"}}, {"Refresh Frequency", SettingType::ENUM, &CrossPointSettings::refreshFrequency, + nullptr, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}}, - {"Check for updates", SettingType::ACTION, nullptr, {}}, + {"FTP Username", SettingType::TEXT, nullptr, &CrossPointSettings::ftpUsername, {}}, + {"FTP Password", SettingType::TEXT, nullptr, &CrossPointSettings::ftpPassword, {}}, + {"Check for updates", SettingType::ACTION, nullptr, nullptr, {}}, }; } // namespace @@ -137,6 +146,10 @@ void SettingsActivity::toggleCurrentSetting() { } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { const uint8_t currentValue = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); + } else if (setting.type == SettingType::TEXT && setting.stringValuePtr != nullptr) { + // For now, TEXT settings are display-only in this UI + // In a future version, this could launch a text input dialog + return; } else if (setting.type == SettingType::ACTION) { if (std::string(setting.name) == "Check for updates") { xSemaphoreTake(renderingMutex, portMAX_DELAY); @@ -195,6 +208,8 @@ void SettingsActivity::render() const { } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); valueText = settingsList[i].enumValues[value]; + } else if (settingsList[i].type == SettingType::TEXT && settingsList[i].stringValuePtr != nullptr) { + valueText = SETTINGS.*(settingsList[i].stringValuePtr); } const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 83beb9d9..34351f8e 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -11,13 +11,14 @@ class CrossPointSettings; -enum class SettingType { TOGGLE, ENUM, ACTION }; +enum class SettingType { TOGGLE, ENUM, ACTION, TEXT }; // Structure to hold setting information struct SettingInfo { const char* name; // Display name of the setting SettingType type; // Type of setting uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM) + std::string CrossPointSettings::* stringValuePtr; // Pointer to string member (for TEXT) std::vector enumValues; }; From 32552e9d918951be7c56da60cb7d3158bd9684c5 Mon Sep 17 00:00:00 2001 From: altsysrq Date: Mon, 5 Jan 2026 21:30:31 -0600 Subject: [PATCH 3/5] Enhance CrossPointSettings with hotspot scheduler settings and update related serialization methods --- src/CrossPointSettings.cpp | 35 ++++++++++++++++++- src/CrossPointSettings.h | 12 +++++++ .../network/FileTransferActivity.cpp | 9 +++-- src/activities/settings/SettingsActivity.cpp | 12 +++++-- src/activities/settings/SettingsActivity.h | 18 +++++----- src/network/CrossPointFtpServer.cpp | 4 +-- 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 2ea2adfc..ca830ae0 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 17; +constexpr uint8_t SETTINGS_COUNT = 22; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -44,6 +44,11 @@ bool CrossPointSettings::saveToFile() const { serialization::writeString(outputFile, ftpPassword); serialization::writeString(outputFile, httpUsername); serialization::writeString(outputFile, httpPassword); + serialization::writePod(outputFile, hotspotSchedulerEnabled); + serialization::writePod(outputFile, hotspotSchedulerHour); + serialization::writePod(outputFile, hotspotSchedulerMinute); + serialization::writePod(outputFile, hotspotSchedulerShutdownTime); + serialization::writePod(outputFile, screenMargin); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -104,6 +109,16 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readString(inputFile, httpPassword); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, hotspotSchedulerEnabled); + if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, hotspotSchedulerHour); + if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, hotspotSchedulerMinute); + if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, hotspotSchedulerShutdownTime); + if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, screenMargin); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); @@ -220,3 +235,21 @@ int CrossPointSettings::getReaderFontId() const { } } } +unsigned int CrossPointSettings::getHotspotShutdownMinutes() const { + switch (hotspotSchedulerShutdownTime) { + case SHUTDOWN_5_MIN: + return 5; + case SHUTDOWN_10_MIN: + return 10; + case SHUTDOWN_15_MIN: + return 15; + case SHUTDOWN_30_MIN: + return 30; + case SHUTDOWN_60_MIN: + return 60; + case SHUTDOWN_120_MIN: + return 120; + default: + return 30; + } +} \ No newline at end of file diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 5415b90f..107bffb9 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -53,6 +53,9 @@ class CrossPointSettings { // E-ink refresh frequency (pages between full refreshes) enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 }; + // Hotspot scheduler settings + enum HOTSPOT_SHUTDOWN_TIME { SHUTDOWN_5_MIN = 0, SHUTDOWN_10_MIN = 1, SHUTDOWN_15_MIN = 2, SHUTDOWN_30_MIN = 3, SHUTDOWN_60_MIN = 4, SHUTDOWN_120_MIN = 5 }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -78,6 +81,8 @@ class CrossPointSettings { uint8_t sleepTimeout = SLEEP_10_MIN; // E-ink refresh frequency (default 15 pages) uint8_t refreshFrequency = REFRESH_15; + // Screen margin setting (in pixels, default 0) + uint8_t screenMargin = 0; // Network credentials for FTP and HTTP servers std::string ftpUsername = "crosspoint"; @@ -85,6 +90,12 @@ class CrossPointSettings { std::string httpUsername = "crosspoint"; std::string httpPassword = "reader"; + // Hotspot scheduler settings + uint8_t hotspotSchedulerEnabled = 0; // 0 = disabled, 1 = enabled + uint8_t hotspotSchedulerHour = 12; // Hour (0-23) + uint8_t hotspotSchedulerMinute = 0; // Minute (0-59) + uint8_t hotspotSchedulerShutdownTime = SHUTDOWN_30_MIN; // Default 30 minutes + ~CrossPointSettings() = default; // Get singleton instance @@ -92,6 +103,7 @@ class CrossPointSettings { uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 400; } int getReaderFontId() const; + unsigned int getHotspotShutdownMinutes() const; bool saveToFile() const; bool loadFromFile(); diff --git a/src/activities/network/FileTransferActivity.cpp b/src/activities/network/FileTransferActivity.cpp index 142efe05..07fe7a3c 100644 --- a/src/activities/network/FileTransferActivity.cpp +++ b/src/activities/network/FileTransferActivity.cpp @@ -75,10 +75,8 @@ void FileTransferActivity::onEnter() { // Launch protocol selection subactivity Serial.printf("[%lu] [FTACT] Launching ProtocolSelectionActivity...\n", millis()); - enterNewActivity(new ProtocolSelectionActivity(renderer, mappedInput, - [this](const FileTransferProtocol protocol) { - onProtocolSelected(protocol); - })); + enterNewActivity(new ProtocolSelectionActivity( + renderer, mappedInput, [this](const FileTransferProtocol protocol) { onProtocolSelected(protocol); })); } void FileTransferActivity::onExit() { @@ -144,7 +142,8 @@ void FileTransferActivity::onExit() { } void FileTransferActivity::onProtocolSelected(const FileTransferProtocol protocol) { - Serial.printf("[%lu] [FTACT] Protocol selected: %s\n", millis(), protocol == FileTransferProtocol::HTTP ? "HTTP" : "FTP"); + Serial.printf("[%lu] [FTACT] Protocol selected: %s\n", millis(), + protocol == FileTransferProtocol::HTTP ? "HTTP" : "FTP"); selectedProtocol = protocol; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index a51339d6..07acdcf1 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -13,7 +13,11 @@ namespace { constexpr int settingsCount = 16; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE - {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, nullptr, {"Dark", "Light", "Custom", "Cover"}}, + {"Sleep Screen", + SettingType::ENUM, + &CrossPointSettings::sleepScreen, + nullptr, + {"Dark", "Light", "Custom", "Cover"}}, {"Status Bar", SettingType::ENUM, &CrossPointSettings::statusBar, nullptr, {"None", "No Progress", "Full"}}, {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, nullptr, {}}, {"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, nullptr, {}}, @@ -37,7 +41,11 @@ const SettingInfo settingsList[settingsCount] = { &CrossPointSettings::fontFamily, nullptr, {"Bookerly", "Noto Sans", "Open Dyslexic"}}, - {"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, nullptr, {"Small", "Medium", "Large", "X Large"}}, + {"Reader Font Size", + SettingType::ENUM, + &CrossPointSettings::fontSize, + nullptr, + {"Small", "Medium", "Large", "X Large"}}, {"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, nullptr, {"Tight", "Normal", "Wide"}}, {"Reader Paragraph Alignment", SettingType::ENUM, diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 5ac17686..de87aa64 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -11,13 +11,13 @@ class CrossPointSettings; -enum class SettingType { TOGGLE, ENUM, ACTION, TEXT }; +enum class SettingType { TOGGLE, ENUM, ACTION, TEXT, VALUE }; // Structure to hold setting information struct SettingInfo { - const char* name; // Display name of the setting - SettingType type; // Type of setting - uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM) + const char* name; // Display name of the setting + SettingType type; // Type of setting + uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM) std::string CrossPointSettings::* stringValuePtr; // Pointer to string member (for TEXT) std::vector enumValues; @@ -31,17 +31,19 @@ struct SettingInfo { // Static constructors static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { - return {name, SettingType::TOGGLE, ptr}; + return {name, SettingType::TOGGLE, ptr, nullptr, {}, {0, 0, 0}}; } static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector values) { - return {name, SettingType::ENUM, ptr, std::move(values)}; + return {name, SettingType::ENUM, ptr, nullptr, std::move(values), {0, 0, 0}}; } - static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } + static SettingInfo Action(const char* name) { + return {name, SettingType::ACTION, nullptr, nullptr, {}, {0, 0, 0}}; + } static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { - return {name, SettingType::VALUE, ptr, {}, valueRange}; + return {name, SettingType::VALUE, ptr, nullptr, {}, valueRange}; } }; diff --git a/src/network/CrossPointFtpServer.cpp b/src/network/CrossPointFtpServer.cpp index 31b96f6b..62d9c02a 100644 --- a/src/network/CrossPointFtpServer.cpp +++ b/src/network/CrossPointFtpServer.cpp @@ -7,9 +7,7 @@ #include "../CrossPointSettings.h" -CrossPointFtpServer::~CrossPointFtpServer() { - stop(); -} +CrossPointFtpServer::~CrossPointFtpServer() { stop(); } bool CrossPointFtpServer::begin() { if (isRunning) { From f1ead2101a70c56fdc6d35b30804cfc60f54f4ad Mon Sep 17 00:00:00 2001 From: altsysrq Date: Mon, 5 Jan 2026 21:35:21 -0600 Subject: [PATCH 4/5] Refactor hotspot shutdown time enum for improved readability and add hotspot scheduler time settings --- src/CrossPointSettings.h | 15 +++++++++++---- src/activities/settings/SettingsActivity.h | 4 +--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 107bffb9..4e0b1db7 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -54,7 +54,14 @@ class CrossPointSettings { enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 }; // Hotspot scheduler settings - enum HOTSPOT_SHUTDOWN_TIME { SHUTDOWN_5_MIN = 0, SHUTDOWN_10_MIN = 1, SHUTDOWN_15_MIN = 2, SHUTDOWN_30_MIN = 3, SHUTDOWN_60_MIN = 4, SHUTDOWN_120_MIN = 5 }; + enum HOTSPOT_SHUTDOWN_TIME { + SHUTDOWN_5_MIN = 0, + SHUTDOWN_10_MIN = 1, + SHUTDOWN_15_MIN = 2, + SHUTDOWN_30_MIN = 3, + SHUTDOWN_60_MIN = 4, + SHUTDOWN_120_MIN = 5 + }; // Sleep screen settings uint8_t sleepScreen = DARK; @@ -84,9 +91,9 @@ class CrossPointSettings { // Screen margin setting (in pixels, default 0) uint8_t screenMargin = 0; - // Network credentials for FTP and HTTP servers - std::string ftpUsername = "crosspoint"; - std::string ftpPassword = "reader"; + // Network credentials for FTP and HTTP se // 0 = disabled, 1 = enabled + uint8_t hotspotSchedulerHour = 12; // Hour (0-23) + uint8_t hotspotSchedulerMinute = 0; std::string httpUsername = "crosspoint"; std::string httpPassword = "reader"; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index de87aa64..1afafd4f 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -38,9 +38,7 @@ struct SettingInfo { return {name, SettingType::ENUM, ptr, nullptr, std::move(values), {0, 0, 0}}; } - static SettingInfo Action(const char* name) { - return {name, SettingType::ACTION, nullptr, nullptr, {}, {0, 0, 0}}; - } + static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr, nullptr, {}, {0, 0, 0}}; } static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { return {name, SettingType::VALUE, ptr, nullptr, {}, valueRange}; From 81d4f76679ba3ca7500719b2039b69e1301a3008 Mon Sep 17 00:00:00 2001 From: altsysrq Date: Tue, 6 Jan 2026 18:15:48 -0600 Subject: [PATCH 5/5] Refactor hotspot scheduler settings for improved clarity and organization --- src/CrossPointSettings.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 4e0b1db7..fc6daad0 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -91,16 +91,16 @@ class CrossPointSettings { // Screen margin setting (in pixels, default 0) uint8_t screenMargin = 0; - // Network credentials for FTP and HTTP se // 0 = disabled, 1 = enabled - uint8_t hotspotSchedulerHour = 12; // Hour (0-23) - uint8_t hotspotSchedulerMinute = 0; + // Network credentials for FTP and HTTP servers + std::string ftpUsername = "crosspoint"; + std::string ftpPassword = "reader"; std::string httpUsername = "crosspoint"; std::string httpPassword = "reader"; // Hotspot scheduler settings - uint8_t hotspotSchedulerEnabled = 0; // 0 = disabled, 1 = enabled - uint8_t hotspotSchedulerHour = 12; // Hour (0-23) - uint8_t hotspotSchedulerMinute = 0; // Minute (0-59) + uint8_t hotspotSchedulerEnabled = 0; // 0 = disabled, 1 = enabled + uint8_t hotspotSchedulerHour = 12; // Hour (0-23) + uint8_t hotspotSchedulerMinute = 0; // Minute (0-59) uint8_t hotspotSchedulerShutdownTime = SHUTDOWN_30_MIN; // Default 30 minutes ~CrossPointSettings() = default;