ESP32 Webserver Chart von Datalogger SD Karte

Hallo an alle, ich bin gerade dabei einen Datenlogger zu bauen, nun würde ich gern die auf der SD Karte gespeicherten Daten(Zeitstempel;Messwert) über den AsyncWebsever auf dem ESP32 als Charts graphisch anzeigen lassen. Ich habe das halbe Internet durchsucht und finde nur Beispiele mit RealTimeCharts und komme nicht weiter. Mein Problem ist die Daten in einem html-String zusammenzustellen ohne das der String zu groß wird und ich bekomme es auch nicht hin den String gestückelt und Daten sequentiell mit einem http-request zu senden. Mein nicht funktionierender Ansatz:

server.on("/charts.html", HTTP_GET, [](AsyncWebServerRequest *request) {
    //Serial.println("Web Server: temperature page");
    //String html = CHART_LINK_CANVAS;  // Use the HTML content from the temperature.h file    
    request->send(SD, "/chart1.html","text/html");
    
  });
char currentChar;
String myString = "";
const char XMDATA[] PROGMEM = "";
const char YDATA[] PROGMEM = "";

const char *CHART1 = R"=====(

  <!DOCTYPE html>
  <html>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script>
  <body>
  <canvas id="myChart" style="width:100%;max-width:600px"></canvas>

  <script>
  const xValues = [
)=====";  
  //XMDATA
  
const char *CHART2 = R"=====(  
  ];

new Chart("myChart", {
  type: "line",
  data: {
    labels: xValues,
    datasets: [{ 
      data: [
)=====";        
        //YDATA
const char *CHART3 = R"=====(        
        ],
      borderColor: "red",
      fill: false
    }]
  },
  options: {
    legend: {display: false}
  }
});
</script>

)=====";

void readDataChartXM(fs::FS &fs, const char * path){
  int j = 0;  
  File myFile = fs.open(path);
  if (myFile) {
    Serial.println(path);
    // read from the file until there's nothing else in it:
    while (myFile.available()) {      
      currentChar = myFile.read();
      if (currentChar == ';'){
        if (j == 0){          
          Serial.println( "\"" + myString + "\""  + ",");
          j = 1;
        }else{          
          j=0;
        }                 
        myString = "";
      }else if(currentChar != '\r' && currentChar != '\n'){        
        myString += currentChar;
      }     
    }  
    // close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening");
  }
}
void readDataChart(fs::FS &fs, const char * path){
  int j = 0;  
  File myFile = fs.open(path);
  if (myFile) {
    Serial.println(path);
    // read from the file until there's nothing else in it:
    while (myFile.available()) {      
      currentChar = myFile.read();
      if (currentChar == ';'){
        if (j == 0){         
          j = 1;
        }else{
          Serial.println(myString + ",");
          j=0;
        }                
        myString = "";
      }else if(currentChar != '\r' && currentChar != '\n'){
        myString += currentChar;
      }   
    }  
    // close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening ");
  }
}
void createChart(){  
  appendFile(SD,"/chart1.html",CHART1);
  readDataChartXM(SD, "/dhttemp.txt");
  appendFile(SD,"/chart1.html",CHART2);
  readDataChart(SD, "/dhttemp.txt");
  appendFile(SD,"/chart1.html",CHART3);  
}
// Initialisiere die Tabelle von Strings
const char* const string_table[] PROGMEM = { CHART1, XMDATA, CHART2, YDATA, CHART3 };  

Ich habe auch versucht den String im FLASH-Speicher zusammen zu basteln, ohne Erfolg.
Zuletzt war meine Idee ihn auf SD-Karte zu schreiben , was daran scheitert das nur eine Datei gleichzeitig geöffnet werden kann und auch nicht so schön ist. Gibt es irgendeine Lösung auf dem ESP32? auf dem Arduino 33 IOT konnte ich mit client.print() die Daten direkt senden.. ohne das der Programmspeicher überläuft. Ich wäre über jede Hilfe sehr dankbar.

Mit einem ESP32 habe ich so eine Grafik (Ausschnitt) im Browser mit chart.js erzeugt:

Wenn es das ist, was Du möchtest, dann ja :slightly_smiling_face:

