mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 08:07:40 +03:00
Merge 7136220b55 into d4ae108d9b
This commit is contained in:
commit
f965292d04
@ -28,6 +28,25 @@ build_flags =
|
|||||||
-std=c++2a
|
-std=c++2a
|
||||||
# Enable UTF-8 long file names in SdFat
|
# Enable UTF-8 long file names in SdFat
|
||||||
-DUSE_UTF8_LONG_NAMES=1
|
-DUSE_UTF8_LONG_NAMES=1
|
||||||
|
# LWIP TCP/IP stack optimizations for WiFi file transfer performance
|
||||||
|
# These settings optimize buffer sizes and TCP parameters for maximum throughput
|
||||||
|
-DCONFIG_LWIP_MAX_SOCKETS=10
|
||||||
|
-DCONFIG_LWIP_TCP_MSS=1436
|
||||||
|
-DCONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744
|
||||||
|
-DCONFIG_LWIP_TCP_WND_DEFAULT=5744
|
||||||
|
-DCONFIG_LWIP_TCP_RECVMBOX_SIZE=12
|
||||||
|
-DCONFIG_LWIP_UDP_RECVMBOX_SIZE=12
|
||||||
|
-DCONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32
|
||||||
|
-DCONFIG_LWIP_TCP_RTO_TIME=3000
|
||||||
|
# WiFi performance optimizations
|
||||||
|
-DCONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=16
|
||||||
|
-DCONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
|
||||||
|
-DCONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1
|
||||||
|
-DCONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
|
||||||
|
# TCP optimizations for file uploads
|
||||||
|
-DCONFIG_LWIP_TCP_OVERSIZE=1
|
||||||
|
-DCONFIG_LWIP_WND_SCALE=1
|
||||||
|
-DCONFIG_LWIP_TCP_RCV_SCALE=2
|
||||||
|
|
||||||
; Board configuration
|
; Board configuration
|
||||||
board_build.flash_mode = dio
|
board_build.flash_mode = dio
|
||||||
|
|||||||
@ -48,7 +48,7 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
|
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
|
||||||
2048, // Stack size
|
6144, // Stack size (increased from 2KB to 6KB for stability)
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
@ -147,6 +147,11 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
// AP mode - start access point
|
// AP mode - start access point
|
||||||
state = WebServerActivityState::AP_STARTING;
|
state = WebServerActivityState::AP_STARTING;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
|
// WiFi performance optimizations for AP mode
|
||||||
|
WiFi.setSleep(false); // Disable WiFi sleep
|
||||||
|
WiFi.setTxPower(WIFI_POWER_19_5dBm); // Maximum TX power for ESP32-C3
|
||||||
|
|
||||||
startAccessPoint();
|
startAccessPoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,6 +192,12 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
|||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
delay(100);
|
delay(100);
|
||||||
|
|
||||||
|
// WiFi performance optimizations for maximum throughput
|
||||||
|
WiFi.setSleep(false); // Disable WiFi sleep
|
||||||
|
WiFi.setTxPower(WIFI_POWER_19_5dBm); // Maximum TX power for ESP32-C3
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEBACT] WiFi optimizations applied (sleep disabled, max TX power)\n", millis());
|
||||||
|
|
||||||
// Start soft AP
|
// Start soft AP
|
||||||
bool apStarted;
|
bool apStarted;
|
||||||
if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) {
|
if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) {
|
||||||
@ -300,6 +311,15 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
constexpr int HANDLE_CLIENT_ITERATIONS = 10;
|
constexpr int HANDLE_CLIENT_ITERATIONS = 10;
|
||||||
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
|
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
|
||||||
webServer->handleClient();
|
webServer->handleClient();
|
||||||
|
|
||||||
|
// CRITICAL: Yield to WiFi stack and other tasks between iterations
|
||||||
|
// This prevents WiFi stack starvation in STA mode and improves stability
|
||||||
|
yield();
|
||||||
|
|
||||||
|
// Add small delay every few iterations to reduce CPU pressure
|
||||||
|
if (i % 3 == 2) {
|
||||||
|
delay(1); // 1ms delay every 3 iterations
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastHandleClientTime = millis();
|
lastHandleClientTime = millis();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ CrossPointWebServer::CrossPointWebServer() {}
|
|||||||
CrossPointWebServer::~CrossPointWebServer() { stop(); }
|
CrossPointWebServer::~CrossPointWebServer() { stop(); }
|
||||||
|
|
||||||
void CrossPointWebServer::begin() {
|
void CrossPointWebServer::begin() {
|
||||||
if (running) {
|
if (running.load(std::memory_order_acquire)) {
|
||||||
Serial.printf("[%lu] [WEB] Web server already running\n", millis());
|
Serial.printf("[%lu] [WEB] Web server already running\n", millis());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -51,13 +51,13 @@ void CrossPointWebServer::begin() {
|
|||||||
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
||||||
server.reset(new WebServer(port));
|
server.reset(new WebServer(port));
|
||||||
|
|
||||||
// Disable WiFi sleep to improve responsiveness and prevent 'unreachable' errors.
|
// WiFi performance optimizations for maximum throughput
|
||||||
// This is critical for reliable web server operation on ESP32.
|
WiFi.setSleep(false); // Disable WiFi sleep to improve responsiveness
|
||||||
WiFi.setSleep(false);
|
|
||||||
|
|
||||||
// Note: WebServer class doesn't have setNoDelay() in the standard ESP32 library.
|
// Set WiFi TX power to maximum for best signal and throughput
|
||||||
// We rely on disabling WiFi sleep for responsiveness.
|
WiFi.setTxPower(WIFI_POWER_19_5dBm); // Maximum power for ESP32-C3
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEB] WiFi optimizations applied (sleep disabled, max TX power)\n", millis());
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after WebServer allocation: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after WebServer allocation: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
@ -86,7 +86,7 @@ void CrossPointWebServer::begin() {
|
|||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
server->begin();
|
server->begin();
|
||||||
running = true;
|
running.store(true, std::memory_order_release);
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
||||||
// Show the correct IP based on network mode
|
// Show the correct IP based on network mode
|
||||||
@ -96,14 +96,15 @@ void CrossPointWebServer::begin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::stop() {
|
void CrossPointWebServer::stop() {
|
||||||
if (!running || !server) {
|
const bool wasRunning = running.load(std::memory_order_acquire);
|
||||||
Serial.printf("[%lu] [WEB] stop() called but already stopped (running=%d, server=%p)\n", millis(), running,
|
if (!wasRunning || !server) {
|
||||||
|
Serial.printf("[%lu] [WEB] stop() called but already stopped (running=%d, server=%p)\n", millis(), wasRunning,
|
||||||
server.get());
|
server.get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] STOP INITIATED - setting running=false first\n", millis());
|
Serial.printf("[%lu] [WEB] STOP INITIATED - setting running=false first\n", millis());
|
||||||
running = false; // Set this FIRST to prevent handleClient from using server
|
running.store(false, std::memory_order_release);
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap before stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap before stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
@ -111,6 +112,10 @@ void CrossPointWebServer::stop() {
|
|||||||
delay(100);
|
delay(100);
|
||||||
Serial.printf("[%lu] [WEB] Waited 100ms for handleClient to finish\n", millis());
|
Serial.printf("[%lu] [WEB] Waited 100ms for handleClient to finish\n", millis());
|
||||||
|
|
||||||
|
// Lock mutex to ensure no handleClient() is currently accessing server
|
||||||
|
std::lock_guard<std::mutex> lock(serverMutex);
|
||||||
|
|
||||||
|
if (server) {
|
||||||
server->stop();
|
server->stop();
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after server->stop(): %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after server->stop(): %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
@ -121,23 +126,25 @@ void CrossPointWebServer::stop() {
|
|||||||
server.reset();
|
server.reset();
|
||||||
Serial.printf("[%lu] [WEB] Web server stopped and deleted\n", millis());
|
Serial.printf("[%lu] [WEB] Web server stopped and deleted\n", millis());
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
|
||||||
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
// Upload state is now instance variables and will be cleaned up automatically
|
||||||
// later in the file and will be cleared when they go out of scope or on next upload
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap final: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap final: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleClient() const {
|
void CrossPointWebServer::handleClient() const {
|
||||||
static unsigned long lastDebugPrint = 0;
|
static unsigned long lastDebugPrint = 0;
|
||||||
|
|
||||||
// Check running flag FIRST before accessing server
|
// Check running flag FIRST before accessing server (atomic read)
|
||||||
if (!running) {
|
if (!running.load(std::memory_order_acquire)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check server pointer is valid
|
// Lock mutex to safely access server pointer
|
||||||
|
std::lock_guard<std::mutex> lock(serverMutex);
|
||||||
|
|
||||||
|
// Double-check server pointer is valid while holding mutex
|
||||||
if (!server) {
|
if (!server) {
|
||||||
Serial.printf("[%lu] [WEB] WARNING: handleClient called with null server!\n", millis());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,22 +268,27 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||||
server->send(200, "application/json", "");
|
server->send(200, "application/json", "");
|
||||||
server->sendContent("[");
|
server->sendContent("[");
|
||||||
char output[512];
|
|
||||||
constexpr size_t outputSize = sizeof(output);
|
|
||||||
bool seenFirst = false;
|
bool seenFirst = false;
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
|
|
||||||
scanFiles(currentPath.c_str(), [this, &output, &doc, seenFirst](const FileInfo& info) mutable {
|
scanFiles(currentPath.c_str(), [this, &doc, seenFirst](const FileInfo& info) mutable {
|
||||||
doc.clear();
|
doc.clear();
|
||||||
doc["name"] = info.name;
|
doc["name"] = info.name;
|
||||||
doc["size"] = info.size;
|
doc["size"] = info.size;
|
||||||
doc["isDirectory"] = info.isDirectory;
|
doc["isDirectory"] = info.isDirectory;
|
||||||
doc["isEpub"] = info.isEpub;
|
doc["isEpub"] = info.isEpub;
|
||||||
|
|
||||||
const size_t written = serializeJson(doc, output, outputSize);
|
// Calculate required size for JSON output
|
||||||
if (written >= outputSize) {
|
const size_t requiredSize = measureJson(doc) + 1; // +1 for null terminator
|
||||||
// JSON output truncated; skip this entry to avoid sending malformed JSON
|
|
||||||
Serial.printf("[%lu] [WEB] Skipping file entry with oversized JSON for name: %s\n", millis(), info.name.c_str());
|
// Dynamically allocate exact size needed (handles 500-char filenames safely)
|
||||||
|
std::unique_ptr<char[]> output(new char[requiredSize]);
|
||||||
|
|
||||||
|
const size_t written = serializeJson(doc, output.get(), requiredSize);
|
||||||
|
if (written >= requiredSize) {
|
||||||
|
// This should never happen with measureJson, but handle it anyway
|
||||||
|
Serial.printf("[%lu] [WEB] ERROR: JSON serialization failed for: %s (required: %d)\n", millis(),
|
||||||
|
info.name.c_str(), requiredSize);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +297,7 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
} else {
|
} else {
|
||||||
seenFirst = true;
|
seenFirst = true;
|
||||||
}
|
}
|
||||||
server->sendContent(output);
|
server->sendContent(output.get());
|
||||||
});
|
});
|
||||||
server->sendContent("]");
|
server->sendContent("]");
|
||||||
// End of streamed response, empty chunk to signal client
|
// End of streamed response, empty chunk to signal client
|
||||||
@ -293,21 +305,15 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static variables for upload handling
|
// Upload state is now instance variables in the class (see header)
|
||||||
static FsFile uploadFile;
|
// with mutex protection for thread safety
|
||||||
static String uploadFileName;
|
|
||||||
static String uploadPath = "/";
|
|
||||||
static size_t uploadSize = 0;
|
|
||||||
static bool uploadSuccess = false;
|
|
||||||
static String uploadError = "";
|
|
||||||
|
|
||||||
void CrossPointWebServer::handleUpload() const {
|
void CrossPointWebServer::handleUpload() const {
|
||||||
static unsigned long lastWriteTime = 0;
|
// Lock upload mutex for thread-safe access to upload state
|
||||||
static unsigned long uploadStartTime = 0;
|
std::lock_guard<std::mutex> lock(uploadMutex);
|
||||||
static size_t lastLoggedSize = 0;
|
|
||||||
|
|
||||||
// Safety check: ensure server is still valid
|
// Safety check: ensure server is still valid
|
||||||
if (!running || !server) {
|
if (!running.load(std::memory_order_acquire) || !server) {
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] ERROR: handleUpload called but server not running!\n", millis());
|
Serial.printf("[%lu] [WEB] [UPLOAD] ERROR: handleUpload called but server not running!\n", millis());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -315,10 +321,28 @@ void CrossPointWebServer::handleUpload() const {
|
|||||||
const HTTPUpload& upload = server->upload();
|
const HTTPUpload& upload = server->upload();
|
||||||
|
|
||||||
if (upload.status == UPLOAD_FILE_START) {
|
if (upload.status == UPLOAD_FILE_START) {
|
||||||
|
// Check heap before starting upload
|
||||||
|
const size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < 50000) { // Less than 50KB free
|
||||||
|
uploadError = "Insufficient memory for upload";
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] REJECTED - low memory: %d bytes\n", millis(), freeHeap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-allocate String capacities to avoid reallocations during upload
|
||||||
|
uploadFileName.clear();
|
||||||
|
uploadFileName.reserve(upload.filename.length() + 16);
|
||||||
uploadFileName = upload.filename;
|
uploadFileName = upload.filename;
|
||||||
|
|
||||||
|
uploadPath.clear();
|
||||||
|
uploadPath.reserve(256); // Typical path length
|
||||||
|
|
||||||
uploadSize = 0;
|
uploadSize = 0;
|
||||||
uploadSuccess = false;
|
uploadSuccess = false;
|
||||||
uploadError = "";
|
|
||||||
|
uploadError.clear();
|
||||||
|
uploadError.reserve(128); // Pre-allocate error string capacity
|
||||||
|
|
||||||
uploadStartTime = millis();
|
uploadStartTime = millis();
|
||||||
lastWriteTime = millis();
|
lastWriteTime = millis();
|
||||||
lastLoggedSize = 0;
|
lastLoggedSize = 0;
|
||||||
@ -341,10 +365,12 @@ void CrossPointWebServer::handleUpload() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str());
|
Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str());
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), freeHeap);
|
||||||
|
|
||||||
// Create file path
|
// Build file path efficiently with pre-allocation
|
||||||
String filePath = uploadPath;
|
String filePath;
|
||||||
|
filePath.reserve(uploadPath.length() + uploadFileName.length() + 2);
|
||||||
|
filePath = uploadPath;
|
||||||
if (!filePath.endsWith("/")) filePath += "/";
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
filePath += uploadFileName;
|
filePath += uploadFileName;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <SdFat.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// Structure to hold file information
|
// Structure to hold file information
|
||||||
@ -27,17 +30,30 @@ class CrossPointWebServer {
|
|||||||
void handleClient() const;
|
void handleClient() const;
|
||||||
|
|
||||||
// Check if server is running
|
// Check if server is running
|
||||||
bool isRunning() const { return running; }
|
bool isRunning() const { return running.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
// Get the port number
|
// Get the port number
|
||||||
uint16_t getPort() const { return port; }
|
uint16_t getPort() const { return port; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<WebServer> server = nullptr;
|
std::unique_ptr<WebServer> server = nullptr;
|
||||||
bool running = false;
|
std::atomic<bool> running{false};
|
||||||
|
mutable std::mutex serverMutex; // Protects server pointer access
|
||||||
bool apMode = false; // true when running in AP mode, false for STA mode
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
||||||
uint16_t port = 80;
|
uint16_t port = 80;
|
||||||
|
|
||||||
|
// Upload state (instance variables with mutex protection)
|
||||||
|
mutable std::mutex uploadMutex;
|
||||||
|
mutable FsFile uploadFile;
|
||||||
|
mutable String uploadFileName;
|
||||||
|
mutable String uploadPath;
|
||||||
|
mutable size_t uploadSize;
|
||||||
|
mutable bool uploadSuccess;
|
||||||
|
mutable String uploadError;
|
||||||
|
mutable unsigned long lastWriteTime;
|
||||||
|
mutable unsigned long uploadStartTime;
|
||||||
|
mutable size_t lastLoggedSize;
|
||||||
|
|
||||||
// File scanning
|
// File scanning
|
||||||
void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const;
|
void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const;
|
||||||
String formatFileSize(size_t bytes) const;
|
String formatFileSize(size_t bytes) const;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user