ESP32 + serveur web : comment envoyer un fichier ?

Bonsoir,

j'ai un ESP32 avec un serveur web (ESPAsyncWebServer) qui enregistre des données dans un tableau (j'en ai causé tantôt, mais le problème n'est plus au même point)

J'aimerai maintenant que dans l'interface web on puisse télécharger les données brutes dans un fichier (CSV, mais je suppose que ça n'est pas le premier problème).

Comment faire envoyer des données (non statiques par nature) par le serveur web ?

Pour les fichiers statiques (pages web) j'ai des instructions de type

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", String(), false, processor);
  });

mais je n'ai pas trouvé d'exemple de code pour envoyer un fichier construit dynamiquement à la demande.

Est-ce que quelqu'un connais un exemple de code qui fait ce genre de chose ?

ce que tu veux faire n'est pas très claire.

Comment le serveur c'est qu'il doit envoyer quelque chose?
En fait tu doit définir qui est serveur, qui est client, pour savoir en faite qui amorce la communication.

J'ai cru comprendre que tu veux dans ton interface web, avoir un bouton qui appelle une URL de ton serveur WEB et que celui-ci te renvois le contenu d'un fichier CSV.
C'est cela ?
Tu veux en faire quoi de ton fichier dans ton UI, uniuqement le sauver sur ton PC?

Exactement : l'ESP32 est le serveur web, il fait l'acquisition des données. Le navigateur est le client, un clic sur un bouton (ou un lien) déclenche le téléchargement des données. Donc dans le sens Serveur --> Client

Dans un premier temps oui.

Dans un deuxième, j'espère pouvoir formater les données correctement pour les donner à un script JS comme https://www.chartjs.org/ pour tracer directement des courbes dans le navigateur (client-side donc)

Bon en farfouillant depuis tout à l'heure je commence à trouver des mots clés peut-être plus pertinents et notamment plein de choses à comprendre à : ESPAsyncWebServer | Async Web Server for ESP8266 and ESP32 mais j'ai du mal à y voir clair...

Un détail que j'avais omis initialement, j’utilise par ailleurs un websocket pour l'interface web mais si j'ai bien compris, de toute façon le WS n'est pas adapté au transfert de données un peu volumineuses.

Si tu veux simplement télécharger, tu peux faire un lien standard en HTML, sans forcément faire de l'ajax.

Dans ce cas là, il faudra surement faire une requête AJAX, pour donner les données au composant charJS.
Bien que je crois que charJS peut directement télécharger les données, mais je ne me rappel plus comment fonctionne le rafraichissement de donnée dynamique.
Par contre ce n'est pas du CSV qui faudra, je crois qu'il utilise du JSon comme c'est souvent le cas en HTML5.
Après faire du JSon, n'est pas vraiment plus compliqué que tu CSV

Je n'ai pas compris ce que tu veux dire ?

Pourquoi ça?
une websocket est une socket normal, hormis la façon d'établir la connexion, qui passe par du HTPP, avant de se convertir en socket standard.
Pourquoi as tu besoin d'une websocket, ton serveur WEB, doit te renvoyer les données en continue.
La websocket est idéeal, pour avertir un client, que quelques chose c'est passé sur le serveur, sans que le client doivent "poller" pour être avertit.

oui : j'affiche les données en continu sur la page web et, à la demande, je démarre l'enregistrement

Hello
Peut-être une piste ici :

A tester...

Merci,

ce n'est pas tout à fait ce que je cherche : manifestement il y a un passage par le FS. Je préférerai ne rien stocker sur la carte et faire l'opération en RAM.
De ce que j'ai lu, c'est assez lent et surtout je souhaite pouvoir passer (idéalement, je n'ai encore rien testé) les données à chartJS. Le format n'est pas le même (c'est du JSON si je ne me trompe pas) et ça va multiplier inutilement les fichiers.

Un autre lien ici, mais c'est toujours un fichier dans le SPIFFS.
Au pire, tu peux l'effacer lorsqu'il est téléchargé.

Ici, une manière de télécharger depuis une carte SD :
https://www.reddit.com/r/esp32/comments/zs1q9x/download_large_file_from_sd_card_using/

il faut générer la réponse HTTP correcte

par exemple testez cela (écrit ici donc non testé)

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

const char*   ssid = "xxx";
const char*   password = "xxx";
AsyncWebServer server(80);

const char contenu[] PROGMEM = u8R"rawliteral(
  <html>
  <head>
    <meta charset="UTF-8">
    <title>Serveur de fichiers ESP32</title>
  </head>
  <body>
    <h1>Bienvenue sur le serveur de fichiers ESP32 !</h1>
    <p><a href='/download'>Télécharger le fichier</a></p>
  </body>
  </html>
)rawliteral";

int toto = 42;
int tutu = 123;

void setup() {
  Serial.begin(115200);

  // Se connecter au Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Connexion au Wi-Fi en cours...");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.write('.');
    delay(500);
  }

  Serial.println(" => Connecté");
  Serial.print("Joindre le serveur web sur http://"); 
  Serial.println(WiFi.localIP()); 

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", contenu);
  });

  server.on("/download", HTTP_GET, [](AsyncWebServerRequest * request) {
    // Préparer le contenu du fichier
    String contenuFichier = "Fichier génré dynamiquement en mémoire";
    contenuFichier += "\r\ntoto = "; contenuFichier += toto;
    contenuFichier += "\r\ntutu = "; contenuFichier += tutu;
    contenuFichier += "\r\n";

    AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", contenuFichier);
    response->addHeader("Content-Disposition", "attachment; filename=fichier.txt");
    request->send(response);
    // on modifie pour la prochaine fois
    toto++;
    tutu++;
  });

  server.begin();
}

