mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 23:57:39 +03:00
Merge 138d897212 into 66811bf50b
This commit is contained in:
commit
92799ae0a1
@ -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
|
||||
|
||||
@ -14,7 +14,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
|
||||
|
||||
@ -42,10 +42,15 @@ 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);
|
||||
serialization::writePod(outputFile, hotspotSchedulerEnabled);
|
||||
serialization::writePod(outputFile, hotspotSchedulerHour);
|
||||
serialization::writePod(outputFile, hotspotSchedulerMinute);
|
||||
serialization::writePod(outputFile, hotspotSchedulerShutdownTime);
|
||||
serialization::writePod(outputFile, screenMargin);
|
||||
serialization::writePod(outputFile, sleepScreenCoverMode);
|
||||
serialization::writeString(outputFile, std::string(opdsServerUrl));
|
||||
serialization::writePod(outputFile, textAntiAliasing);
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
@ -98,9 +103,23 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, refreshFrequency);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, screenMargin);
|
||||
serialization::readString(inputFile, ftpUsername);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, sleepScreenCoverMode);
|
||||
serialization::readString(inputFile, ftpPassword);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readString(inputFile, httpUsername);
|
||||
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;
|
||||
{
|
||||
std::string urlStr;
|
||||
@ -226,3 +245,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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
class CrossPointSettings {
|
||||
private:
|
||||
@ -52,6 +53,16 @@ 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,10 +89,20 @@ class CrossPointSettings {
|
||||
uint8_t sleepTimeout = SLEEP_10_MIN;
|
||||
// E-ink refresh frequency (default 15 pages)
|
||||
uint8_t refreshFrequency = REFRESH_15;
|
||||
// Reader screen margin settings
|
||||
uint8_t screenMargin = 5;
|
||||
// OPDS browser settings
|
||||
char opdsServerUrl[128] = "";
|
||||
// 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";
|
||||
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;
|
||||
|
||||
@ -90,6 +111,7 @@ class CrossPointSettings {
|
||||
|
||||
uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 400; }
|
||||
int getReaderFontId() const;
|
||||
unsigned int getHotspotShutdownMinutes() const;
|
||||
|
||||
bool saveToFile() const;
|
||||
bool loadFromFile();
|
||||
|
||||
486
src/activities/network/FileTransferActivity.cpp
Normal file
486
src/activities/network/FileTransferActivity.cpp
Normal file
@ -0,0 +1,486 @@
|
||||
#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;
|
||||
};
|
||||
@ -16,31 +16,59 @@ namespace {
|
||||
constexpr int settingsCount = 18;
|
||||
const SettingInfo settingsList[settingsCount] = {
|
||||
// Should match with SLEEP_SCREEN_MODE
|
||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
|
||||
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
|
||||
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}),
|
||||
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
|
||||
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing),
|
||||
SettingInfo::Toggle("Short Power Button Click", &CrossPointSettings::shortPwrBtn),
|
||||
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
||||
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
|
||||
SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,
|
||||
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}),
|
||||
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
||||
{"Prev, Next", "Next, Prev"}),
|
||||
SettingInfo::Enum("Reader Font Family", &CrossPointSettings::fontFamily,
|
||||
{"Bookerly", "Noto Sans", "Open Dyslexic"}),
|
||||
SettingInfo::Enum("Reader Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
||||
SettingInfo::Enum("Reader Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
||||
SettingInfo::Value("Reader Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
||||
SettingInfo::Enum("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
||||
{"Justify", "Left", "Center", "Right"}),
|
||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
||||
SettingInfo::Action("Calibre Settings"),
|
||||
SettingInfo::Action("Check for updates")};
|
||||
{"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,
|
||||
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"}},
|
||||
{"FTP Username", SettingType::TEXT, nullptr, &CrossPointSettings::ftpUsername, {}},
|
||||
{"FTP Password", SettingType::TEXT, nullptr, &CrossPointSettings::ftpPassword, {}},
|
||||
{"Check for updates", SettingType::ACTION, nullptr, nullptr, {}},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void SettingsActivity::taskTrampoline(void* param) {
|
||||
@ -127,15 +155,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<uint8_t>(setting.enumValues.size());
|
||||
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
|
||||
// Decreasing would also be nice for large ranges I think but oh well can't have everything
|
||||
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||
// Wrap to minValue if exceeding setting value boundary
|
||||
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
|
||||
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
|
||||
} else {
|
||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||
}
|
||||
} 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 (strcmp(setting.name, "Calibre Settings") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
@ -202,8 +225,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::VALUE && settingsList[i].valuePtr != nullptr) {
|
||||
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
||||
} 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);
|
||||
|
||||
@ -11,13 +11,14 @@
|
||||
|
||||
class CrossPointSettings;
|
||||
|
||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
|
||||
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/VALUE)
|
||||
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<std::string> enumValues;
|
||||
|
||||
struct ValueRange {
|
||||
@ -30,17 +31,17 @@ 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<std::string> 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};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
#include "activities/boot_sleep/SleepActivity.h"
|
||||
#include "activities/browser/OpdsBookBrowserActivity.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"
|
||||
@ -217,7 +217,7 @@ void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
|
||||
|
||||
void onGoToFileTransfer() {
|
||||
exitActivity();
|
||||
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
||||
enterNewActivity(new FileTransferActivity(renderer, mappedInputManager, onGoHome));
|
||||
}
|
||||
|
||||
void onGoToSettings() {
|
||||
|
||||
63
src/network/CrossPointFtpServer.cpp
Normal file
63
src/network/CrossPointFtpServer.cpp
Normal file
@ -0,0 +1,63 @@
|
||||
#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