Ja schon, ich habe da ca. 4300 Messpunkte über einen Monat pro Sensor die von SD Karte gelesen werden und an den Client gesendet werden sollen.

Das ist eine Menge!

Würdest Du denn, wie bei mir, 100 Meßpunkte angezeigt bekommen? Wäre das gezeigte Programm dafür geeignet? Wie groß ist die Datei auf der SD-Karte?

Mich irritiert die Verwendung von ProgMem, weil der ESP32 das nicht hat.

Ich weiß nicht genau ob 100 Meßpunkte hier gehen, da das ganze Programm für die Charts hier nicht so funktioniert. Das mit PROGMEM war nur ein Versuch den String im Flash zusammen zustellen, da dort nach Datenblatt Nano-ESP32-S3 16Mb sein sollten. Auf meinem Nano 33 IOT hat eine Datei nach ca. 16 Tagen 70 Kb.


Das ist ein Auschnitt von vor 2 Tagen on dem Nano 33 IOT. Auf dem Nano 33 IOT läuft dieser Code mit der WIFININA.h soweit.


/*
  SD card datalogger

  This example shows how to log data from three analog sensors
  to an SD card using the SD library.

  The circuit:
   analog sensors on analog ins 0, 1, and 2
   SD card attached to SPI bus as follows:
 ** MOSI - pin 11
 ** MISO - pin 12
 ** CLK - pin 13
 ** CS - pin 4 (for MKRZero SD: SDCARD_SS_PIN)

  created  24 Nov 2010
  modified 9 Apr 2012
  by Tom Igoe

  This example code is in the public domain.

*/

#include <SPI.h>
#include <SD.h>

File root;
File myFile;
const int chipSelect = 10;
String myString = "";
String Uhrzeit = "";
char currentChar;
float value;
int actualFilenumber;


void readDataChartXM(WiFiClient client, String datei){
  int j = 0;  
  myFile = SD.open(datei);
  if (myFile) {
    Serial.println(datei);
    // read from the file until there's nothing else in it:
    while (myFile.available()) {      
      currentChar = myFile.read();
      if (currentChar == ';'){
        if (j == 0){          
          client.print("\"" + myString + "\""  + ",");
          j = 1;
        }else{          
          j=0;
        }                 
        myString = "";
      }else if(currentChar != '\r' && currentChar != '\n'){        
        myString += currentChar;
      }     
    }  
    // close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening" + datei);
  }
}
void readDataChart(WiFiClient client, String datei){
  int j = 0;  
  myFile = SD.open(datei);
  if (myFile) {
    Serial.println(datei);
    // read from the file until there's nothing else in it:
    while (myFile.available()) {      
      currentChar = myFile.read();
      if (currentChar == ';'){
        if (j == 0){         
          j = 1;
        }else{
          client.print(myString + ",");
          j=0;
        }                
        myString = "";
      }else if(currentChar != '\r' && currentChar != '\n'){
        myString += currentChar;
      }     
    }  
    // close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening " + datei);
  }
}


void readDataWifi(WiFiClient client){
  int i = 0;
  myFile = SD.open("datalog3.txt");
  if (myFile) {
    Serial.println("test.txt:");
    // read from the file until there's nothing else in it:
    while (myFile.available()) {      
      currentChar = myFile.read();
      if (currentChar == ';'){        
        client.print(myString);        
        client.println();
        myString = "";
      }else{
        //client.write(currentChar);
        myString += currentChar;
      }     
    }    
    // close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening test.txt");
  }
}
void clientIncreaseFilenumber(){

  actualFilenumber += 1;
  
}
void printDirectory(File dir, int numTabs) {
  while (true) {
    File entry =  dir.openNextFile();
    if (! entry) {
      // no more files
      break;
    }
    for (uint8_t i = 0; i < numTabs; i++) {
      Serial.print('\t');
    }
    Serial.print(entry.name());
    if (entry.isDirectory()) {
      Serial.println("/");
      printDirectory(entry, numTabs + 1);
    } else {
      // files have sizes, directories do not
      Serial.print("\t\t");
      Serial.println(entry.size(), DEC);
    }
    entry.close();
  }
}
void printRoot() {
  root = SD.open("/");
  printDirectory(root, 0);
}
void clientprintDirectory(File dir, int numTabs, WiFiClient client) {  
  while (true) {    
    File entry =  dir.openNextFile();
    if (! entry) {
      // no more files
      break;
    }
    for (uint8_t i = 0; i < numTabs; i++) {
      client.print('\t');
    }
    client.print(entry.name());
    if (entry.isDirectory()) {
      client.println("/");
      clientprintDirectory(entry, numTabs + 1, client);
    } else {
      // files have sizes, directories do not
      client.print("\t\t");
      client.println(entry.size(), DEC);
    }
    entry.close();    
  }
  
}

