ESP32 transfer files via wifi

Hello!

In my project, I have a set of data stored on an SD card. Ideally, the card will be full, and the files inside will total around 60 GB.

Due to the way the project is designed, it would be best not to remove the SD card from the device. That’s why I want to test whether it’s possible to transfer these files from the SD to a computer. The transfer doesn’t need to be particularly fast—it could take up to 30 minutes, and that wouldn’t be an issue.

However, I’m testing this sketch, and if my calculations are correct, the transfer would take around 40 hours, with a download rate of approximately 430 kbps.

I have no experience with this topic, and I wanted to ask if anyone knows what I could try to download the files from the SD card at a somewhat faster speed.

Of course, I am running tests with my ESP32S3 right next to the router.

P.S.: Just as I don’t want to remove the SD card from the device, I also can’t connect via Ethernet.
I’ve seen that there are many discussions about using FTP, but none (that I’ve found) specifically about large files.

Here is my sketch, working at 240MHz (in my project it might be 80MHz):

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>

#define MMC_D2 4  
#define MMC_D3 5
#define MMC_CMD 6
#define MMC_CLK 7
#define MMC_D0 15
#define MMC_D1 16

#define SDMMC_SPEED SDMMC_FREQ_HIGHSPEED

const char* ssid = "SSID";      
const char* password = "PW";  

AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);
  while(!Serial)
    delay(10);

  Serial.println("Serial started.");
  
  pinMode(LED_BUILTIN, OUTPUT);
  delay(10);
  digitalWrite(LED_BUILTIN, LOW);

  // Initialize the SD card
  SD_MMC.setPins(MMC_CLK, MMC_CMD, MMC_D0, MMC_D1, MMC_D2, MMC_D3);
  Serial.println("Initializing SD card...");
  if (!SD_MMC.begin("/sdcard", true, SDMMC_SPEED)) {
    Serial.println("Failed to initialize SD card.");
    return;
  }
  Serial.println("SD card initialized successfully.");

  digitalWrite(LED_BUILTIN, HIGH);   // SD card is ready.
 
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
    attempts++;
    if (attempts > 10) { // If not connected within 10 attempts, print error message
      Serial.println("Failed to connect to Wi-Fi");
      break;
    }
  }
  
  // Double check.
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("Connected to Wi-Fi");
    Serial.print("IP: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("Could not connect to Wi-Fi.");
  }


  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    String response = "<h1>Available files on SD</h1><ul>";

    // List all files on the SD card
    Serial.println("Listing files on SD...");
    File root = SD_MMC.open("/");
    if (root) {
      File file = root.openNextFile();
      while (file) {
        String fileName = file.name();
        Serial.print("File found: ");
        Serial.println(fileName);
        
        // Replace spaces with %20 in the URL
        fileName.replace(" ", "%20");
        
        response += "<li><a href='/download?file=" + fileName + "'>" + fileName + "</a></li>";
        file = root.openNextFile();
      }
      root.close();
    } else {
      Serial.println("Could not open SD root directory.");
    }

    response += "</ul>";
    request->send(200, "text/html", response);
  });

  // Route to download a file
  server.on("/download", HTTP_GET, [](AsyncWebServerRequest *request){
    String fileName = request->getParam("file")->value();  // Get the filename from the URL
    
    // Print the requested filename
    Serial.print("Requested file: ");
    Serial.println(fileName);

    String filePath = "/" + fileName;
    
    // Print the full path
    Serial.print("File path: ");
    Serial.println(filePath);

    // Check if the file exists
    File file = SD_MMC.open(filePath);
    if (file) {
      // Send the file to the browser with its original name
      Serial.println("File found, sending...");

      // File is downloaded with its original name
      AsyncWebServerResponse *response = request->beginResponse(SD_MMC, filePath, "application/octet-stream");
      response->addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
      request->send(response);
      
      file.close();
    } else {
      Serial.println("Error: File does not exist.");
      request->send(404, "text/plain", "File not found");
    }
  });

  // Start the server
  server.begin();
}

