mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 23:57:39 +03:00
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:
parent
1d8815249d
commit
39f403ae84
@ -1,3 +1,13 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
crosspoint_version = 0.11.2
|
crosspoint_version = 0.11.2
|
||||||
default_envs = default
|
default_envs = default
|
||||||
@ -11,49 +21,41 @@ upload_speed = 921600
|
|||||||
check_tool = cppcheck
|
check_tool = cppcheck
|
||||||
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
|
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
|
||||||
check_skip_packages = yes
|
check_skip_packages = yes
|
||||||
|
|
||||||
board_upload.flash_size = 16MB
|
board_upload.flash_size = 16MB
|
||||||
board_upload.maximum_size = 16777216
|
board_upload.maximum_size = 16777216
|
||||||
board_upload.offset_address = 0x10000
|
board_upload.offset_address = 0x10000
|
||||||
|
build_flags =
|
||||||
build_flags =
|
-DARDUINO_USB_MODE=1
|
||||||
-DARDUINO_USB_MODE=1
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
|
||||||
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
|
-DDISABLE_FS_H_WARNING=1
|
||||||
-DDISABLE_FS_H_WARNING=1
|
-DXML_GE=0
|
||||||
# https://libexpat.github.io/doc/api/latest/#XML_GE
|
-DXML_CONTEXT_BYTES=1024
|
||||||
-DXML_GE=0
|
-std=c++2a
|
||||||
-DXML_CONTEXT_BYTES=1024
|
-DUSE_UTF8_LONG_NAMES=1
|
||||||
-std=c++2a
|
|
||||||
# Enable UTF-8 long file names in SdFat
|
|
||||||
-DUSE_UTF8_LONG_NAMES=1
|
|
||||||
|
|
||||||
; Board configuration
|
|
||||||
board_build.flash_mode = dio
|
board_build.flash_mode = dio
|
||||||
board_build.flash_size = 16MB
|
board_build.flash_size = 16MB
|
||||||
board_build.partitions = partitions.csv
|
board_build.partitions = partitions.csv
|
||||||
|
extra_scripts =
|
||||||
extra_scripts =
|
pre:scripts/build_html.py
|
||||||
pre:scripts/build_html.py
|
lib_deps =
|
||||||
|
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
||||||
; Libraries
|
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
||||||
lib_deps =
|
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
||||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
||||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
ArduinoJson @ 7.4.2
|
||||||
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
QRCode @ 0.0.1
|
||||||
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
xreef/SimpleFTPServer @ 3.0.1
|
||||||
ArduinoJson @ 7.4.2
|
|
||||||
QRCode @ 0.0.1
|
|
||||||
|
|
||||||
[env:default]
|
[env:default]
|
||||||
extends = base
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}-dev\"
|
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}-dev\"
|
||||||
|
|
||||||
[env:gh_release]
|
[env:gh_release]
|
||||||
extends = base
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}\"
|
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}\"
|
||||||
|
|||||||
@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// Increment this when adding new persisted settings fields
|
||||||
constexpr uint8_t SETTINGS_COUNT = 16;
|
constexpr uint8_t SETTINGS_COUNT = 22;
|
||||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -43,6 +43,12 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, refreshInterval);
|
serialization::writePod(outputFile, refreshInterval);
|
||||||
serialization::writePod(outputFile, defaultFolder);
|
serialization::writePod(outputFile, defaultFolder);
|
||||||
serialization::writeString(outputFile, customDefaultFolder);
|
serialization::writeString(outputFile, customDefaultFolder);
|
||||||
|
serialization::writePod(outputFile, scheduleEnabled);
|
||||||
|
serialization::writePod(outputFile, scheduleFrequency);
|
||||||
|
serialization::writePod(outputFile, scheduleProtocol);
|
||||||
|
serialization::writePod(outputFile, scheduleNetworkMode);
|
||||||
|
serialization::writePod(outputFile, scheduleHour);
|
||||||
|
serialization::writePod(outputFile, scheduleAutoShutdown);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||||
@ -101,6 +107,18 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
serialization::readString(inputFile, customDefaultFolder);
|
serialization::readString(inputFile, customDefaultFolder);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, scheduleEnabled);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, scheduleFrequency);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, scheduleProtocol);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, scheduleNetworkMode);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, scheduleHour);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, scheduleAutoShutdown);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
|
|||||||
@ -77,6 +77,14 @@ class CrossPointSettings {
|
|||||||
// Default folder for file browser (enum index: 0=Root, 1=Custom, 2=Last Used)
|
// Default folder for file browser (enum index: 0=Root, 1=Custom, 2=Last Used)
|
||||||
uint8_t defaultFolder = FOLDER_LAST_USED; // Default to last used (current behavior)
|
uint8_t defaultFolder = FOLDER_LAST_USED; // Default to last used (current behavior)
|
||||||
|
|
||||||
|
// Schedule settings for auto-starting file transfer server
|
||||||
|
uint8_t scheduleEnabled = 0; // 0=disabled, 1=enabled
|
||||||
|
uint8_t scheduleFrequency = 0; // 0=1hr, 1=2hr, 2=3hr, 3=6hr, 4=12hr, 5=24hr, 6=Scheduled time
|
||||||
|
uint8_t scheduleProtocol = 0; // 0=HTTP, 1=FTP
|
||||||
|
uint8_t scheduleNetworkMode = 0; // 0=Join Network, 1=Create Hotspot
|
||||||
|
uint8_t scheduleHour = 0; // 0-23: Hour of day for scheduled start (when scheduleFrequency=6)
|
||||||
|
uint8_t scheduleAutoShutdown = 2; // 0=5min, 1=10min, 2=20min, 3=30min, 4=60min, 5=120min
|
||||||
|
|
||||||
// Custom default folder path (used when defaultFolder == FOLDER_CUSTOM)
|
// Custom default folder path (used when defaultFolder == FOLDER_CUSTOM)
|
||||||
std::string customDefaultFolder = "/books";
|
std::string customDefaultFolder = "/books";
|
||||||
|
|
||||||
@ -115,6 +123,33 @@ class CrossPointSettings {
|
|||||||
return "/"; // Fallback
|
return "/"; // Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned long getScheduleIntervalMs() const {
|
||||||
|
// Map enum index to milliseconds: 0=1hr, 1=2hr, 2=3hr, 3=6hr, 4=12hr, 5=24hr, 6=Scheduled
|
||||||
|
constexpr unsigned long intervals[] = {
|
||||||
|
1UL * 60UL * 60UL * 1000UL, // 0: 1 hour
|
||||||
|
2UL * 60UL * 60UL * 1000UL, // 1: 2 hours
|
||||||
|
3UL * 60UL * 60UL * 1000UL, // 2: 3 hours
|
||||||
|
6UL * 60UL * 60UL * 1000UL, // 3: 6 hours
|
||||||
|
12UL * 60UL * 60UL * 1000UL, // 4: 12 hours
|
||||||
|
24UL * 60UL * 60UL * 1000UL, // 5: 24 hours
|
||||||
|
0UL // 6: Scheduled (use time-based check)
|
||||||
|
};
|
||||||
|
return (scheduleFrequency < 7) ? intervals[scheduleFrequency] : intervals[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long getAutoShutdownMs() const {
|
||||||
|
// Map enum index to milliseconds: 0=5min, 1=10min, 2=20min, 3=30min, 4=60min, 5=120min
|
||||||
|
constexpr unsigned long durations[] = {
|
||||||
|
5UL * 60UL * 1000UL, // 0: 5 minutes
|
||||||
|
10UL * 60UL * 1000UL, // 1: 10 minutes
|
||||||
|
20UL * 60UL * 1000UL, // 2: 20 minutes (default)
|
||||||
|
30UL * 60UL * 1000UL, // 3: 30 minutes
|
||||||
|
60UL * 60UL * 1000UL, // 4: 60 minutes
|
||||||
|
120UL * 60UL * 1000UL // 5: 120 minutes
|
||||||
|
};
|
||||||
|
return (scheduleAutoShutdown < 6) ? durations[scheduleAutoShutdown] : durations[2];
|
||||||
|
}
|
||||||
|
|
||||||
bool saveToFile() const;
|
bool saveToFile() const;
|
||||||
bool loadFromFile();
|
bool loadFromFile();
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t STATE_FILE_VERSION = 2;
|
constexpr uint8_t STATE_FILE_VERSION = 3;
|
||||||
constexpr char STATE_FILE[] = "/.crosspoint/state.bin";
|
constexpr char STATE_FILE[] = "/.crosspoint/state.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ bool CrossPointState::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, STATE_FILE_VERSION);
|
serialization::writePod(outputFile, STATE_FILE_VERSION);
|
||||||
serialization::writeString(outputFile, openEpubPath);
|
serialization::writeString(outputFile, openEpubPath);
|
||||||
serialization::writeString(outputFile, lastBrowsedFolder);
|
serialization::writeString(outputFile, lastBrowsedFolder);
|
||||||
|
serialization::writePod(outputFile, lastScheduledServerTime);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -35,11 +36,17 @@ bool CrossPointState::loadFromFile() {
|
|||||||
if (version == 1) {
|
if (version == 1) {
|
||||||
// Version 1: only had openEpubPath
|
// Version 1: only had openEpubPath
|
||||||
serialization::readString(inputFile, openEpubPath);
|
serialization::readString(inputFile, openEpubPath);
|
||||||
lastBrowsedFolder = "/"; // Default for old version
|
lastScheduledServerTime = 0;
|
||||||
} else if (version == STATE_FILE_VERSION) {
|
} else if (version == 2) {
|
||||||
// Version 2: has openEpubPath and lastBrowsedFolder
|
// Version 2: has openEpubPath and lastBrowsedFolder
|
||||||
serialization::readString(inputFile, openEpubPath);
|
serialization::readString(inputFile, openEpubPath);
|
||||||
serialization::readString(inputFile, lastBrowsedFolder);
|
serialization::readString(inputFile, lastBrowsedFolder);
|
||||||
|
lastScheduledServerTime = 0;
|
||||||
|
} else if (version == STATE_FILE_VERSION) {
|
||||||
|
// Version 3: has openEpubPath, lastBrowsedFolder, and lastScheduledServerTime
|
||||||
|
serialization::readString(inputFile, openEpubPath);
|
||||||
|
serialization::readString(inputFile, lastBrowsedFolder);
|
||||||
|
serialization::readPod(inputFile, lastScheduledServerTime);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
|
|||||||
@ -9,6 +9,7 @@ class CrossPointState {
|
|||||||
public:
|
public:
|
||||||
std::string openEpubPath;
|
std::string openEpubPath;
|
||||||
std::string lastBrowsedFolder = "/";
|
std::string lastBrowsedFolder = "/";
|
||||||
|
unsigned long lastScheduledServerTime = 0; // Timestamp when scheduled server was last started
|
||||||
~CrossPointState() = default;
|
~CrossPointState() = default;
|
||||||
|
|
||||||
// Get singleton instance
|
// Get singleton instance
|
||||||
|
|||||||
@ -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(); }
|
|
||||||
};
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
#include "CrossPointWebServerActivity.h"
|
#include "FileTransferActivity.h"
|
||||||
|
|
||||||
#include <DNSServer.h>
|
#include <DNSServer.h>
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
@ -29,12 +29,12 @@ DNSServer* dnsServer = nullptr;
|
|||||||
constexpr uint16_t DNS_PORT = 53;
|
constexpr uint16_t DNS_PORT = 53;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
void FileTransferActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
auto* self = static_cast<FileTransferActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onEnter() {
|
void FileTransferActivity::onEnter() {
|
||||||
ActivityWithSubactivity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
@ -42,7 +42,7 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
state = WebServerActivityState::MODE_SELECTION;
|
state = FileTransferActivityState::MODE_SELECTION;
|
||||||
networkMode = NetworkMode::JOIN_NETWORK;
|
networkMode = NetworkMode::JOIN_NETWORK;
|
||||||
isApMode = false;
|
isApMode = false;
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
@ -50,7 +50,7 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
lastHandleClientTime = 0;
|
lastHandleClientTime = 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
|
xTaskCreate(&FileTransferActivity::taskTrampoline, "WebServerActivityTask",
|
||||||
2048, // Stack size
|
2048, // Stack size
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
@ -65,15 +65,16 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onExit() {
|
void FileTransferActivity::onExit() {
|
||||||
ActivityWithSubactivity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
|
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)
|
// Stop the file transfer servers first (before disconnecting WiFi)
|
||||||
stopWebServer();
|
stopHttpServer();
|
||||||
|
stopFtpServer();
|
||||||
|
|
||||||
// Stop mDNS
|
// Stop mDNS
|
||||||
MDNS.end();
|
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());
|
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(),
|
Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(),
|
||||||
mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot");
|
mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot");
|
||||||
|
|
||||||
@ -146,24 +147,41 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
// Exit mode selection subactivity
|
// Exit mode selection subactivity
|
||||||
exitActivity();
|
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
|
// STA mode - launch WiFi selection
|
||||||
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
state = WebServerActivityState::WIFI_SELECTION;
|
state = FileTransferActivityState::WIFI_SELECTION;
|
||||||
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
} else {
|
} else {
|
||||||
// AP mode - start access point
|
// AP mode - start access point
|
||||||
state = WebServerActivityState::AP_STARTING;
|
state = FileTransferActivityState::AP_STARTING;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
startAccessPoint();
|
startAccessPoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
void FileTransferActivity::onWifiSelectionComplete(const bool connected) {
|
||||||
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
||||||
|
|
||||||
if (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);
|
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the web server
|
// Start the file transfer server
|
||||||
startWebServer();
|
startServer();
|
||||||
} else {
|
} else {
|
||||||
// User cancelled - go back to mode selection
|
// User cancelled - go back to mode selection
|
||||||
exitActivity();
|
exitActivity();
|
||||||
state = WebServerActivityState::MODE_SELECTION;
|
state = FileTransferActivityState::MODE_SELECTION;
|
||||||
enterNewActivity(new NetworkModeSelectionActivity(
|
enterNewActivity(new NetworkModeSelectionActivity(
|
||||||
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||||
[this]() { onGoBack(); }));
|
[this]() { onGoBack(); }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::startAccessPoint() {
|
void FileTransferActivity::startAccessPoint() {
|
||||||
Serial.printf("[%lu] [WEBACT] Starting Access Point mode...\n", millis());
|
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());
|
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());
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Start the web server
|
// Start the file transfer server
|
||||||
startWebServer();
|
startServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::startWebServer() {
|
void FileTransferActivity::startServer() {
|
||||||
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
if (selectedProtocol == FileTransferProtocol::HTTP) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Starting HTTP server...\n", millis());
|
||||||
|
|
||||||
// Create the web server instance
|
// Create the HTTP server instance
|
||||||
webServer.reset(new CrossPointWebServer());
|
httpServer.reset(new CrossPointWebServer());
|
||||||
webServer->begin();
|
httpServer->begin();
|
||||||
|
|
||||||
if (webServer->isRunning()) {
|
if (httpServer->isRunning()) {
|
||||||
state = WebServerActivityState::SERVER_RUNNING;
|
state = FileTransferActivityState::SERVER_RUNNING;
|
||||||
Serial.printf("[%lu] [WEBACT] Web server started successfully\n", millis());
|
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
|
// 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.
|
// that had its own rendering task. We need to make sure our display is shown.
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
render();
|
render();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
Serial.printf("[%lu] [WEBACT] Rendered File Transfer screen\n", millis());
|
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 {
|
} else {
|
||||||
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start web server!\n", millis());
|
Serial.printf("[%lu] [WEBACT] Starting FTP server...\n", millis());
|
||||||
webServer.reset();
|
|
||||||
// Go back on error
|
// Create the FTP server instance
|
||||||
onGoBack();
|
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() {
|
void FileTransferActivity::stopHttpServer() {
|
||||||
if (webServer && webServer->isRunning()) {
|
if (httpServer && httpServer->isRunning()) {
|
||||||
Serial.printf("[%lu] [WEBACT] Stopping web server...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Stopping HTTP server...\n", millis());
|
||||||
webServer->stop();
|
httpServer->stop();
|
||||||
Serial.printf("[%lu] [WEBACT] Web server stopped\n", millis());
|
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) {
|
if (subActivity) {
|
||||||
// Forward loop to subactivity
|
// Forward loop to subactivity
|
||||||
subActivity->loop();
|
subActivity->loop();
|
||||||
@ -289,15 +340,18 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle different states
|
// Handle different states
|
||||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
if (state == FileTransferActivityState::SERVER_RUNNING) {
|
||||||
// Handle DNS requests for captive portal (AP mode only)
|
// Handle DNS requests for captive portal (AP mode only)
|
||||||
if (isApMode && dnsServer) {
|
if (isApMode && dnsServer) {
|
||||||
dnsServer->processNextRequest();
|
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
|
// 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;
|
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
|
||||||
|
|
||||||
// Log if there's a significant gap between handleClient calls (>100ms)
|
// 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
|
// 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
|
// in chunks and each handleClient() call processes incoming data
|
||||||
// Reduced from 10 to 3 to prevent watchdog timer issues
|
// Reduced from 10 to 3 to prevent watchdog timer issues
|
||||||
constexpr int HANDLE_CLIENT_ITERATIONS = 3;
|
constexpr int HANDLE_CLIENT_ITERATIONS = 3;
|
||||||
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
|
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS; i++) {
|
||||||
webServer->handleClient();
|
if (httpRunning && httpServer->isRunning()) {
|
||||||
|
httpServer->handleClient();
|
||||||
|
} else if (ftpRunning && ftpServer->isRunning()) {
|
||||||
|
ftpServer->handleClient();
|
||||||
|
}
|
||||||
// Feed the watchdog timer between iterations to prevent resets
|
// Feed the watchdog timer between iterations to prevent resets
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
// Yield to other tasks to prevent starvation
|
// Yield to other tasks to prevent starvation
|
||||||
@ -321,6 +379,18 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
lastHandleClientTime = millis();
|
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
|
// Handle exit on Back button
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
@ -329,7 +399,7 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::displayTaskLoop() {
|
void FileTransferActivity::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
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
|
// Only render our own UI when server is running
|
||||||
// Subactivities handle their own rendering
|
// Subactivities handle their own rendering
|
||||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
if (state == FileTransferActivityState::SERVER_RUNNING) {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderServerRunning();
|
renderServerRunning();
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
} else if (state == WebServerActivityState::AP_STARTING) {
|
} else if (state == FileTransferActivityState::AP_STARTING) {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, BOLD);
|
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
|
// Use consistent line spacing
|
||||||
constexpr int LINE_SPACING = 28; // Space between lines
|
constexpr int LINE_SPACING = 28; // Space between lines
|
||||||
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, BOLD);
|
|
||||||
|
|
||||||
if (isApMode) {
|
if (isApMode) {
|
||||||
// AP mode display - center the content block
|
// AP mode display - center the content block
|
||||||
int startY = 55;
|
int startY = 55;
|
||||||
@ -445,3 +523,67 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
|||||||
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
|
const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
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);
|
||||||
|
}
|
||||||
89
src/activities/network/FileTransferActivity.h
Normal file
89
src/activities/network/FileTransferActivity.h
Normal 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());
|
||||||
|
}
|
||||||
|
};
|
||||||
129
src/activities/network/ProtocolSelectionActivity.cpp
Normal file
129
src/activities/network/ProtocolSelectionActivity.cpp
Normal 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();
|
||||||
|
}
|
||||||
41
src/activities/network/ProtocolSelectionActivity.h
Normal file
41
src/activities/network/ProtocolSelectionActivity.h
Normal 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;
|
||||||
|
};
|
||||||
@ -61,7 +61,7 @@ void WifiSelectionActivity::onExit() {
|
|||||||
WiFi.scanDelete();
|
WiFi.scanDelete();
|
||||||
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity)
|
// Note: We do NOT disconnect WiFi here - the parent activity (FileTransferActivity)
|
||||||
// manages WiFi connection state. We just clean up the scan and task.
|
// manages WiFi connection state. We just clean up the scan and task.
|
||||||
|
|
||||||
// Acquire mutex before deleting task to ensure task isn't using it
|
// Acquire mutex before deleting task to ensure task isn't using it
|
||||||
|
|||||||
182
src/activities/settings/ScheduleSettingsActivity.cpp
Normal file
182
src/activities/settings/ScheduleSettingsActivity.cpp
Normal 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();
|
||||||
|
}
|
||||||
33
src/activities/settings/ScheduleSettingsActivity.h
Normal file
33
src/activities/settings/ScheduleSettingsActivity.h
Normal 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;
|
||||||
|
};
|
||||||
@ -6,11 +6,12 @@
|
|||||||
#include "FolderPickerActivity.h"
|
#include "FolderPickerActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
|
#include "ScheduleSettingsActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int settingsCount = 17;
|
constexpr int settingsCount = 18;
|
||||||
const SettingInfo settingsList[settingsCount] = {
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||||
@ -50,6 +51,7 @@ const SettingInfo settingsList[settingsCount] = {
|
|||||||
{"Root", "Custom", "Last Used"}},
|
{"Root", "Custom", "Last Used"}},
|
||||||
{"Choose Custom Folder", SettingType::ACTION, nullptr, {}},
|
{"Choose Custom Folder", SettingType::ACTION, nullptr, {}},
|
||||||
{"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}},
|
{"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}},
|
||||||
|
{"File Transfer Schedule", SettingType::ACTION, nullptr, {}},
|
||||||
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -167,6 +169,14 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
},
|
},
|
||||||
"/")); // Start from root directory
|
"/")); // Start from root directory
|
||||||
xSemaphoreGive(renderingMutex);
|
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 {
|
} else {
|
||||||
// Only toggle if it's a toggle type and has a value pointer
|
// Only toggle if it's a toggle type and has a value pointer
|
||||||
|
|||||||
64
src/main.cpp
64
src/main.cpp
@ -16,7 +16,7 @@
|
|||||||
#include "activities/boot_sleep/BootActivity.h"
|
#include "activities/boot_sleep/BootActivity.h"
|
||||||
#include "activities/boot_sleep/SleepActivity.h"
|
#include "activities/boot_sleep/SleepActivity.h"
|
||||||
#include "activities/home/HomeActivity.h"
|
#include "activities/home/HomeActivity.h"
|
||||||
#include "activities/network/CrossPointWebServerActivity.h"
|
#include "activities/network/FileTransferActivity.h"
|
||||||
#include "activities/reader/ReaderActivity.h"
|
#include "activities/reader/ReaderActivity.h"
|
||||||
#include "activities/settings/SettingsActivity.h"
|
#include "activities/settings/SettingsActivity.h"
|
||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
@ -220,7 +220,63 @@ void onGoToFileTransfer() {
|
|||||||
enterNewActivity(new BleFileTransferActivity(renderer, mappedInputManager, onGoHome));
|
enterNewActivity(new BleFileTransferActivity(renderer, mappedInputManager, onGoHome));
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [ ] Starting WiFi file transfer (Bluetooth disabled)\n", millis());
|
Serial.printf("[%lu] [ ] Starting WiFi file transfer (Bluetooth disabled)\n", millis());
|
||||||
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
enterNewActivity(new FileTransferActivity(renderer, mappedInputManager, onGoHome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if scheduled file transfer should be triggered
|
||||||
|
void checkScheduledFileTransfer() {
|
||||||
|
// Only check if scheduling is enabled
|
||||||
|
if (!SETTINGS.scheduleEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't start if Bluetooth is enabled (conflicts with WiFi)
|
||||||
|
if (SETTINGS.bluetoothEnabled) {
|
||||||
|
Serial.printf("[%lu] [SCH] Scheduled file transfer skipped - Bluetooth enabled\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned long currentTime = millis();
|
||||||
|
bool shouldStart = false;
|
||||||
|
|
||||||
|
if (SETTINGS.scheduleFrequency == 6) {
|
||||||
|
// Scheduled time mode - check if current hour matches scheduled hour
|
||||||
|
// Note: This is a simple check. For a real-time clock, you'd need an RTC module.
|
||||||
|
// Here we approximate based on uptime and assume device wakes at specific times.
|
||||||
|
// This is a placeholder - for production, integrate with an RTC or NTP time sync.
|
||||||
|
|
||||||
|
// For now, we trigger on first wake if not already triggered today
|
||||||
|
// A proper implementation would need actual time tracking
|
||||||
|
const unsigned long timeSinceLastServer = currentTime - APP_STATE.lastScheduledServerTime;
|
||||||
|
const unsigned long oneDay = 24UL * 60UL * 60UL * 1000UL;
|
||||||
|
|
||||||
|
if (APP_STATE.lastScheduledServerTime == 0 || timeSinceLastServer >= oneDay) {
|
||||||
|
shouldStart = true;
|
||||||
|
Serial.printf("[%lu] [SCH] Scheduled time mode - triggering (configured for %02d:00)\n",
|
||||||
|
millis(), SETTINGS.scheduleHour);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Interval mode
|
||||||
|
const unsigned long scheduleInterval = SETTINGS.getScheduleIntervalMs();
|
||||||
|
const unsigned long timeSinceLastServer = currentTime - APP_STATE.lastScheduledServerTime;
|
||||||
|
|
||||||
|
// Check if it's time to start the server
|
||||||
|
// On first boot, lastScheduledServerTime will be 0, so we check if it's been at least the interval
|
||||||
|
if (APP_STATE.lastScheduledServerTime == 0 || timeSinceLastServer >= scheduleInterval) {
|
||||||
|
shouldStart = true;
|
||||||
|
Serial.printf("[%lu] [SCH] Interval mode - triggering (interval: %lu ms, last: %lu ms ago)\n",
|
||||||
|
millis(), scheduleInterval, timeSinceLastServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldStart) {
|
||||||
|
// Update the last scheduled server time
|
||||||
|
APP_STATE.lastScheduledServerTime = currentTime;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
|
|
||||||
|
// Start the file transfer activity
|
||||||
|
onGoToFileTransfer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,6 +352,10 @@ void setup() {
|
|||||||
enterNewActivity(new BootActivity(renderer, mappedInputManager));
|
enterNewActivity(new BootActivity(renderer, mappedInputManager));
|
||||||
|
|
||||||
APP_STATE.loadFromFile();
|
APP_STATE.loadFromFile();
|
||||||
|
|
||||||
|
// Check if scheduled file transfer should be triggered
|
||||||
|
checkScheduledFileTransfer();
|
||||||
|
|
||||||
if (APP_STATE.openEpubPath.empty()) {
|
if (APP_STATE.openEpubPath.empty()) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
111
src/network/CrossPointFtpServer.cpp
Normal file
111
src/network/CrossPointFtpServer.cpp
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
#include "CrossPointFtpServer.h"
|
||||||
|
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// FTP server credentials
|
||||||
|
constexpr const char* FTP_USERNAME = "crosspoint";
|
||||||
|
constexpr const char* FTP_PASSWORD = "reader";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
CrossPointFtpServer::CrossPointFtpServer() {}
|
||||||
|
|
||||||
|
CrossPointFtpServer::~CrossPointFtpServer() { stop(); }
|
||||||
|
|
||||||
|
void CrossPointFtpServer::begin() {
|
||||||
|
if (running) {
|
||||||
|
Serial.printf("[%lu] [FTP] FTP server already running\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a valid network connection (either STA connected or AP mode)
|
||||||
|
const wifi_mode_t wifiMode = WiFi.getMode();
|
||||||
|
const bool isStaConnected = (wifiMode & WIFI_MODE_STA) && (WiFi.status() == WL_CONNECTED);
|
||||||
|
const bool isInApMode = (wifiMode & WIFI_MODE_AP) && (WiFi.softAPgetStationNum() >= 0); // AP is running
|
||||||
|
|
||||||
|
if (!isStaConnected && !isInApMode) {
|
||||||
|
Serial.printf("[%lu] [FTP] Cannot start FTP server - no valid network (mode=%d, status=%d)\n", millis(), wifiMode,
|
||||||
|
WiFi.status());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store AP mode flag for later use
|
||||||
|
apMode = isInApMode;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [FTP] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
Serial.printf("[%lu] [FTP] Network mode: %s\n", millis(), apMode ? "AP" : "STA");
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [FTP] Creating FTP server on port 21...\n", millis());
|
||||||
|
|
||||||
|
// Create FTP server instance
|
||||||
|
ftpServer.reset(new ::FtpServer());
|
||||||
|
|
||||||
|
// Disable WiFi sleep to improve responsiveness and prevent 'unreachable' errors.
|
||||||
|
// This is critical for reliable FTP server operation on ESP32.
|
||||||
|
WiFi.setSleep(false);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [FTP] [MEM] Free heap after FTPServer allocation: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
|
if (!ftpServer) {
|
||||||
|
Serial.printf("[%lu] [FTP] Failed to create FTPServer!\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize FTP server with credentials
|
||||||
|
ftpServer->begin(FTP_USERNAME, FTP_PASSWORD);
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [FTP] FTP server started on port 21\n", millis());
|
||||||
|
// Show the correct IP based on network mode
|
||||||
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
|
Serial.printf("[%lu] [FTP] Access at ftp://%s/\n", millis(), ipAddr.c_str());
|
||||||
|
Serial.printf("[%lu] [FTP] Username: %s\n", millis(), FTP_USERNAME);
|
||||||
|
Serial.printf("[%lu] [FTP] Password: %s\n", millis(), FTP_PASSWORD);
|
||||||
|
Serial.printf("[%lu] [FTP] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointFtpServer::stop() {
|
||||||
|
if (!running || !ftpServer) {
|
||||||
|
Serial.printf("[%lu] [FTP] stop() called but already stopped (running=%d, ftpServer=%p)\n", millis(), running,
|
||||||
|
ftpServer.get());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [FTP] STOP INITIATED - setting running=false first\n", millis());
|
||||||
|
running = false; // Set this FIRST to prevent handleClient from using server
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [FTP] [MEM] Free heap before stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
|
// Add delay to allow any in-flight handleClient() calls to complete
|
||||||
|
delay(100);
|
||||||
|
Serial.printf("[%lu] [FTP] Waited 100ms for handleClient to finish\n", millis());
|
||||||
|
|
||||||
|
// SimpleFTPServer doesn't have explicit stop method, just delete
|
||||||
|
ftpServer.reset();
|
||||||
|
Serial.printf("[%lu] [FTP] FTP server stopped and deleted\n", millis());
|
||||||
|
Serial.printf("[%lu] [FTP] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
Serial.printf("[%lu] [FTP] [MEM] Free heap final: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointFtpServer::handleClient() const {
|
||||||
|
static unsigned long lastDebugPrint = 0;
|
||||||
|
|
||||||
|
// Check running flag FIRST before accessing server
|
||||||
|
if (!running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check server pointer is valid
|
||||||
|
if (!ftpServer) {
|
||||||
|
Serial.printf("[%lu] [FTP] WARNING: handleClient called with null server!\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print debug every 10 seconds to confirm handleClient is being called
|
||||||
|
if (millis() - lastDebugPrint > 10000) {
|
||||||
|
Serial.printf("[%lu] [FTP] handleClient active, server running on port 21\n", millis());
|
||||||
|
lastDebugPrint = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
ftpServer->handleFTP();
|
||||||
|
}
|
||||||
36
src/network/CrossPointFtpServer.h
Normal file
36
src/network/CrossPointFtpServer.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Must include SDCardManager before SimpleFTPServer to get SdFat
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
// Configure SimpleFTPServer to use SdFat 2.x BEFORE including the library
|
||||||
|
#define STORAGE_TYPE_SDFAT2
|
||||||
|
#include <SimpleFTPServer.h>
|
||||||
|
|
||||||
|
class CrossPointFtpServer {
|
||||||
|
public:
|
||||||
|
CrossPointFtpServer();
|
||||||
|
~CrossPointFtpServer();
|
||||||
|
|
||||||
|
// Start the FTP server (call after WiFi is connected)
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// Stop the FTP server
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
// Call this periodically to handle client requests
|
||||||
|
void handleClient() const;
|
||||||
|
|
||||||
|
// Check if server is running
|
||||||
|
bool isRunning() const { return running; }
|
||||||
|
|
||||||
|
// Get the port number
|
||||||
|
uint16_t getPort() const { return 21; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<::FtpServer> ftpServer;
|
||||||
|
bool running = false;
|
||||||
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user