void clientprintRoot(WiFiClient client) {
  root = SD.open("/");
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println("Connection: close"); 
  client.println();
  client.println("<!DOCTYPE HTML>");
  client.println("<html>");
  client.println("<style>");
  client.println("textarea{width:300px;height:300px;}");
  client.println("</style>");
  client.println("<form>");  
  client.print("<textarea>");
  clientprintDirectory(root, 0, client);
  client.print("</textarea>");
  client.println("</form>");
  client.println("</html>");
  client.println();  
}
void clientWifiStatus(WiFiClient client) {
  // print the SSID of the network you're attached to:
  client.print("SSID: ");
  client.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  client.print("IP Address: ");
  client.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  client.print("signal strength (RSSI):");
  client.print(rssi);
  client.println(" dBm");
  
}
void writeData(String filename, String data){
  String dataString = Uhrzeit + ";" + data + ";";  
  // open the file. note that only one file can be open at a time,
  // so you have to close this one before opening another.
  File dataFile = SD.open(filename, FILE_WRITE);
  // if the file is available, write to it:
  if (dataFile) {
    dataFile.println(dataString);
    dataFile.close();    
  }
  // if the file isn't open, pop up an error:
  else {
    Serial.println("error opening " + filename);
  }
}

void setup_sd() {
  Serial.print("Initializing SD card...");
  // see if the card is present and can be initialized:
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    while (1);
  }
  printRoot();
  Serial.println("card initialized.");
}

void loop_sd() {
   
  writeData(actualFilenumber + "_temp1.txt", String(tempOutside));
  writeData(actualFilenumber + "_temp2.txt", String(tempOutside2));
  writeData(actualFilenumber + "_temp3.txt", String(tempOutside3));
  writeData(actualFilenumber + "_tmp.txt", String(tmp));
  writeData(actualFilenumber + "_CO2.txt", String(CO2));
  writeData(actualFilenumber + "_humi.txt", String(humi));
  writeData(actualFilenumber + "_moist1.txt", String(sensorValue));
  writeData(actualFilenumber + "_moist2.txt", String(sensorValue2));
  writeData(actualFilenumber + "_moist3.txt", String(sensorValue3));
  writeData(actualFilenumber + "_dist.txt", String(distance));
  writeData(actualFilenumber + "_air.txt", String(airValue));

}

void addPHeader(WiFiClient client){ 
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/plain");
  client.println("Connection: close");  // the connection will be closed after completion of the response
  //client.println("Refresh: 5");  // refresh the page automatically every 5 sec
  client.println(); 
  
}

void addHeader(WiFiClient client){ 
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println("Connection: close");  // the connection will be closed after completion of the response
  //client.println("Refresh: 5");  // refresh the page automatically every 5 sec
  client.println(); 
  
}

