feat: Add File Transfer functionality with HTTP and FTP protocols

- Introduced FileTransferActivity to manage file transfer operations.
- Added ProtocolSelectionActivity for users to choose between HTTP and FTP.
- Implemented WifiSelectionActivity to handle WiFi connections for file transfers.
- Created ScheduleSettingsActivity to configure automatic file transfer scheduling.
- Integrated CrossPointFtpServer to support FTP file transfers.
- Updated main application logic to trigger scheduled file transfers.
- Enhanced SettingsActivity to include an option for file transfer scheduling.
- Improved memory management and task handling in various activities.
This commit is contained in:
altsysrq
2026-01-04 16:37:49 -06:00
parent 1d8815249d
commit 39f403ae84
17 changed files with 995 additions and 172 deletions

View File

@@ -1,73 +0,0 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <memory>
#include <string>
#include "NetworkModeSelectionActivity.h"
#include "activities/ActivityWithSubactivity.h"
#include "network/CrossPointWebServer.h"
// Web server activity states
enum class WebServerActivityState {
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, // Web server is running and handling requests
SHUTTING_DOWN // Shutting down server and WiFi
};
/**
* CrossPointWebServerActivity is the entry point for file transfer functionality.
* It:
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
* - For STA mode: Launches WifiSelectionActivity to connect to an existing network
* - For AP mode: Creates an Access Point that clients can connect to
* - Starts the CrossPointWebServer when connected
* - Handles client requests in its loop() function
* - Cleans up the server and shuts down WiFi on exit
*/
class CrossPointWebServerActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
WebServerActivityState state = WebServerActivityState::MODE_SELECTION;
const std::function<void()> onGoBack;
// Network mode
NetworkMode networkMode = NetworkMode::JOIN_NETWORK;
bool isApMode = false;
// Web server - owned by this activity
std::unique_ptr<CrossPointWebServer> webServer;
// 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 onNetworkModeSelected(NetworkMode mode);
void onWifiSelectionComplete(bool connected);
void startAccessPoint();
void startWebServer();
void stopWebServer();
public:
explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CrossPointWebServer", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool skipLoopDelay() override { return webServer && webServer->isRunning(); }
};

View File

