...

Prozess

Mit AsyncWebServer::on() wird ein Handler-Methode zur Bereitstellung der angeforderten Daten registriert. Diesem Handler wird ein Zeiger auf ein Objekt der AsyncWebServerRequest-Klasse übergeben. Eines der wesentlichen Aufgaben dieses AsyncWebServerRequest-Objekts ist es, die Verbindungsdaten zu verwalten.

Zur Verwaltung und Rückgabe der angeforderten Daten wird per AsyncWebServerRequest::beginResponse() ein AsyncWebServerResponse-Objekt erzeugt. Dieses kann bearbeitet und per AsyncWebServerRequest::send() verschickt werden. Mit Aufruf der Methode send() wird das AsyncWebServerResponse-Objekt zum Eigentum des AsyncWebServerRequest-Objekts und wird durch dessen Destruktor gelöscht.

Es gibt abkürzende Methoden, die den oben skizzierten Prozess implizit ausführen, z.B.

//Send index.htm with default content type
request->send(SPIFFS, "/index.htm");

Template-Handling

Die Rückgabe-Daten können Platzhalter in der Form "%template%" enthalten. Dem AsyncWebServerResponse-Objekt kann eine Callback-Methode mitgegeben werden, die zur Ersetzung eines jeden Platzhalters in den Rückgabedaten aufgerufen wird. Z.B. eine einfache Callback-Methode (processor) für die Rückgabe von Datei-Daten und deren Registrierung:

String processor(const String& var) {
  if(var == "HELLO_FROM_TEMPLATE")
    return F("Hello world!");
  return String();
}

// ...

//Send index.htm with template processor function
request->send(SPIFFS, "/index.html", String(), false, processor);

Wenn in "index.html" die Zeichenfolge "%HELLO_FROM_TEMPLATE% auftritt, wird die Callback-Methode processor aufgerufen. Der Methode wird der Platzhaltername (hier "HELLO_FROM_TEMPLATE", ohne Prozentzeichen) mitgegeben. Sie liefert als String die Daten zurück, durch die der Platzhalter ersetz werden soll. In der Request-Antwort würde also "Hello world!" übermittelt werden.

Solange, wie Platzhalter durch statische Daten ersetzt werden sollen, funktioniert dieses Verfahren einwandfrei. In vielen Fällen muss man jedoch volatile Daten für die Beantwortung des Request erst zusammenstellen. Ein Beispiel wäre der Einsatz einer gleichen Systemzeit (millis()) an verschiedenen Stellen der Rückgabe-Daten. Wird diese Zeit jedes Mal durch Abruf von millis() neu ermittelt, werden wergen des Zeitversatzes der einzelnen Aufrufe der Callback-Methode alle ersetzten Systemzeiten unterschiedlich ausfallen.

Man muss also die Daten vorab zusammenstellen und speichern. Dies kann nur innerhalb des Request-Handlers erfolgen. Die Ablage der Ersetzungsdaten (also die Systemzeit in obigem Beispiel) in statischen Variablen ist ungeeignet, da konkurrierende Aufrufe unvorhersehbaren Einfluss auf den Inhalt dieser Variablen haben können. Vornehmlich bei Transfer-Encoding: chunked können die Chunks unterschiedlicher Request vermischt beantwortet werden. Dieser Fakt ist insbesondere deshalb zu berücksichtigen, weil wegen alles Responses mit Template-Ersetzung chunked Responses sind!

Für die Verwaltung der Rückgabedaten ist das AsyncWebServerResponse-Objekt zuständig. Sinnvoll wäre es, diesem die vorbereiteten Daten anzuhängen. Leider gibt es jedoch hierfür keine direkte Möglichkeit. Mit etwas Aufwand kann man jedoch man ein entsprechendes Verfahren einbauen!

Schaut man sich den Quellcode der passenden Variante von AsyncWebServerRequest::beginResponse() findet man:

AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, 
                                                              const String& contentType, bool download, 
                                                              AwsTemplateProcessor callback){
  if(fs.exists(path) || (!download && fs.exists(path+".gz")))
    return new AsyncFileResponse(fs, path, contentType, download, callback);
  return NULL;
}

Das wesentliche Element ist die Erzeugung eines AsyncFileResponse-Objekts. Das kann man nachbauen!

Hier ein Beispiel für die AsyncFileResponse-Variante. Bei andere AsyncWebServerResponse-Typen kann man analog vorgehen.

Das SPIFFS enthält eine Datei mit dem Namen "timer.html", die per HTTP-Request abgefordert werden kann. Diese Datei enthält an mehreren Stellen den Platzhalter "%millis%", der einheitlich mit dem Wert von millis() zum Zeitpunkt des Abrufs gefüllt werden soll:

<!DOCTYPE HTML><html lang="de">

  <head>
    <title>Timer</title>
  </head>

  <body>
    <p>Dies ist der erste Platzhalter: %millis%.</p>
    <p>&nbsp;</p>
    <p>Dies ist der zweite Platzhalter: %millis%.</p>
  </body>
</html>

Zunächst wird eine neue Klasse von AsyncFileResponse abgeleitet, die in der Lage ist den aktuellen Wert von millis() aufzunehmen (Member-Variable current):

01: #include <FS.h>
02: #include <ESPAsyncWebServer.h>
03: 
04: 
05: class TimerResponse : public AsyncFileResponse {
06: public:
07:   TimerResponse(String path) : AsyncFileResponse(SPIFFS, path, String(), false,
08:                                       [this](const String& var)->String { return TimerProcessor(var); }) {}
09: 
10:   String TimerProcessor(const String & var) {
11:     if (var == "millis")
12:       return String(current);
13: 
14:     return String();
15:   }
16: 
17:   unsigned long current;
18: };

In Zeile 17 wird die zusätzliche Variable current definiert. Die Callback-Methode (Zeile 10 ff) muss eine Member-Funktion sein, damit sie auf die Klassen-Member zugreifen kann. Eine Member-Funktion kann aber nicht direkt als Callback-Funktion genutzt werden. Abhilfe schafft der Umweg über eine kleine Lambda-Funktion:

[this](const String& var)->String { return TimerProcessor(var); })

[this] fängt einen Zeiger auf das Objekt zu dem diese Funktion gehört und die Lambda-Funktion ist damit in der Lage eine Member-Funktion aufzurufen.

Als nächstes gilt es eine kleine Handler-Funktion zu schreiben, die eine Instanz der TimerResponse-K-Klasse erzeugt, sie mit Inhalt füllt und versendet:

void ndleTimerHTML(AsyncWebServerRequest * request) {
  if (!SPIFFS.exists("/timer.html")) {
    request->send(404);
    return;
  }

  TimerResponse *response = new TimerResponse("/timer.html");
  response->current = millis();
  request->send(response);
}

Als letztes muss der Handler beim Server registriert werden:

AsyncWebServer server(80);

...
server.on("/timer.html", HTTP_ANY, handleTimerHTML);
...

 

Fortsetzung folgt