void addLink(WiFiClient client){ 
  String Link = R"=====(<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>)====="; 
  client.print(Link);
}
void addCanvas(WiFiClient client, String id){ 
  String First = R"=====(<canvas id=")=====";  
  
  String Second = R"=====(" style="width:100%;max-width:1800px"></canvas>
  )====="; 
  client.print(First);
  client.print(String(id));
  client.print(Second);
}
void loadCharts(WiFiClient client, String name){ 

 
  String First2 = R"=====(
     
      new Chart(")=====";

     String First3 = R"=====(", {
        type: "line",
        data: {
          labels: xValues,
          datasets: [{
            
  )=====";

  String Second = R"=====(
            
            borderColor: "red",
            fill: false
          }, { 
  )=====";

            
  String Third = R"=====(          
            borderColor: "green",
            fill: false
          }, { 
  )=====";          

  String Fourth = R"=====(          
            borderColor: "Orange",
            fill: false
          }, { 
  )=====";          
  String Fifth = R"=====(           
            borderColor: "blue",
            fill: false
          }]
        },
        options: {
          legend: {display: false}
        }
      });
      
    )=====";              
  
  client.print("<script>"); 
  client.print("const xValues = [");  
  readDataChartXM(client, actualFilenumber + "_temp1.txt");
  client.print("];");  
  client.print(First2);
  client.print(name);
  client.print(First3);  
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_temp1.txt");
  client.print("],");
  client.print(Second);
  //readDataChart2(client);
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_temp2.txt");
  client.print("],");
  client.print(Third);
  //readDataChart3(client);
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_temp3.txt");
  client.print("],");
  client.print(Fourth);
  //readDataChart4(client);
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_tmp.txt");
  client.print("],");
  client.print(Fifth);
  client.print("</script>"); 
}

void loadNewChart(WiFiClient client, String name){
 

      
  String First2 = R"=====(
    
      new Chart(")=====";

     String First3 = R"=====(", {
        type: "line",
        data: {
          labels: x2Values,
          datasets: [{
            
  )=====";

  String Second = R"=====(
            
            borderColor: "red",
            fill: false
          }, { 
  )=====";

            
  String Third = R"=====(          
            borderColor: "green",
            fill: false
          }, { 
  )=====";          
            
  String Fourth = R"=====(           
            borderColor: "blue",
            fill: false
          }]
        },
        options: {
          legend: {display: false}
        }
      });
     
    )=====";

  client.print("<script>"); 
  client.print("const x2Values = [");  
  readDataChartXM(client, actualFilenumber + "_moist1.txt");
  client.print("];");  
  client.print(First2);
  client.print(name);
  client.print(First3);  
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_moist1.txt");
  client.print("],");
  client.print(Second);
  //readDataChart2(client);
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_moist2.txt");
  client.print("],");
  client.print(Third);
  //readDataChart3(client);
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_moist3.txt");
  client.print("],");
  client.print(Fourth); 
  client.print("</script>"); 
}
void loadNewChart2(WiFiClient client, String name){
 

      
  String First2 = R"=====(
    
      new Chart(")=====";

     String First3 = R"=====(", {
        type: "line",
        data: {
          labels: x3Values,
          datasets: [{
            
  )=====";

  String Second = R"=====(
            
            borderColor: "red",
            fill: false
          }]
          }, options: {
          legend: {display: false}
        }
      }); 
  )=====";

  client.print("<script>"); 
  client.print("const x3Values = [");  
  readDataChartXM(client, actualFilenumber + "_CO2.txt");
  client.print("];");  
  client.print(First2);
  client.print(name);
  client.print(First3);  
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_CO2.txt");
  client.print("],");
  client.print(Second);  
  client.print("</script>"); 
}