void loop() {
  // Nothing to do here
}

try a web search for ESP32 FTP server. e.g. SimpleFTP Server and ESP32 FTP Client, e.g. ESP32_FTPClient
FTP was implemented to transfer files between computer systems with flow control, error correction, etc - on a PC have a look at filezilla-project

Thanks for the reply.
To make this work I had to:

#include <WiFi.h>
#include <SimpleFTPServer.h>
#include <SD_MMC.h>

const char* ssid = "ssid";      // Wi-Fi network name
const char* password = "pw";  // Wi-Fi network password

// SD card pins configuration
#define MMC_D2 4  
#define MMC_D3 5
#define MMC_CMD 6
#define MMC_CLK 7
#define MMC_D0 15
#define MMC_D1 16

// FTP server instance
FtpServer ftpServer;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
  }

  // Connect to Wi-Fi
  Serial.println("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  WiFi.setSleep(false);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected!");
  Serial.printf("Connected to: %s\n", ssid);
  Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());

  // Initialize SD card
  Serial.print("Initializing SD card...");
  SD_MMC.setPins(MMC_CLK, MMC_CMD, MMC_D0, MMC_D1, MMC_D2, MMC_D3);
  if (!SD_MMC.begin("/sdcard", true, SDMMC_SPEED)) {
    Serial.println("Error initializing SD card.");
    return;
  }
  Serial.println("SD card initialized successfully.");

  // Start FTP server with credentials
  ftpServer.begin("user", "password");  // Replace with your FTP credentials
  Serial.println("FTP server started.");
}

void loop() {
  // Process FTP server requests
  ftpServer.handleFTP();  // Handle FTP requests
}

Edit FtpServerKey.h:

