Is there any Async TCP HTTP download that actually works on ESP32... that have a working example and not throwing panic attacks at random events?
I am not a programming wizard and I can't run around in circles with chatgpt and grok
Is there any Async TCP HTTP download that actually works on ESP32... that have a working example and not throwing panic attacks at random events?
I am not a programming wizard and I can't run around in circles with chatgpt and grok
Hi @borquee !
You wrote "Async TCP HTTP download", so you are talking about a TCP client? Could you perhaps say a little more about your intended application?
ec2021
Need to download images of the local web server (over wifi) and display them on a small screen … periodically … images are about 50-100kb jpegs … am all fine with displaying
Ok.
While preparing to answer you I just realised that the libraries I used last year to create an ESP32 web server (used for up to 10 users to input A, B, C, D answers for a quiz on a PC) throw the error you mentioned
However I have found this 1:1 replacements and tested them successfully with my application:
https://github.com/ESP32Async/AsyncTCP
https://github.com/ESP32Async/ESPAsyncWebServer
There is an example for a TCP Client
https://github.com/ESP32Async/AsyncTCP/blob/main/examples/Client/Client.ino
This would be the place where data are received
client->onData([](void *arg, AsyncClient *client, void *data, size_t len) {
Serial.printf("** data received by client: %" PRIu16 ": len=%u\n", client->localPort(), len);
});
and where you could copy them to your display.
The example creates a number of clients (MAX_CLIENTS) which you would not require.
Good luck!
ec2021
The libs are available via library manager:
I think you might only need to install the AsyncTCP ...
Maybe these might help...
And there is also an example how to fetch a Website ... That should be close to your needs:
https://github.com/ESP32Async/AsyncTCP/tree/main/examples/FetchWebsite
Lucky: that example was added ten days ago.
Note usage notes in the README to avoid crashes when using AsyncTCP, which require changing some settings, probably using platform.local.txt
There are plenty of trivial one-connection-at-a-time web servers with ESP32; being async makes sense if you can't control when clients contact you with potentially overlapping requests.
But as a client, periodically downloading 100KB images locally: what else is your board doing? Would it block too long? With ESP32, you might try doing that in a separate task. You could even do a loop
-driven stateful download in the main task. No extra async library would be needed. Unlikely to panic; if it fails, it would be in more mundane ways.
It has an lvgl scrolling text, status text, slider, background image and on top of all that espnow connection … all working fine except download
I'm just gonna leave it here ...
Downloads multiple files at once ... didn't crash once
(Use that configuration changes just to be safe!)
// Structure to hold download state
struct DownloadContext {
AsyncClient* client;
File file;
String host;
String path;
String filename;
String saveFilename;
int port;
bool headerParsed;
int contentLength;
int downloadedBytes;
int tip;
};
void downloadFile(const char* cUrl, int tip) {
// Ensure SPIFFS is mounted
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS Mount Failed");
return;
}
//parse url
String url = String(cUrl);
String host;
int port;
String path;
String filename;
//parse URL
// Find the "://" to skip the protocol (e.g., "http://")
int protocolIndex = url.indexOf("://");
int startIndex = (protocolIndex == -1) ? 0 : protocolIndex + 3;
// Find the first '/' after the protocol; this starts the path.
int pathIndex = url.indexOf("/", startIndex);
// If no slash is found, treat everything after protocol as the host.
if (pathIndex == -1) {
host = url.substring(startIndex);
port = 80; // default port
path = "";
filename = "";
return;
}
// Extract host (and optional port) from the URL.
String hostPort = url.substring(startIndex, pathIndex);
// Check if a port is specified with a colon.
int colonIndex = hostPort.indexOf(":");
if (colonIndex != -1) {
host = hostPort.substring(0, colonIndex);
String portString = hostPort.substring(colonIndex + 1);
port = portString.toInt();
} else {
host = hostPort;
port = 80; // default port if not specified
}
// The path starts from the first '/' after the host.
path = url.substring(pathIndex);
// The filename is the substring after the last '/'.
int lastSlash = url.lastIndexOf("/");
if (lastSlash != -1 && lastSlash < url.length() - 1) {
filename = url.substring(lastSlash + 1);
} else {
filename = "";
}
// Allocate our context
DownloadContext* ctx = new DownloadContext;
ctx->host = host;
ctx->port = port;
ctx->path = path;
ctx->filename = filename;
ctx->saveFilename = "/" + filename;
ctx->headerParsed = false;
ctx->tip = tip;
ctx->downloadedBytes = 0;
// Open file for writing
ctx->file = SPIFFS.open(ctx->saveFilename, FILE_WRITE);
if (!ctx->file) {
Serial.println("Failed to open file for writing");
delete ctx;
return;
}
// Create AsyncTCP client
AsyncClient* client = new AsyncClient();
if (!client) {
Serial.println("Unable to allocate client");
ctx->file.close();
delete ctx;
return;
}
ctx->client = client;
// onConnect: send the HTTP GET request once connected
client->onConnect([](void* arg, AsyncClient* client) {
DownloadContext* ctx = (DownloadContext*)arg;
// Serial.println("Connected to server");
// Build HTTP GET request
char request[256];
snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: ESP32\r\nConnection: close\r\n\r\n", ctx->path.c_str(), ctx->host.c_str());
client->write(request, strlen(request));
},
ctx);
// onData: receive data, skip the HTTP header and write the body to file
client->onData([](void* arg, AsyncClient* client, void* data, size_t len) {
DownloadContext* ctx = (DownloadContext*)arg;
uint8_t* buf = (uint8_t*)data;
if (!ctx->headerParsed) {
// Look for the end of the HTTP header ("\r\n\r\n")
for (size_t i = 0; i < len - 3; i++) {
if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n') {
size_t headerEnd = i + 4;
ctx->headerParsed = true;
// Extract the header into a String for parsing
String header = "";
for (size_t j = 0; j < headerEnd; j++) {
header += (char)buf[j];
}
// Parse HTTP status code (e.g., "HTTP/1.1 200 OK")
int firstSpace = header.indexOf(' ');
int secondSpace = header.indexOf(' ', firstSpace + 1);
int statusCode = -1;
if (firstSpace != -1 && secondSpace != -1) {
String codeStr = header.substring(firstSpace + 1, secondSpace);
statusCode = codeStr.toInt();
}
// If status code is not 200, stop everything and delete the file
if (statusCode != 200) {
Serial.printf("HTTP Error: %d. Aborting download and deleting file.\n", statusCode);
client->close();
ctx->file.close();
SPIFFS.remove(ctx->saveFilename.c_str());
return;
}
// Optionally, parse Content-Length (if needed)
int contentLength = -1;
int contentIndex = header.indexOf("Content-Length:");
if (contentIndex != -1) {
int colonIndex = header.indexOf(":", contentIndex);
int lineEnd = header.indexOf("\r\n", colonIndex);
if (colonIndex != -1 && lineEnd != -1) {
String clStr = header.substring(colonIndex + 1, lineEnd);
clStr.trim();
contentLength = clStr.toInt();
ctx->contentLength = contentLength;
}
}
// Write any remaining bytes (body) to the file
int bodyLen = len - headerEnd;
ctx->file.write(buf + headerEnd, bodyLen);
ctx->downloadedBytes += bodyLen;
return;
}
}
} else {
// Header already parsed: write all subsequent data to the file.
ctx->file.write(buf, len);
ctx->downloadedBytes += len;
}
},
ctx);
// onDisconnect: clean up once the connection is closed
client->onDisconnect([](void* arg, AsyncClient* client) {
DownloadContext* ctx = (DownloadContext*)arg;
ctx->file.close();
Serial.printf("Download complete: %d bytes received.\n", ctx->downloadedBytes);
bool fileDeleted = false;
if (ctx->contentLength != -1) {
if (ctx->downloadedBytes == ctx->contentLength) {
Serial.println("Downloaded data matches Content-Length.");
//do your thing ...
Serial.printf("ctx->tip %d\n", ctx->tip);
} else {
Serial.printf("Mismatch: Downloaded %d bytes but expected %d bytes. Deleting file.\n",
ctx->downloadedBytes, ctx->contentLength);
SPIFFS.remove(ctx->saveFilename.c_str());
fileDeleted = true;
}
} else {
Serial.println("Content-Length header was not provided; cannot verify download size.");
}
if (!fileDeleted) {
Serial.printf("File saved as %s\n", ctx->saveFilename.c_str());
}
delete ctx;
delete client;
},
ctx);
// onError: handle errors by closing the client and cleaning up
client->onError([](void* arg, AsyncClient* client, int8_t error) {
Serial.print("Connection error: ");
Serial.println(error);
client->close();
},
ctx);
// Start the connection using the resolved IP address
if (!client->connect(ctx->host.c_str(), ctx->port)) {
Serial.println("Connection failed");
ctx->file.close();
delete ctx;
delete client;
}
}