void loadNewChart3(WiFiClient client, String name){
 
  String First2 = R"=====(
    
      new Chart(")=====";

     String First3 = R"=====(", {
        type: "line",
        data: {
          labels: x4Values,
          datasets: [{
            
  )=====";

  String Second = R"=====(
            
            borderColor: "red",
            fill: false
          }]
          }, options: {
          legend: {display: false}
        }
      }); 
  )=====";

  client.print("<script>"); 
  client.print("const x4Values = [");  
  readDataChartXM(client, actualFilenumber + "_humi.txt");
  client.print("];");  
  client.print(First2);
  client.print(name);
  client.print(First3);  
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_humi.txt");
  client.print("],");
  client.print(Second);  
  client.print("</script>"); 
}
void loadNewChart4(WiFiClient client, String name){
 
  String First2 = R"=====(
    
      new Chart(")=====";

     String First3 = R"=====(", {
        type: "line",
        data: {
          labels: x5Values,
          datasets: [{
            
  )=====";

  String Second = R"=====(
            
            borderColor: "red",
            fill: false
          }]
          }, options: {
          legend: {display: false}
        }
      }); 
  )=====";

  client.print("<script>"); 
  client.print("const x5Values = [");  
  readDataChartXM(client, actualFilenumber + "_dist.txt");
  client.print("];");  
  client.print(First2);
  client.print(name);
  client.print(First3);  
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_dist.txt");
  client.print("],");
  client.print(Second);  
  client.print("</script>");
}
void loadNewChart5(WiFiClient client, String name){
 

  String First2 = R"=====(
    
      new Chart(")=====";

     String First3 = R"=====(", {
        type: "line",
        data: {
          labels: x6Values,
          datasets: [{
            
  )=====";

  String Second = R"=====(
            
            borderColor: "red",
            fill: false
          }]
          }, options: {
          legend: {display: false}
        }
      }); 
  )=====";

  client.print("<script>"); 
  client.print("const x6Values = [");  
  readDataChartXM(client, actualFilenumber + "_air.txt");
  client.print("];");  
  client.print(First2);
  client.print(name);
  client.print(First3);  
  client.print("data: ["); 
  readDataChart(client, actualFilenumber + "_air.txt");
  client.print("],");
  client.print(Second);  
  client.print("</script>");
}
if (currentLine.endsWith("GET /CH")) {
          addHeader(client);
          addLink(client);
          client.println("<h1>Temperaturen</h1>");
          addCanvas(client, "myChart1");
          client.println("<h1>Bodenfeuchte</h1>");
          addCanvas(client, "myChart2");
          client.println("<h1>CO2</h1>");
          addCanvas(client, "myChart3");
          client.println("<h1>Luftfeuchte</h1>");
          addCanvas(client, "myChart4");
          client.println("<h1>Distanz</h1>");
          addCanvas(client, "myChart5");
          client.println("<h1>Luftqualitaet</h1>");
          addCanvas(client, "myChart6");                
          loadCharts(client, "myChart1");                
          loadNewChart(client, "myChart2");                 
          loadNewChart2(client, "myChart3");                 
          loadNewChart3(client, "myChart4");                 
          loadNewChart4(client, "myChart5");                
          loadNewChart5(client, "myChart6");
          // The HTTP response ends with another blank line:
          client.println();
          break;      
          
        }

Es gibt wahrscheinlich an jeder Verbesserungsmöglichkeiten...
Bei dem Nano 33 IOT werden ca. 1MB daten von SD Karte Stückweise gelesen und gesendet. Das Gleiche wollte ich jetzt mit dem ESP32 machen.

Sorry, habe ProgMem mit EEPROM verwechselt :worried:

Kennst Du das Dateisystem LittleFS des ESP32? Dort lege ich Stylesheet- und HTML-Dateien ab, wobei sich die HTML-Datei die Daten für die Grafik per JSON holt. Damit entfällt CSS und HTML im Programmtext. Bei Dir aber nicht entscheidend. JSON steht bei mir dann auch in einem String-Objekt.

Aber hast Du schonmal versucht, chart1.html in LittleFS abzulegen und dort aufzurufen?

Hallo
Grundsätzlich denke ich ja 4000 Datensätze ist schon mal eine Hausnummer , sollte man nicht auf dem ESP auswerten wollen.

Dennoch denke ich sollte das gehen .
Die Datensätze liegen zeilenweise in einer Datei auf dem ESP.
Eine statische Webseite auf dem ESP mit Skript und Fetch.
Dann die Daten jeweils 100-200 Zeilen auslesen und als Json versenden. Das Java Script auf der HTML Seite müsste dann entsprechend die Pakete zuordnen und in die Arrays für das Chart einbauen.

Gruß Heinz

Hallo, sowas mit Json habe ich mir jetzt nach weiterer Recherche auch überlegt. Mit dem Js-Scripting in der Html habe ich noch nicht soviel gemacht. Naja, mein Plan war jetzt die Daten gleich als Json String auf Sd speichern und ein canvasjs chart mit Zoom-Slider einzubauen. Mit dem Http-Request kann ich dann hoffentlich die ganze Datei von Sd auf einmal Senden. Wird aber noch viel Arbeit für mich.

Je nach Menge der Datenpunkte kann dieser Ansatz zu einem Speicherproblem führen oder Du streamst das file komplett.

Gruß Tommy