// esp32 configuration
#ifndef DEFAULT_FTP_SERVER_NETWORK_TYPE_ESP32
	#define DEFAULT_FTP_SERVER_NETWORK_TYPE_ESP32 		NETWORK_ESP32
	#define DEFAULT_STORAGE_TYPE_ESP32 					STORAGE_SD_MMC
	/**

And this FileZilla config:

But the transfer rate is slower than my first sketch :frowning:

It's slower? KiB/s is usually Bytes per second, and lowercase b is bits. But doing the math, you probably meant 430K B/s in your original test.

I whipped up a NetworkServer just sending static bytes from a fixed 4KB buffer, and got a little over 400KB/s as well to transfer 5MB. With a phone on the same WiFi, I get 300 Mbps with fast.com. You could also test the max read speed from the SD, into a buffer and doing nothing with it, to see if that is anywhere fast enough.

What is the slowest part of the circuit, the SD card read or the WiFi? Do you need ack?

@kenb4 @sonofcy

Thanks for helping me clarify. I actually meant 430 KB.

The write speed I get with this SD card is around 1.8 MB/s. I'm assuming the read speed is around this value or higher.

I achieve that speed by saving the file in 3 KB chunks (fastest, tested) using this task:

void sdWriteTask(void *parameter)
{
  while (sdWritePos < PSRAM_BUFFER_SIZE)
  {
      int bytesAvailable = PSRAM_BUFFER_SIZE - sdWritePos;
      int bytesToWrite= min(bytesAvailable , BLOCK_SD);
      audioFile.write((uint8_t *)psramBuffer + sdWritePos, bytesToWrite);
      sdWritePos += bytesToWrite;
  } 
  audioFile.flush();
  audioFile.close();
  SD_MMC.end();
  vTaskDelete(NULL);
}

I fill ~7.5 MB of data into the PSRAM and then save the entire buffer to the SD in ~4.2 seconds.

The following line:

      audioFile.write((uint8_t *)psramBuffer + sdWritePos, bytesToWrite);

allows selecting the amount of data sent to the file, but I don't know how to do something similar with:

    if (file) {
      // Send the file to the browser with its original name
      Serial.println("File found, sending...");

      // File is downloaded with its original name
      AsyncWebServerResponse *response = request->beginResponse(SD_MMC, filePath, "application/octet-stream");
      response->addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
      request->send(response);
      
      file.close();
    }

Or:

void loop() {
  // Process FTP server requests
  ftpServer.handleFTP();  // Handle FTP requests
}

So, I wouln't know how to check if I have a bottleneck with the SD read or any other part of the sketch.

@sonofcy I don't need an ACK or anything like that at the moment. This is just a feasibility test (to avoid disassembling the prototype). The final release might or might not need it—I was considering using CRC or something similar at the end of the transmission.

I assumed CRC pr better, but when the receiver checks the CRC and finds an error (unless correctable), then the receiver needs to send a NACK so the transmitter knows to retry.

FWIW, note what -- at one time at least -- was considered a large file by the ESPAsyncWebServer

your ESP is able to serve even large (large in terms of ESP, e.g. 100kB)

The code there demonstrates an AwsResponseFiller, which can be implemented to send giant blobs with no other overhead. For example, with a global buffer

constexpr size_t gbuf_base = 4096;
char gbuf[gbuf_base * 3];  // big enough for any `(x % 4096) + 5760`

that is optionally initialized in setup just to avoid downloading random bytes or NULs, and to visually recognize a proper response

  for (size_t i = 0; i < sizeof(gbuf); i++) {
    gbuf[i] = (i + 1) % 64 ? '.' : '\n';
  }

with a handler

  server.on("/b", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (const AsyncWebParameter *param = request->getParam("n")) {
      if (long size = param->value().toInt()) {
        bool verbose = request->hasParam("v");
        AsyncWebServerResponse *resp = request->beginResponse(
          "text/plain",
          size,
          [size, verbose](uint8_t *buffer, size_t maxLen, size_t total) -> size_t {
            if (verbose) {
              Serial.print(maxLen);
              Serial.print(" + ");
              Serial.print(total);
              Serial.print(" = ");
              Serial.println(maxLen + total);
            }
            size_t offset = total % gbuf_base;
            size_t chunk = min(sizeof(gbuf) - offset, maxLen);
            memcpy(buffer, gbuf + offset, chunk);
            if (total + chunk >= size) {
              buffer[chunk - 1] = '\n';  // always end with newline
            }
            return chunk;
          });
        request->send(resp);
        return;
      }
    }
    request->send(400);
  });

So for example (quotes are needed because of the &)

$ curl '10.0.0.132/b?v&n=5000000' > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 4882k  100 4882k    0     0   403k      0  0:00:12  0:00:12 --:--:--  402k

I still get between low-mid 300s to mid-500 KB/s. Looking at the chunks

2888 + 4739214 = 4742102
4308 + 4742102 = 4746410
5760 + 4746410 = 4752170
5760 + 4752170 = 4757930
5760 + 4757930 = 4763690
5760 + 4763690 = 4769450
5760 + 4769450 = 4775210
5760 + 4775210 = 4780970
5760 + 4780970 = 4786730
5760 + 4786730 = 4792490
5744 + 4792490 = 4798234
2888 + 4798234 = 4801122

There's lots of 5760, which might be a constant resulting from TCP, MTU, etc, and whatever the internals are (perhaps controlled by some compile-time MAGIC_NUMBER). It's the maximum value requested.

In the case of beginResponse(FS&..., that returns a AsyncFileResponse with similar code

size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len) {
  return _content.read(data, len);
}

Getting in the weeds, that len argument depends on

  size_t space = request->client()->space();
//...
      outLen = space;
//...
      readLen = _fillBufferAndProcessTemplates(buf + headLen, outLen);

which in turn is actually in the Async TCP library

  // TCP buffer space available
  size_t space() const;

So yeah: that's not an easy thing to "dial in". Again, the response can't get much faster than doing a memcpy from a static buffer.

Hi I have a similar issue and would be interested to know the final outcome of this. In my case I have a Nano on the SD card shield and as the USB is not used once the program is running I was just going to provide a connection to this and have a routine which sent the data over that to my computer.

Highest datatransfer via ftp was 1gb per hour.

Using a ESP32 S3.