mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 08:07:40 +03:00
Implement aggressive WiFi upload performance optimizations
Add 64KB circular buffer to decouple WiFi receive from SD card writes, enabling continuous data reception while SD writes complete in batches. Key optimizations: - 64KB upload buffer with 32KB batch SD writes - CPU boost to 240MHz during uploads (from 160MHz) - WiFi sleep disabled + max TX power (19.5dBm) - Task priority 5 (from 1) with 6KB stack (from 2KB) - 50 handleClient iterations per loop (from 10) - Thread-safe upload status tracking with mutex - Real-time speed calculation every 500ms Expected improvement: ~100-300 KB/s → 1+ MB/s upload speed in STA mode
This commit is contained in:
parent
d4ae108d9b
commit
cc666e5c18
@ -4,6 +4,7 @@
|
|||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <esp_wifi.h>
|
||||||
#include <qrcode.h>
|
#include <qrcode.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@ -24,8 +25,29 @@ constexpr uint8_t AP_MAX_CONNECTIONS = 4;
|
|||||||
// DNS server for captive portal (redirects all DNS queries to our IP)
|
// DNS server for captive portal (redirects all DNS queries to our IP)
|
||||||
DNSServer* dnsServer = nullptr;
|
DNSServer* dnsServer = nullptr;
|
||||||
constexpr uint16_t DNS_PORT = 53;
|
constexpr uint16_t DNS_PORT = 53;
|
||||||
|
|
||||||
|
// Task configuration for high-performance uploads
|
||||||
|
constexpr uint32_t WEBSERVER_TASK_STACK_SIZE = 6144; // 6KB stack for upload handling
|
||||||
|
constexpr UBaseType_t WEBSERVER_TASK_PRIORITY = 5; // Higher priority for responsiveness
|
||||||
|
|
||||||
|
// WiFi performance: handleClient iterations per loop
|
||||||
|
// Higher values improve upload throughput by processing more data per frame
|
||||||
|
constexpr int HANDLE_CLIENT_ITERATIONS = 50;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// Apply WiFi performance optimizations
|
||||||
|
static void applyWiFiOptimizations() {
|
||||||
|
// Disable WiFi sleep for maximum throughput
|
||||||
|
WiFi.setSleep(false);
|
||||||
|
|
||||||
|
// Set maximum TX power (different for ESP32 variants)
|
||||||
|
// ESP32-C3: max 21dBm, ESP32: max 20.5dBm
|
||||||
|
// Using 78 (19.5dBm) which is safe for all variants
|
||||||
|
esp_wifi_set_max_tx_power(78);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEBACT] WiFi optimizations applied: sleep disabled, TX power maximized\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
@ -48,10 +70,10 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
|
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
|
||||||
2048, // Stack size
|
WEBSERVER_TASK_STACK_SIZE, // Stack size (6KB for upload handling)
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
WEBSERVER_TASK_PRIORITY, // Priority (5 for responsiveness)
|
||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Launch network mode selection subactivity
|
// Launch network mode selection subactivity
|
||||||
@ -162,6 +184,9 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
|||||||
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
|
// Apply WiFi optimizations for maximum upload performance
|
||||||
|
applyWiFiOptimizations();
|
||||||
|
|
||||||
// Start mDNS for hostname resolution
|
// Start mDNS for hostname resolution
|
||||||
if (MDNS.begin(AP_HOSTNAME)) {
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
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);
|
||||||
@ -215,6 +240,9 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
|||||||
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID);
|
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID);
|
||||||
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
|
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
|
||||||
|
|
||||||
|
// Apply WiFi optimizations for maximum upload performance
|
||||||
|
applyWiFiOptimizations();
|
||||||
|
|
||||||
// Start mDNS for hostname resolution
|
// Start mDNS for hostname resolution
|
||||||
if (MDNS.begin(AP_HOSTNAME)) {
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
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);
|
||||||
@ -297,9 +325,9 @@ 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 - HTTP file uploads send data
|
||||||
// in chunks and each handleClient() call processes incoming data
|
// in chunks and each handleClient() call processes incoming data
|
||||||
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();
|
||||||
|
yield(); // Allow other tasks to run between iterations
|
||||||
}
|
}
|
||||||
lastHandleClientTime = millis();
|
lastHandleClientTime = millis();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <esp_pm.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@ -15,15 +16,169 @@ namespace {
|
|||||||
// Note: Items starting with "." are automatically hidden
|
// Note: Items starting with "." are automatically hidden
|
||||||
const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
|
const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
|
||||||
constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
|
constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
|
||||||
|
|
||||||
|
// CPU frequency for upload boost (240MHz for maximum performance)
|
||||||
|
constexpr uint32_t UPLOAD_CPU_FREQ_MHZ = 240;
|
||||||
|
constexpr uint32_t NORMAL_CPU_FREQ_MHZ = 160;
|
||||||
|
|
||||||
|
// Speed calculation interval
|
||||||
|
constexpr unsigned long SPEED_CALC_INTERVAL_MS = 500;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// File listing page template - now using generated headers:
|
CrossPointWebServer::CrossPointWebServer() {
|
||||||
// - HomePageHtml (from html/HomePage.html)
|
uploadMutex = xSemaphoreCreateMutex();
|
||||||
// - FilesPageHeaderHtml (from html/FilesPageHeader.html)
|
uploadPath = "/";
|
||||||
// - FilesPageFooterHtml (from html/FilesPageFooter.html)
|
}
|
||||||
CrossPointWebServer::CrossPointWebServer() {}
|
|
||||||
|
|
||||||
CrossPointWebServer::~CrossPointWebServer() { stop(); }
|
CrossPointWebServer::~CrossPointWebServer() {
|
||||||
|
stop();
|
||||||
|
freeUploadBuffer();
|
||||||
|
if (uploadMutex) {
|
||||||
|
vSemaphoreDelete(uploadMutex);
|
||||||
|
uploadMutex = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer management functions
|
||||||
|
bool CrossPointWebServer::allocateUploadBuffer() const {
|
||||||
|
if (uploadBuffer) return true; // Already allocated
|
||||||
|
|
||||||
|
uploadBuffer = static_cast<uint8_t*>(malloc(UPLOAD_BUFFER_SIZE));
|
||||||
|
if (!uploadBuffer) {
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] ERROR: Failed to allocate %d byte upload buffer!\n", millis(),
|
||||||
|
UPLOAD_BUFFER_SIZE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadBufferHead = 0;
|
||||||
|
uploadBufferTail = 0;
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] Allocated %dKB upload buffer, free heap: %d\n", millis(),
|
||||||
|
UPLOAD_BUFFER_SIZE / 1024, ESP.getFreeHeap());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::freeUploadBuffer() const {
|
||||||
|
if (uploadBuffer) {
|
||||||
|
free(uploadBuffer);
|
||||||
|
uploadBuffer = nullptr;
|
||||||
|
uploadBufferHead = 0;
|
||||||
|
uploadBufferTail = 0;
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] Freed upload buffer, free heap: %d\n", millis(), ESP.getFreeHeap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t CrossPointWebServer::bufferUsed() const {
|
||||||
|
if (uploadBufferHead >= uploadBufferTail) {
|
||||||
|
return uploadBufferHead - uploadBufferTail;
|
||||||
|
}
|
||||||
|
return UPLOAD_BUFFER_SIZE - uploadBufferTail + uploadBufferHead;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t CrossPointWebServer::bufferFree() const { return UPLOAD_BUFFER_SIZE - bufferUsed() - 1; }
|
||||||
|
|
||||||
|
bool CrossPointWebServer::writeToBuffer(const uint8_t* data, size_t len) const {
|
||||||
|
if (!uploadBuffer || len > bufferFree()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
uploadBuffer[uploadBufferHead] = data[i];
|
||||||
|
uploadBufferHead = (uploadBufferHead + 1) % UPLOAD_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t CrossPointWebServer::flushBufferToSD(size_t maxBytes) const {
|
||||||
|
if (!uploadBuffer || !uploadFile) return 0;
|
||||||
|
|
||||||
|
const size_t available = bufferUsed();
|
||||||
|
if (available == 0) return 0;
|
||||||
|
|
||||||
|
size_t toWrite = maxBytes > 0 ? std::min(available, maxBytes) : available;
|
||||||
|
size_t totalWritten = 0;
|
||||||
|
|
||||||
|
// Write in chunks to avoid blocking too long
|
||||||
|
constexpr size_t CHUNK_SIZE = 4096;
|
||||||
|
uint8_t chunk[CHUNK_SIZE];
|
||||||
|
|
||||||
|
while (toWrite > 0) {
|
||||||
|
const size_t chunkLen = std::min(toWrite, CHUNK_SIZE);
|
||||||
|
|
||||||
|
// Copy from circular buffer to linear chunk
|
||||||
|
for (size_t i = 0; i < chunkLen; i++) {
|
||||||
|
chunk[i] = uploadBuffer[uploadBufferTail];
|
||||||
|
uploadBufferTail = (uploadBufferTail + 1) % UPLOAD_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t written = uploadFile.write(chunk, chunkLen);
|
||||||
|
totalWritten += written;
|
||||||
|
|
||||||
|
if (written != chunkLen) {
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] SD write error: expected %d, wrote %d\n", millis(), chunkLen, written);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
toWrite -= chunkLen;
|
||||||
|
yield(); // Allow WiFi stack to process
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU frequency management
|
||||||
|
void CrossPointWebServer::boostCPU() const {
|
||||||
|
if (cpuBoosted) return;
|
||||||
|
|
||||||
|
if (setCpuFrequencyMhz(UPLOAD_CPU_FREQ_MHZ)) {
|
||||||
|
cpuBoosted = true;
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] CPU boosted to %dMHz\n", millis(), UPLOAD_CPU_FREQ_MHZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::restoreCPU() const {
|
||||||
|
if (!cpuBoosted) return;
|
||||||
|
|
||||||
|
if (setCpuFrequencyMhz(NORMAL_CPU_FREQ_MHZ)) {
|
||||||
|
cpuBoosted = false;
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] CPU restored to %dMHz\n", millis(), NORMAL_CPU_FREQ_MHZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread-safe upload status getters
|
||||||
|
bool CrossPointWebServer::isUploading() const {
|
||||||
|
if (!uploadMutex) return false;
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
const bool result = uploadInProgress;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String CrossPointWebServer::getCurrentUploadFile() const {
|
||||||
|
if (!uploadMutex) return "";
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
String result = uploadFileName;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
float CrossPointWebServer::getCurrentUploadSpeed() const {
|
||||||
|
if (!uploadMutex) return 0.0f;
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
const float result = uploadSpeedKBps;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CrossPointWebServer::getUploadProgress() const {
|
||||||
|
if (!uploadMutex) return 0;
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
uint8_t result = 0;
|
||||||
|
if (uploadTotalExpected > 0) {
|
||||||
|
result = static_cast<uint8_t>((uploadSize * 100) / uploadTotalExpected);
|
||||||
|
}
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::begin() {
|
void CrossPointWebServer::begin() {
|
||||||
if (running) {
|
if (running) {
|
||||||
@ -293,19 +448,7 @@ 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
|
|
||||||
static FsFile uploadFile;
|
|
||||||
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;
|
|
||||||
static unsigned long uploadStartTime = 0;
|
|
||||||
static size_t lastLoggedSize = 0;
|
|
||||||
|
|
||||||
// Safety check: ensure server is still valid
|
// Safety check: ensure server is still valid
|
||||||
if (!running || !server) {
|
if (!running || !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());
|
||||||
@ -315,24 +458,25 @@ 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) {
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
|
||||||
uploadFileName = upload.filename;
|
uploadFileName = upload.filename;
|
||||||
uploadSize = 0;
|
uploadSize = 0;
|
||||||
|
uploadTotalExpected = upload.totalSize; // May be 0 if unknown
|
||||||
uploadSuccess = false;
|
uploadSuccess = false;
|
||||||
uploadError = "";
|
uploadError = "";
|
||||||
uploadStartTime = millis();
|
uploadStartTime = millis();
|
||||||
lastWriteTime = millis();
|
lastSpeedCalcTime = millis();
|
||||||
lastLoggedSize = 0;
|
lastSpeedCalcSize = 0;
|
||||||
|
uploadSpeedKBps = 0.0f;
|
||||||
|
uploadInProgress = true;
|
||||||
|
|
||||||
// Get upload path from query parameter (defaults to root if not specified)
|
// Get upload path from query parameter
|
||||||
// Note: We use query parameter instead of form data because multipart form
|
|
||||||
// fields aren't available until after file upload completes
|
|
||||||
if (server->hasArg("path")) {
|
if (server->hasArg("path")) {
|
||||||
uploadPath = server->arg("path");
|
uploadPath = server->arg("path");
|
||||||
// Ensure path starts with /
|
|
||||||
if (!uploadPath.startsWith("/")) {
|
if (!uploadPath.startsWith("/")) {
|
||||||
uploadPath = "/" + uploadPath;
|
uploadPath = "/" + uploadPath;
|
||||||
}
|
}
|
||||||
// Remove trailing slash unless it's root
|
|
||||||
if (uploadPath.length() > 1 && uploadPath.endsWith("/")) {
|
if (uploadPath.length() > 1 && uploadPath.endsWith("/")) {
|
||||||
uploadPath = uploadPath.substring(0, uploadPath.length() - 1);
|
uploadPath = uploadPath.substring(0, uploadPath.length() - 1);
|
||||||
}
|
}
|
||||||
@ -340,9 +484,22 @@ void CrossPointWebServer::handleUpload() const {
|
|||||||
uploadPath = "/";
|
uploadPath = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
|
||||||
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(), ESP.getFreeHeap());
|
||||||
|
|
||||||
|
// Allocate upload buffer and boost CPU
|
||||||
|
if (!allocateUploadBuffer()) {
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
uploadError = "Failed to allocate upload buffer";
|
||||||
|
uploadInProgress = false;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boostCPU();
|
||||||
|
|
||||||
// Create file path
|
// Create file path
|
||||||
String filePath = uploadPath;
|
String filePath = uploadPath;
|
||||||
if (!filePath.endsWith("/")) filePath += "/";
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
@ -356,62 +513,121 @@ void CrossPointWebServer::handleUpload() const {
|
|||||||
|
|
||||||
// Open file for writing
|
// Open file for writing
|
||||||
if (!SdMan.openFileForWrite("WEB", filePath, uploadFile)) {
|
if (!SdMan.openFileForWrite("WEB", filePath, uploadFile)) {
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
uploadError = "Failed to create file on SD card";
|
uploadError = "Failed to create file on SD card";
|
||||||
|
uploadInProgress = false;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
restoreCPU();
|
||||||
|
freeUploadBuffer();
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str());
|
Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] File created successfully: %s\n", millis(), filePath.c_str());
|
Serial.printf("[%lu] [WEB] [UPLOAD] File created successfully: %s\n", millis(), filePath.c_str());
|
||||||
|
|
||||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||||
if (uploadFile && uploadError.isEmpty()) {
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
const unsigned long writeStartTime = millis();
|
const bool hasError = !uploadError.isEmpty();
|
||||||
const size_t written = uploadFile.write(upload.buf, upload.currentSize);
|
xSemaphoreGive(uploadMutex);
|
||||||
const unsigned long writeEndTime = millis();
|
|
||||||
const unsigned long writeDuration = writeEndTime - writeStartTime;
|
|
||||||
|
|
||||||
if (written != upload.currentSize) {
|
if (uploadFile && !hasError) {
|
||||||
uploadError = "Failed to write to SD card - disk may be full";
|
// Try to write to buffer first (fast path - doesn't block on SD)
|
||||||
uploadFile.close();
|
if (!writeToBuffer(upload.buf, upload.currentSize)) {
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] WRITE ERROR - expected %d, wrote %d\n", millis(), upload.currentSize,
|
// Buffer full - need to flush to SD first
|
||||||
written);
|
const size_t flushed = flushBufferToSD(UPLOAD_BATCH_WRITE_SIZE);
|
||||||
} else {
|
if (flushed == 0) {
|
||||||
uploadSize += written;
|
// Direct write as fallback
|
||||||
|
const size_t written = uploadFile.write(upload.buf, upload.currentSize);
|
||||||
// Log progress every 50KB or if write took >100ms
|
if (written != upload.currentSize) {
|
||||||
if (uploadSize - lastLoggedSize >= 51200 || writeDuration > 100) {
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
const unsigned long timeSinceStart = millis() - uploadStartTime;
|
uploadError = "Failed to write to SD card - disk may be full";
|
||||||
const unsigned long timeSinceLastWrite = millis() - lastWriteTime;
|
xSemaphoreGive(uploadMutex);
|
||||||
const float kbps = (uploadSize / 1024.0) / (timeSinceStart / 1000.0);
|
uploadFile.close();
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] WRITE ERROR - expected %d, wrote %d\n", millis(), upload.currentSize,
|
||||||
Serial.printf(
|
written);
|
||||||
"[%lu] [WEB] [UPLOAD] Progress: %d bytes (%.1f KB), %.1f KB/s, write took %lu ms, gap since last: %lu "
|
return;
|
||||||
"ms\n",
|
}
|
||||||
millis(), uploadSize, uploadSize / 1024.0, kbps, writeDuration, timeSinceLastWrite);
|
} else {
|
||||||
lastLoggedSize = uploadSize;
|
// Try buffer again after flush
|
||||||
|
if (!writeToBuffer(upload.buf, upload.currentSize)) {
|
||||||
|
// Still can't fit - direct write
|
||||||
|
const size_t written = uploadFile.write(upload.buf, upload.currentSize);
|
||||||
|
if (written != upload.currentSize) {
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
uploadError = "Failed to write to SD card - disk may be full";
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
uploadFile.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastWriteTime = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush buffer when it reaches threshold
|
||||||
|
if (bufferUsed() >= UPLOAD_BATCH_WRITE_SIZE) {
|
||||||
|
flushBufferToSD(UPLOAD_BATCH_WRITE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
|
uploadSize += upload.currentSize;
|
||||||
|
|
||||||
|
// Calculate speed every 500ms
|
||||||
|
const unsigned long now = millis();
|
||||||
|
if (now - lastSpeedCalcTime >= SPEED_CALC_INTERVAL_MS) {
|
||||||
|
const size_t bytesSinceLastCalc = uploadSize - lastSpeedCalcSize;
|
||||||
|
const float secondsElapsed = (now - lastSpeedCalcTime) / 1000.0f;
|
||||||
|
if (secondsElapsed > 0) {
|
||||||
|
uploadSpeedKBps = (bytesSinceLastCalc / 1024.0f) / secondsElapsed;
|
||||||
|
}
|
||||||
|
lastSpeedCalcTime = now;
|
||||||
|
lastSpeedCalcSize = uploadSize;
|
||||||
|
|
||||||
|
// Log progress
|
||||||
|
const float avgSpeed = (uploadSize / 1024.0f) / ((now - uploadStartTime) / 1000.0f);
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] Progress: %d bytes (%.1f KB), current: %.1f KB/s, avg: %.1f KB/s\n",
|
||||||
|
millis(), uploadSize, uploadSize / 1024.0f, uploadSpeedKBps, avgSpeed);
|
||||||
|
}
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (upload.status == UPLOAD_FILE_END) {
|
} else if (upload.status == UPLOAD_FILE_END) {
|
||||||
if (uploadFile) {
|
if (uploadFile) {
|
||||||
|
// Flush remaining buffer to SD
|
||||||
|
flushBufferToSD();
|
||||||
uploadFile.close();
|
uploadFile.close();
|
||||||
|
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
if (uploadError.isEmpty()) {
|
if (uploadError.isEmpty()) {
|
||||||
uploadSuccess = true;
|
uploadSuccess = true;
|
||||||
Serial.printf("[%lu] [WEB] Upload complete: %s (%d bytes)\n", millis(), uploadFileName.c_str(), uploadSize);
|
const unsigned long duration = millis() - uploadStartTime;
|
||||||
|
const float avgSpeed = (uploadSize / 1024.0f) / (duration / 1000.0f);
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] Complete: %s (%d bytes in %lu ms, avg %.1f KB/s)\n", millis(),
|
||||||
|
uploadFileName.c_str(), uploadSize, duration, avgSpeed);
|
||||||
}
|
}
|
||||||
|
uploadInProgress = false;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreCPU();
|
||||||
|
freeUploadBuffer();
|
||||||
|
|
||||||
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
||||||
if (uploadFile) {
|
if (uploadFile) {
|
||||||
uploadFile.close();
|
uploadFile.close();
|
||||||
// Try to delete the incomplete file
|
|
||||||
String filePath = uploadPath;
|
String filePath = uploadPath;
|
||||||
if (!filePath.endsWith("/")) filePath += "/";
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
filePath += uploadFileName;
|
filePath += uploadFileName;
|
||||||
SdMan.remove(filePath.c_str());
|
SdMan.remove(filePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(uploadMutex, portMAX_DELAY);
|
||||||
uploadError = "Upload aborted";
|
uploadError = "Upload aborted";
|
||||||
Serial.printf("[%lu] [WEB] Upload aborted\n", millis());
|
uploadInProgress = false;
|
||||||
|
xSemaphoreGive(uploadMutex);
|
||||||
|
|
||||||
|
restoreCPU();
|
||||||
|
freeUploadBuffer();
|
||||||
|
Serial.printf("[%lu] [WEB] [UPLOAD] Aborted\n", millis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDCardManager.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -12,6 +15,11 @@ struct FileInfo {
|
|||||||
bool isDirectory;
|
bool isDirectory;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Upload buffer configuration for high-speed transfers
|
||||||
|
// 64KB buffer allows WiFi to receive data while SD card writes complete
|
||||||
|
constexpr size_t UPLOAD_BUFFER_SIZE = 64 * 1024; // 64KB circular buffer
|
||||||
|
constexpr size_t UPLOAD_BATCH_WRITE_SIZE = 32 * 1024; // Flush to SD every 32KB
|
||||||
|
|
||||||
class CrossPointWebServer {
|
class CrossPointWebServer {
|
||||||
public:
|
public:
|
||||||
CrossPointWebServer();
|
CrossPointWebServer();
|
||||||
@ -32,12 +40,51 @@ class CrossPointWebServer {
|
|||||||
// Get the port number
|
// Get the port number
|
||||||
uint16_t getPort() const { return port; }
|
uint16_t getPort() const { return port; }
|
||||||
|
|
||||||
|
// Upload status getters (thread-safe)
|
||||||
|
bool isUploading() const;
|
||||||
|
String getCurrentUploadFile() const;
|
||||||
|
float getCurrentUploadSpeed() const; // KB/s
|
||||||
|
uint8_t getUploadProgress() const; // 0-100%
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<WebServer> server = nullptr;
|
std::unique_ptr<WebServer> server = nullptr;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
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 (protected by mutex)
|
||||||
|
mutable SemaphoreHandle_t uploadMutex = nullptr;
|
||||||
|
mutable FsFile uploadFile;
|
||||||
|
mutable String uploadFileName;
|
||||||
|
mutable String uploadPath;
|
||||||
|
mutable size_t uploadSize = 0;
|
||||||
|
mutable size_t uploadTotalExpected = 0;
|
||||||
|
mutable bool uploadSuccess = false;
|
||||||
|
mutable String uploadError;
|
||||||
|
mutable bool uploadInProgress = false;
|
||||||
|
mutable float uploadSpeedKBps = 0.0f;
|
||||||
|
mutable unsigned long uploadStartTime = 0;
|
||||||
|
mutable unsigned long lastSpeedCalcTime = 0;
|
||||||
|
mutable size_t lastSpeedCalcSize = 0;
|
||||||
|
|
||||||
|
// Upload buffer for decoupling WiFi receive from SD writes
|
||||||
|
mutable uint8_t* uploadBuffer = nullptr;
|
||||||
|
mutable size_t uploadBufferHead = 0; // Write position
|
||||||
|
mutable size_t uploadBufferTail = 0; // Read position
|
||||||
|
mutable bool cpuBoosted = false;
|
||||||
|
|
||||||
|
// Buffer management
|
||||||
|
bool allocateUploadBuffer() const;
|
||||||
|
void freeUploadBuffer() const;
|
||||||
|
size_t bufferUsed() const;
|
||||||
|
size_t bufferFree() const;
|
||||||
|
bool writeToBuffer(const uint8_t* data, size_t len) const;
|
||||||
|
size_t flushBufferToSD(size_t maxBytes = 0) const;
|
||||||
|
|
||||||
|
// CPU frequency management for upload performance
|
||||||
|
void boostCPU() const;
|
||||||
|
void restoreCPU() const;
|
||||||
|
|
||||||
// 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