void loop() {}

On fabrique ici le contenu du fichier d'abord sous forme de String puis on balance tout d'un coup

C'est OK si le fichier est petit. Si c'est un gros CSV dont on ne connait pas la longueur à priori, il faudra utiliser le mécanisme HTTP1.1 de Chunked Response.

Chuncked est effectivement une des pistes rencontrées pendant mes recherches, ça n'a pas l'air d'être super simple à manipuler mais je vais voir ça.

Deux questions quand-même :
« petit » : quelle est la limite (et à quoi est-elle due ?)

« dont on ne connaît pas la longueur à priori » Je peux la connaître à l'exécution : je compte les acquisition au fur et à mesure mais je ne la connais pas à la compilation. Comment faut-il comprendre « à priori » ?

tout est relatif comme dirait votre avatar :slight_smile:

Dans mon exemple je construis une String en RAM, donc il faut de la RAM dispo. ça dépend donc de ce que fait le reste de votre code et de la mémoire disponible dans le tas au moment de bâtir la réponse.

imaginez que vous vouliez lire 1000 fois la valeur analogique de A0 au moment ou la requête "/download" est reçue et que vous génériez un fichier JSON en ASCII qui dit juste

{"A0":[1562,2903,173,3287,4095,927,2048,3811,15,3320,...]}

comme les valeurs lues sur A0 peuvent varier enter 0 et 4095 vous ne savez pas à priori quelle sera la taille de votre réponse (certaines entrées ont juste 1 chiffre comme la valeur 1 par exemple mais d'autres peuvent avoir 2, 3 ou 4 chiffres (10, 100, 1000 par exemple) .

vous pourriez faire les 1000 lectures puis calculer la longueur mais si vous voulez remplir petit à petit la réponse en effectuant les lectures alors là vous ne savez pas à combien d'octets vous allez terminer

Si vous générez un fichier en binaire, c'est plus facile à calculer car chaque entrée tient alors sur 2 octets si vous avez lu la valeur dans un uint16_t par exemple mais ce n'est plus un CSV.

➜ si vous avez déjà les données en mémoire au moment de la requête, oui vous pouvez calculer (c'est un peu laborieux) la taille de la réponse. Mais un chunk sera plus simple à gérer.

:wink:
Ça pouvait être une limite bien définie liée au protocole HTTP ou fonctionnement de la RAM de l'ESP32

Il y a un moyen de la connaître à la volée, la compilation donne la mémoire disponible une fois les variables globales décomptées mais à l'exécution ?

Ce sera le cas : on arrête l'acquisition avant l'exploitation des données.

C'est noté et bienvenu !

vous aurez combien de valeurs ? (quelle taille de fichier ?)

sur votre ESP32 il y a la fonction esp_get_free_heap_size()

sinon au lieu d'utiliser une grosse String, faites un buffer de taille fixe correspond au max attendu et remplissez le avec les fonctions habituelles des cString

Je me suis limité à 3600

(1 mesure par s et une limite pratique de l’heure de cours)

Le max = tout le fichier ? Ça peut se compter en dizaine de ko... On peut manipuler des chaines de cette taille là sans pb ?

si vous allouez le buffer de manière statique à la compilation (variable globale) vous saurez ce qu'il vous reste et si ça rentre

par exemple ceci doit compiler avec un buffer de 100Ko

char fichier[102400]; // 100Ko

void setup() {
  Serial.begin(115200);
  strlcpy(fichier,  "Hello ", sizeof fichier);
  strlcat(fichier,  "Word!", sizeof fichier);
  Serial.println(fichier);
}

void loop() {}

Si tu pense être limité, dans ce cas une piste est d'envoyer ton binaire directement, c'est à dire ton buffer de donnée d'acquisition.

Dans ton javascript, tu recrée le Json pour charJS.

Tu as du code?

Pas encore, c’est le projet du week-end… mais j’ai commencé par en imaginer le principe.

Du coup la websocket aussi c'est ce que tu as prévus ?