mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 07:37:37 +03:00
Add FTP server support with protocol selection and QR codes
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 <noreply@anthropic.com>
This commit is contained in:
parent
14972b34cb
commit
5bf721ae42
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
|
||||
487
src/activities/network/FileTransferActivity.cpp
Normal file
487
src/activities/network/FileTransferActivity.cpp
Normal file
@ -0,0 +1,487 @@
|
||||
#include "FileTransferActivity.h"
|
||||
|
||||
#include <DNSServer.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <WiFi.h>
|
||||
#include <qrcode.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#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<FileTransferActivity*>(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<WifiSelectionActivity*>(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);
|
||||
}
|
||||
78
src/activities/network/FileTransferActivity.h
Normal file
78
src/activities/network/FileTransferActivity.h
Normal file
@ -0,0 +1,78 @@
|
||||
#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/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<void()> 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<CrossPointWebServer> webServer;
|
||||
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;
|
||||
|
||||
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<void()>& 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());
|
||||
}
|
||||
};
|
||||
123
src/activities/network/ProtocolSelectionActivity.cpp
Normal file
123
src/activities/network/ProtocolSelectionActivity.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
#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 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<ProtocolSelectionActivity*>(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();
|
||||
}
|
||||
30
src/activities/network/ProtocolSelectionActivity.h
Normal file
30
src/activities/network/ProtocolSelectionActivity.h
Normal file
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#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<void(FileTransferProtocol)> onProtocolSelected;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
public:
|
||||
explicit ProtocolSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(FileTransferProtocol)>& onProtocolSelected)
|
||||
: Activity("ProtocolSelection", renderer, mappedInput), onProtocolSelected(onProtocolSelected) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -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() {
|
||||
|
||||
65
src/network/CrossPointFtpServer.cpp
Normal file
65
src/network/CrossPointFtpServer.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include "CrossPointFtpServer.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
#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();
|
||||
}
|
||||
}
|
||||
28
src/network/CrossPointFtpServer.h
Normal file
28
src/network/CrossPointFtpServer.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
// Configure SimpleFTPServer to use SdFat2 instead of SD library
|
||||
#define DEFAULT_STORAGE_TYPE_ESP32 2 // STORAGE_SDFAT2
|
||||
|
||||
#include <SimpleFTPServer.h>
|
||||
|
||||
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; }
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user