@@ -1,4 +1,4 @@
#include "CrossPointWebServerActivity.h"
#include "FileTransferActivity.h"
#include <DNSServer.h>
#include <ESPmDNS.h>
@@ -29,12 +29,12 @@ DNSServer* dnsServer = nullptr;
constexpr uint16_t DNS_PORT = 53;
} // namespace
void CrossPointWebServerActivity::taskTrampoline(void* param) {
auto* self = static_cast<CrossPointWebServerActivity*>(param);
void FileTransferActivity::taskTrampoline(void* param) {
auto* self = static_cast<FileTransferActivity*>(param);
self->displayTaskLoop();
}
void CrossPointWebServerActivity::onEnter() {
void FileTransferActivity::onEnter() {
ActivityWithSubactivity::onEnter();
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
@@ -42,7 +42,7 @@ void CrossPointWebServerActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
// Reset state
state = WebServerActivityState::MODE_SELECTION;
state = FileTransferActivityState::MODE_SELECTION;
networkMode = NetworkMode::JOIN_NETWORK;
isApMode = false;
connectedIP.clear();
@@ -50,7 +50,7 @@ void CrossPointWebServerActivity::onEnter() {
lastHandleClientTime = 0;
updateRequired = true;
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
xTaskCreate(&FileTransferActivity::taskTrampoline, "WebServerActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
@@ -65,15 +65,16 @@ void CrossPointWebServerActivity::onEnter() {
));
}
void CrossPointWebServerActivity::onExit() {
void FileTransferActivity::onExit() {
ActivityWithSubactivity::onExit();
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
state = WebServerActivityState::SHUTTING_DOWN;
state = FileTransferActivityState::SHUTTING_DOWN;
// Stop the web server first (before disconnecting WiFi)
stopWebServer();
// Stop the file transfer servers first (before disconnecting WiFi)
stopHttpServer();
stopFtpServer();
// Stop mDNS
MDNS.end();
@@ -127,7 +128,7 @@ void CrossPointWebServerActivity::onExit() {
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
}
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
void FileTransferActivity::onNetworkModeSelected(const NetworkMode mode) {
Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(),
mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot");
@@ -146,24 +147,41 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
// Exit mode selection subactivity
exitActivity();
if (mode == NetworkMode::JOIN_NETWORK) {
// Launch protocol selection subactivity
state = FileTransferActivityState::PROTOCOL_SELECTION;
Serial.printf("[%lu] [WEBACT] Launching ProtocolSelectionActivity...\n", millis());
enterNewActivity(new ProtocolSelectionActivity(
renderer, mappedInput, [this](const FileTransferProtocol protocol) { onProtocolSelected(protocol); },
[this]() { onGoBack(); }));
}
void FileTransferActivity::onProtocolSelected(const FileTransferProtocol protocol) {
Serial.printf("[%lu] [WEBACT] Protocol selected: %s\n", millis(),
protocol == FileTransferProtocol::HTTP ? "HTTP" : "FTP");
selectedProtocol = protocol;
// Exit protocol selection subactivity
exitActivity();
if (networkMode == NetworkMode::JOIN_NETWORK) {
// STA mode - launch WiFi selection
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
WiFi.mode(WIFI_STA);
state = WebServerActivityState::WIFI_SELECTION;
state = FileTransferActivityState::WIFI_SELECTION;
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
} else {
// AP mode - start access point
state = WebServerActivityState::AP_STARTING;
state = FileTransferActivityState::AP_STARTING;
updateRequired = true;
startAccessPoint();
}
}
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
void FileTransferActivity::onWifiSelectionComplete(const bool connected) {
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
if (connected) {
@@ -179,19 +197,19 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
}
// Start the web server
startWebServer();
// Start the file transfer server
startServer();
} else {
// User cancelled - go back to mode selection
exitActivity();
state = WebServerActivityState::MODE_SELECTION;
state = FileTransferActivityState::MODE_SELECTION;
enterNewActivity(new NetworkModeSelectionActivity(
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
[this]() { onGoBack(); }));
}
}
void CrossPointWebServerActivity::startAccessPoint() {
void FileTransferActivity::startAccessPoint() {
Serial.printf("[%lu] [WEBACT] Starting Access Point mode...\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap before AP start: %d bytes\n", millis(), ESP.getFreeHeap());
@@ -243,45 +261,78 @@ void CrossPointWebServerActivity::startAccessPoint() {
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after AP start: %d bytes\n", millis(), ESP.getFreeHeap());
// Start the web server
startWebServer();
// Start the file transfer server
startServer();
}
void CrossPointWebServerActivity::startWebServer() {
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
void FileTransferActivity::startServer() {
if (selectedProtocol == FileTransferProtocol::HTTP) {
Serial.printf("[%lu] [WEBACT] Starting HTTP server...\n", millis());
// Create the web server instance
webServer.reset(new CrossPointWebServer());
webServer->begin();
// Create the HTTP server instance
httpServer.reset(new CrossPointWebServer());
httpServer->begin();
if (webServer->isRunning()) {
state = WebServerActivityState::SERVER_RUNNING;
Serial.printf("[%lu] [WEBACT] Web server started successfully\n", millis());
if (httpServer->isRunning()) {
state = FileTransferActivityState::SERVER_RUNNING;
serverStartTime = millis(); // Track when server started
Serial.printf("[%lu] [WEBACT] HTTP 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());
// 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 HTTP server!\n", millis());
httpServer.reset();
onGoBack();
}
} else {
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start web server!\n", millis());
webServer.reset();
// Go back on error
onGoBack();
Serial.printf("[%lu] [WEBACT] Starting FTP server...\n", millis());
// Create the FTP server instance
ftpServer.reset(new CrossPointFtpServer());
ftpServer->begin();
if (ftpServer->isRunning()) {
state = FileTransferActivityState::SERVER_RUNNING;
serverStartTime = millis(); // Track when server started
Serial.printf("[%lu] [WEBACT] FTP server started successfully\n", millis());
// Force an immediate render
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 FTP server!\n", millis());
ftpServer.reset();
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());
void FileTransferActivity::stopHttpServer() {
if (httpServer && httpServer->isRunning()) {
Serial.printf("[%lu] [WEBACT] Stopping HTTP server...\n", millis());
httpServer->stop();
Serial.printf("[%lu] [WEBACT] HTTP server stopped\n", millis());
}
webServer.reset();
httpServer.reset();
}
void CrossPointWebServerActivity::loop() {
void FileTransferActivity::stopFtpServer() {
if (ftpServer && ftpServer->isRunning()) {
Serial.printf("[%lu] [WEBACT] Stopping FTP server...\n", millis());
ftpServer->stop();
Serial.printf("[%lu] [WEBACT] FTP server stopped\n", millis());
}
ftpServer.reset();
}
void FileTransferActivity::loop() {
if (subActivity) {
// Forward loop to subactivity
subActivity->loop();
@@ -289,15 +340,18 @@ void CrossPointWebServerActivity::loop() {
}
// Handle different states
if (state == WebServerActivityState::SERVER_RUNNING) {
if (state == FileTransferActivityState::SERVER_RUNNING) {
// Handle DNS requests for captive portal (AP mode only)
if (isApMode && dnsServer) {
dnsServer->processNextRequest();
}
// Handle web server requests - call handleClient multiple times per loop
// Handle file transfer server requests - call handleClient multiple times per loop
// to improve responsiveness and upload throughput
if (webServer && webServer->isRunning()) {
const bool httpRunning = httpServer && httpServer->isRunning();
const bool ftpRunning = ftpServer && ftpServer->isRunning();
if (httpRunning || ftpRunning) {
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
// Log if there's a significant gap between handleClient calls (>100ms)
@@ -307,12 +361,16 @@ void CrossPointWebServerActivity::loop() {
}
// Call handleClient multiple times to process pending requests faster
// This is critical for upload performance - HTTP file uploads send data
// This is critical for upload performance - file uploads send data
// in chunks and each handleClient() call processes incoming data
// Reduced from 10 to 3 to prevent watchdog timer issues
constexpr int HANDLE_CLIENT_ITERATIONS = 3;
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS; i++) {
if (httpRunning && httpServer->isRunning()) {
httpServer->handleClient();
} else if (ftpRunning && ftpServer->isRunning()) {
ftpServer->handleClient();
}
// Feed the watchdog timer between iterations to prevent resets
esp_task_wdt_reset();
// Yield to other tasks to prevent starvation
@@ -321,6 +379,18 @@ void CrossPointWebServerActivity::loop() {
lastHandleClientTime = millis();
}
// Check auto-shutdown timer if schedule is enabled
if (SETTINGS.scheduleEnabled && serverStartTime > 0) {
const unsigned long serverUptime = millis() - serverStartTime;
const unsigned long shutdownTimeout = SETTINGS.getAutoShutdownMs();
if (serverUptime >= shutdownTimeout) {
Serial.printf("[%lu] [WEBACT] Auto-shutdown triggered after %lu ms\n", millis(), serverUptime);
onGoBack();
return;
}
}
// Handle exit on Back button
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack();
@@ -329,7 +399,7 @@ void CrossPointWebServerActivity::loop() {
}
}
void CrossPointWebServerActivity::displayTaskLoop() {
void FileTransferActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@@ -341,14 +411,14 @@ void CrossPointWebServerActivity::displayTaskLoop() {
}
}
void CrossPointWebServerActivity::render() const {
void FileTransferActivity::render() const {
// Only render our own UI when server is running
// Subactivities handle their own rendering
if (state == WebServerActivityState::SERVER_RUNNING) {
if (state == FileTransferActivityState::SERVER_RUNNING) {
renderer.clearScreen();
renderServerRunning();
renderer.displayBuffer();
} else if (state == WebServerActivityState::AP_STARTING) {
} 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, BOLD);
@@ -378,12 +448,20 @@ void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std
}
}
void CrossPointWebServerActivity::renderServerRunning() const {
void FileTransferActivity::renderServerRunning() const {
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, BOLD);
if (selectedProtocol == FileTransferProtocol::HTTP) {
renderHttpServerRunning();
} else {
renderFtpServerRunning();
}
}
void FileTransferActivity::renderHttpServerRunning() const {
// Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, BOLD);
if (isApMode) {
// AP mode display - center the content block
int startY = 55;
@@ -445,3 +523,67 @@ void CrossPointWebServerActivity::renderServerRunning() const {
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
void FileTransferActivity::renderFtpServerRunning() const {
// Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines
if (isApMode) {
// AP mode display
int startY = 55;
renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, 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
std::string wifiConfig = std::string("WIFI:T:WPA;S:") + connectedSSID + ";P:" + "" + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
startY += 6 * 29 + 3 * LINE_SPACING;
// Show FTP server info
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, "FTP Server", true, BOLD);
std::string ftpInfo = "ftp://" + connectedIP + "/";
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 4, ftpInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Connect with FTP client:");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "Username: crosspoint");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 7, "Password: reader");
} 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 FTP server info
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, "FTP Server", true, BOLD);
std::string ftpInfo = "ftp://" + connectedIP + "/";
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, ftpInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Use FTP client to connect:");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Username: crosspoint");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "Password: reader");
// Show QR code for FTP URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 8, ftpInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 7, "or scan QR code with your phone:");
}
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <memory>
#include <string>
#include "NetworkModeSelectionActivity.h"
#include "ProtocolSelectionActivity.h"
#include "activities/ActivityWithSubactivity.h"
#include "network/CrossPointWebServer.h"
#include "network/CrossPointFtpServer.h"
// File transfer activity states
enum class FileTransferActivityState {
MODE_SELECTION, // Choosing between Join Network and Create Hotspot
PROTOCOL_SELECTION, // Choosing between HTTP and FTP
WIFI_SELECTION, // WiFi selection subactivity is active (for Join Network mode)
AP_STARTING, // Starting Access Point mode
SERVER_RUNNING, // File transfer server is running and handling requests
SHUTTING_DOWN // Shutting down server and WiFi
};
/**
* FileTransferActivity is the entry point for file transfer functionality.
* It:
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
* - For STA mode: Launches WifiSelectionActivity to connect to an existing network
* - For AP mode: Creates an Access Point that clients can connect to
* - Starts the file transfer server (HTTP or FTP) when connected
* - Handles client requests in its loop() function
* - Cleans up the server and shuts down WiFi on exit
*/
class FileTransferActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
FileTransferActivityState state = FileTransferActivityState::MODE_SELECTION;
const std::function<void()> onGoBack;
// Network mode
NetworkMode networkMode = NetworkMode::JOIN_NETWORK;
bool isApMode = false;
// Transfer protocol
FileTransferProtocol selectedProtocol = FileTransferProtocol::HTTP;
// File transfer servers - owned by this activity
std::unique_ptr<CrossPointWebServer> httpServer;
std::unique_ptr<CrossPointFtpServer> 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;
// Auto-shutdown tracking
unsigned long serverStartTime = 0;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void renderServerRunning() const;
void renderHttpServerRunning() const;
void renderFtpServerRunning() const;
void onNetworkModeSelected(NetworkMode mode);
void onProtocolSelected(FileTransferProtocol protocol);
void onWifiSelectionComplete(bool connected);
void startAccessPoint();
void startServer();
void stopHttpServer();
void stopFtpServer();
public:
explicit FileTransferActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity("FileTransfer", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool skipLoopDelay() override {
return (httpServer && httpServer->isRunning()) || (ftpServer && ftpServer->isRunning());
}
};

View File

@@ -0,0 +1,129 @@
#include "ProtocolSelectionActivity.h"
#include <GfxRenderer.h>
#include "MappedInputManager.h"
#include "fontIds.h"
namespace {
constexpr int MENU_ITEM_COUNT = 2;
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"HTTP (Web Browser)", "FTP (File Client)"};
const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {"Upload/download via web browser",
"Upload/download via FTP client"};
} // namespace
void ProtocolSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<ProtocolSelectionActivity*>(param);
self->displayTaskLoop();
}
void ProtocolSelectionActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Reset selection
selectedIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&ProtocolSelectionActivity::taskTrampoline, "ProtocolSelectTask",
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 back button - cancel
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onCancel();
return;
}
// 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", true, BOLD);
// Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, "Select transfer 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("« Back", "Select", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "../Activity.h"
// Enum for file transfer protocol selection
enum class FileTransferProtocol { HTTP, FTP };
/**
* ProtocolSelectionActivity presents the user with a choice:
* - "HTTP (Web Browser)" - Transfer files via web browser
* - "FTP (File Client)" - Transfer files via FTP client
*
* The onProtocolSelected callback is called with the user's choice.
* The onCancel callback is called if the user presses back.
*/
class ProtocolSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectedIndex = 0;
bool updateRequired = false;
const std::function<void(FileTransferProtocol)> onProtocolSelected;
const std::function<void()> onCancel;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public:
explicit ProtocolSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void(FileTransferProtocol)>& onProtocolSelected,
const std::function<void()>& onCancel)
: Activity("ProtocolSelection", renderer, mappedInput), onProtocolSelected(onProtocolSelected), onCancel(onCancel) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@@ -61,7 +61,7 @@ void WifiSelectionActivity::onExit() {
WiFi.scanDelete();
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)
// Note: We do NOT disconnect WiFi here - the parent activity (FileTransferActivity)
// manages WiFi connection state. We just clean up the scan and task.
// Acquire mutex before deleting task to ensure task isn't using it

View File

@@ -0,0 +1,182 @@
#include "ScheduleSettingsActivity.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "fontIds.h"
namespace {
constexpr int SETTINGS_COUNT = 6;
const char* SETTING_NAMES[SETTINGS_COUNT] = {
"Schedule Enabled",
"Frequency",
"Schedule Time",
"Auto-Shutdown",
"Protocol",
"Network Mode"
};
} // namespace
void ScheduleSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<ScheduleSettingsActivity*>(param);
self->displayTaskLoop();
}
void ScheduleSettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0;
updateRequired = true;
xTaskCreate(&ScheduleSettingsActivity::taskTrampoline, "ScheduleSettingsTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void ScheduleSettingsActivity::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 ScheduleSettingsActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
toggleCurrentSetting();
updateRequired = true;
return;
}
// Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex > 0) ? (selectedIndex - 1) : (SETTINGS_COUNT - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % SETTINGS_COUNT;
updateRequired = true;
}
}
void ScheduleSettingsActivity::toggleCurrentSetting() {
switch (selectedIndex) {
case 0: // Schedule Enabled
SETTINGS.scheduleEnabled = !SETTINGS.scheduleEnabled;
break;
case 1: // Frequency
SETTINGS.scheduleFrequency = (SETTINGS.scheduleFrequency + 1) % 7;
break;
case 2: // Schedule Time (hour)
SETTINGS.scheduleHour = (SETTINGS.scheduleHour + 1) % 24;
break;
case 3: // Auto-Shutdown
SETTINGS.scheduleAutoShutdown = (SETTINGS.scheduleAutoShutdown + 1) % 6;
break;
case 4: // Protocol
SETTINGS.scheduleProtocol = (SETTINGS.scheduleProtocol + 1) % 2;
break;
case 5: // Network Mode
SETTINGS.scheduleNetworkMode = (SETTINGS.scheduleNetworkMode + 1) % 2;
break;
}
SETTINGS.saveToFile();
}
void ScheduleSettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void ScheduleSettingsActivity::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Schedule Settings", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, 40, "Auto-start file transfer server");
// Draw selection
renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30);
// Draw settings
const char* frequencyNames[] = {"1 hour", "2 hours", "3 hours", "6 hours", "12 hours", "24 hours", "Scheduled"};
const char* shutdownNames[] = {"5 min", "10 min", "20 min", "30 min", "60 min", "120 min"};
const char* protocolNames[] = {"HTTP", "FTP"};
const char* networkModeNames[] = {"Join Network", "Create Hotspot"};
for (int i = 0; i < SETTINGS_COUNT; i++) {
const int settingY = 70 + i * 30;
const bool isSelected = (i == selectedIndex);
// Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, SETTING_NAMES[i], !isSelected);
// Draw value
std::string valueText;
switch (i) {
case 0: // Schedule Enabled
valueText = SETTINGS.scheduleEnabled ? "ON" : "OFF";
break;
case 1: // Frequency
valueText = frequencyNames[SETTINGS.scheduleFrequency];
break;
case 2: { // Schedule Time
char timeStr[6];
snprintf(timeStr, sizeof(timeStr), "%02d:00", SETTINGS.scheduleHour);
valueText = timeStr;
break;
}
case 3: // Auto-Shutdown
valueText = shutdownNames[SETTINGS.scheduleAutoShutdown];
break;
case 4: // Protocol
valueText = protocolNames[SETTINGS.scheduleProtocol];
break;
case 5: // Network Mode
valueText = networkModeNames[SETTINGS.scheduleNetworkMode];
break;
}
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(), !isSelected);
}
// Draw info text
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 100,
SETTINGS.scheduleFrequency == 6 ? "Server starts at scheduled time" : "Server starts at intervals");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 80,
"and auto-shuts down after timeout");
// Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "activities/Activity.h"
/**
* ScheduleSettingsActivity allows users to configure automatic file transfer server scheduling.
* Users can set up recurring schedules (hourly, daily) or specific times throughout the week.
*/
class ScheduleSettingsActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
int selectedIndex = 0; // Currently selected option
const std::function<void()> onGoBack;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void toggleCurrentSetting();
public:
explicit ScheduleSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoBack)
: Activity("ScheduleSettings", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@@ -6,11 +6,12 @@
#include "FolderPickerActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "ScheduleSettingsActivity.h"
#include "fontIds.h"
// Define the static settings list
namespace {
constexpr int settingsCount = 17;
constexpr int settingsCount = 18;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@@ -50,6 +51,7 @@ const SettingInfo settingsList[settingsCount] = {
{"Root", "Custom", "Last Used"}},
{"Choose Custom Folder", SettingType::ACTION, nullptr, {}},
{"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}},
{"File Transfer Schedule", SettingType::ACTION, nullptr, {}},
{"Check for updates", SettingType::ACTION, nullptr, {}},
};
} // namespace
@@ -167,6 +169,14 @@ void SettingsActivity::toggleCurrentSetting() {
},
"/")); // Start from root directory
xSemaphoreGive(renderingMutex);
} else if (std::string(setting.name) == "File Transfer Schedule") {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new ScheduleSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
} else {
// Only toggle if it's a toggle type and has a value pointer