Arduino Webserver: POST Daten empfangen

Wenige Beispiele findet man zum Thema: "Empfang von Daten die mittels POST gesendet wurden", da die meisten Arduino Beispiele auf GET (also dem Anhängen von Parametern mittels ? an den URI) behandeln. GET ist aber eigentlich die falsche Methode zur Übertragung von Daten. Eigentlich erfolgt das Übertragen von Daten mittels POST.

Nach einer kurzen Einleitung zu den HTTP Methoden POST und GET, analysieren wir das IDE Beispiel, erstellen einen hoffentlich besseren Sketch und statten diesen noch mit ein paar Besonderheiten aus. Für das durcharbeiten wirst du etwa 45 Minuten benötigen, dafür erhältst du aber einen wirklich sauber funktionierenden Arduino webserver.

Eins noch vorausgeschickt: Der Webserver funktioniert in dieser Form gut für Arduino UNO/Nano/Pro Mini und Arduino MEGA. Für den ESP8266/ESP32 gibt es entsprechende Libraries und man muss das nicht alles zu Fuß machen.

Unterschied POST vs GET

Wenn du diese Seite gefunden hast, sind dir die Unterschiede zwischen POST und GET wahrscheinlich bekannt, daher nur das Wesentliche kurz zusammengefasst.

Bei GET werden Parameter nach einem ? direkt nach dem aufzurufenden URI angehängt (und das sieht auch der Anwender in seinem Browser-Adressfeld)

Der http-header request beginnt beispielsweise mit

GET /form.php?variable=1&nochein=Text HTTP/1.1
HOST: 172.18.67.93
Content-Length: 42

<!doctype html>
<html>

Bei POST werden die übermittelten Daten im HTTP-Body übertragen. HTTP-Header ist vom HTTP-Body durch eine Leerzeile getrennt.

POST /form.php HTTP/1.1
HOST: 172.18.67.93
Content-Length: 24

variable=1&nochein=Text

Die Übertragung erfolgt gleich, das ? entfällt bei POST.

Für den Sketch bedeutet dies, du musst nicht nur die erste Zeile lesen, sondern den ganzen HTTP-Header durchgehen. Du musst erkennen wann eine Leerzeile kommt und erst danach die folgenden Daten interpretieren.

Das Beispiel Ethernet | Webserver Arduino webserver

... und warum es suboptimal ist

Das in der IDE mitgelieferte Beispiel Ethernet | Webserver ist nur eine Minimal-Variante eines Webservers. Jede Methode, jede Eingabe wird akzeptiert und führt zur Ausgabe einer Webseite. Viele Arduino Webserver Beispiele im Internet verwenden daher auch (fälschlicherweise) die GET Methode zur Datenübernahme.

Weiters empfehle ich, z.B. im Firefox unter Tools | Web Developer | Netzwerk den verursachten Traffic anzusehen. Firefox (und andere Browser), fordern nicht nur die Seite aus der Adresszeile an, sondern auch ein favicon.ico - eine kleine Grafik die - wenn vorhanden - auf der Seitenlasche dargestellt wird.

Arduino webserver

Der originale Webserver interpretiert die Anforderung nach dem favicon.ico als weitere Seitenanforderung und schickt somit jedes mal die Seite zweimal an den Browser. Die hereinkommenden Requests sieht man auch im Serial Monitor:

Arduino webserver

Ziele für unseren Webserver mit POST Funktionalität

Für unseren eigenen POST fähigen Webserver stecken wir uns folgende Ziele

  • Daten lesen die mittels POST übermittelt wurden
  • Response abhängig vom Client-Request
  • falsche Seiten sollen zu einem HTTP 404 führen
  • Das favicon.ico soll behandelt werden
  • Keine Verwendung von Arduino-Strings
  • den loop() sauber halten

Umsetzung eines Arduino Webservers mit POST

Zum Versenden von POST Daten verwenden wir gleich den Arduino selbst und hosten darauf ein Eingabeformular das Daten an den Arduino mittels POST sendet. Theoretisch könnte aber auch ein anderer Client unserem Arduino Webserver Daten mittels POST übermitteln.

Zur konkreten Umsetzung machen wir daher einen einfachen Arduino Webserver, mit einem POST Formular, das einige LEDs mittels Buttons ein- und ausschalten kann.

Das Setup() kann man im wesentlich vom IDE Beispiel übernehmen. Der loop() beinhaltet nur den laufenden Funktionsaufruf der für unseren webserver verantwortlich ist:

#include <SPI.h>
#include <Ethernet.h>

byte mac[] { 0x52, 0x64, 0x75, 0x69, 0x6E, 0x6F };
IPAddress ip( 172, 18, 67, 93 );
EthernetServer server(80); // Port 80 is HTTP port
const byte ledPin[] {2, 3, 4, 5, 6, 7, 8, 9};

void setup()
{
  Serial.begin(115200);
  Serial.println(F("\nsimple webserver"));
  for (auto &pin : ledPin) {
    pinMode(pin, OUTPUT); // Set the digital pins ready to write to
  }
  Ethernet.begin(mac, ip); // Start the Ethernet shield:
  printHardwareInfo();
  server.begin(); // Start the webserver:
  Serial.print(F("server available at http://"));
  Serial.println(Ethernet.localIP());
}

void loop()
{
  checkForClient();
}

Das ist eigentlich schon alles. MAC und IP Adresse festlegen, ein Array mit unseren GPIOs für die LEDs, das Festlegen derselbigen als Output, und das Netzwerk-Shield starten.

Nun legen wir in der IDE einen neuen Tab mit dem Namen "server" an. In diesen Tab kommt alles aus dem loop() des IDE Beispiels in unsere neue Funktion

void checkForClient();
{
  // alles mal aus dem Beispiel übernehmen
}

Testweise kann man das kompilieren und hochladen - soll noch immer alles funktionieren.

Unsere eigene HTML Seite am Arduino UNO/MEGA

Unsere eigene HTML Seite - ganz schnörkellos - geben wir in eine eigene Funktion. Vor dem HTTP-Body brauchen wir noch den HTTP-Header mit Returncode 200. Da wir keine Content-Length senden, verwenden wir HTTP/1.0. Nach der Leerzeile kommt der HTTP Body mit dem HTML. Es ist eine schlichte HTML5 Seite mit einem Eingabeformular und zwei Buttons für jeden PIN aus dem ledPIN Array.

void sendPage(EthernetClient &client)
{
  // Serial.println("[server] 200 response send");
  client.println( ("HTTP/1.0 200 OK\r\n"
    "Content-Type: text/html\r\n" 
    "\r\n" // a blank line to split HTTP header and HTTP body
    "<!doctype html>\n" // the HTTP Page itself
    "<html lang='en'>\n"
    "<head>\n"
    "<meta charset='utf-8'>\n"
    "<meta name='viewport' content='width=device-width'>\n"
    "<title>Webserver as pin controller</title>\n"
    "</head>\n"
    "<body style='font-family:Helvetica, sans-serif'>\n" 
    "<h1>Webserver as pin controller</h1>\n"
    "<p>Buttons turn pins on or off</p>\n"
    "<form method='post' action='/' name='pins'>"));
  for (auto &pin : ledPin)
  {
    //client.print( ("<p>"));
    client.print(pin);
    client.print( (" <input name='pinD"));
    client.print(pin);
    client.print( ("' type='submit' value='On'>"));
    client.print( ("<input name='pinD"));
    client.print(pin);
    client.print( ("' type='submit' value='Off'>"));
    if (digitalRead(pin)) client.print(F(" active"));
    client.print( ( "<br>\n"));
  }
  client.print ( ("</form>\n"));
  client.print ( ("</body>\n</html>"));
  client.stop();
}

(Der body style wäre nicht notwendig, aber ich mag Serifen-Schriften nicht besonders).

Aussehen wird das ganze dann ca so:

Arduino webserver

Zum Auslesen des HTTP-Headers und Bodies setzen wir eine state machine ein. Um magic number zu vermeiden verwendest du am besten ein enum class mit diesen 4 Status:

  • REQUEST ... wir lesen Methode, URI und evtl. GET Parameter
  • CONTENT_LENGTH ... wir lesen die Request-Länge
  • EMPTY_LINE ... wir erwarten die Leerzeile erkannt
  • BODY ... wir lesen die Daten aus dem HTTP body ein

umgesetzt so:

enum class Status {REQUEST, CONTENT_LENGTH, EMPTY_LINE, BODY};
Status status = Status::REQUEST;

Damit liest du Zeichen für Zeichen in den lineBuffer ein. Nach jeder Zeilenschaltung behandelst du die übernommene Zeile gem. Status.

while (client.connected()) {
  while (client.available()) {
    char c = client.read();
    Serial.print(c); 
    if ( c == '\n' )
    {
      if (status == Status::REQUEST)
      {
        status = Status::EMPTY_LINE; 
      }
      else if (status == Status::CONTENT_LENGTH)
      {
        status = Status::EMPTY_LINE;
      }
      else if (status > Status::REQUEST && i <= 2) // check if we have an empty line
      {
        status = Status::BODY;
      }
      else if (status == Status::BODY)
      {
        strlcpy(postParameter, lineBuffer, smallbuffersize);
        break; // we have received one line payload and break out
      }
      i = 0;
      strcpy(lineBuffer, "");
    }
    else
    {
    if (i < buffersize)
    {
      lineBuffer[i] = c;
      i++;
      lineBuffer[i] = '\0';
    }
  // MISSING wenn status 3 und content-length --> abbrechen.
  }
}
if (status == Status::BODY) // status 3 could end without linefeed, therefore we takeover here also
{
  strlcpy(postParameter, lineBuffer, smallbuffersize);
}

Etwas komplexer ist es, die erste Zeile zu interpretieren. Die HTTP Request beginnt mit der Methode, ein Blank trennt den URI mit eventuellen Parametern und abschließend wieder ein blank und die Unterscheidung ob es sich um einen HTTP/1.0 oder HTTP/1.1 Request handelt. Zunächst interessiert nur die Methode, der URI und eventuelle Parameter. Zum Zerteilen verwendest du die C++ Funktion strtok:

if (status == Status::REQUEST) // read the first line
{
  //Serial.print(F("lineBuffer="));Serial.println(lineBuffer);
  // now split the input
  char *ptr;
  ptr = strtok(lineBuffer, " "); // strtok willdestroy the newRequest
  strlcpy(method, ptr, smallbuffersize);
  Serial.print(F("method=")); Serial.println(method);
  ptr = strtok(NULL, " ");
  strlcpy(uri, ptr, smallbuffersize); // enthält noch evtl. parameter
  if (strchr(uri, '?') != NULL)
  {
    ptr = strtok(uri, "?"); // split URI from parameters
    strcpy(uri, ptr);
    ptr = strtok(NULL, " ");
    strcpy(requestParameter, ptr);
    Serial.print(F("requestParameter=")); Serial.println(requestParameter);
  }
  Serial.print(F("uri=")); Serial.println(uri);

Die Leerzeile erkennst du an einem LF an den ersten 2 Stellen. HTTP trennt per Definition die Header-Request Felder tatsächlich mit CR+LF (!)

      else if (status > Status::REQUEST && i < 2) // check if we have an empty line
      {
        status = Status::BODY;
      }

Die Parameter aus dem Body kannst du wieder mit strtok teilen oder wenn sie eindeutig genug sind auch mit strncmp durchsuchen:

if ( strncmp( postParameter, "pinD", 4) == 0 ) {
  byte pin = postParameter[4] - 48; // Convert ascii to int
  if ( strncmp( postParameter + 5, "=On", 3) == 0 ) {
    digitalWrite(pin, 1);
  }
  else if ( strncmp( postParameter + 5, "=Off", 4) == 0 ) {
    digitalWrite(pin, 0);
  }
}

Das war schon die ganze Datenübernahme mittes POST: je nach dem welcher Button gedrückt wurde, wird der Pin ein oder ausgeschaltet.

Der Browser erwartet aber noch eine Antwort vom Server. In diesem konkreten Beispiel sendet der Server die Seite retour. Da sich möglicherweise einzelne Pins geändert haben, macht das auch Sinn. Im Regelfall reicht der Vergleich auf "/", aber hier liese sich auch auf eine andere Seiten prüfen (z.B. index.htm):

 // send back a response

if (!strcmp(uri, "/") || !strcmp(uri, "/index.htm"))
  sendPage(client);

Fein. Aber wenn der User eine falsche URI eingibt, läuft der Browser in ein Timeout. Daher behandelst du das besser ordnungsgemäß mit einem HTTP Fehler 404:

else 
  send404(client);

und eine ganz einfache Seite dazu:

void send404(EthernetClient &client)
{
  client.println( ("HTTP/1.0 404 Not Found\r\n"
                   "Content-Type: text/plain\r\n"
                   "\r\n"
                   "File Fot Found\n"));
  client.stop();
}

Beispielhaft sendet der Server eine reine Textseite (ohne HTML Formierung) zurück. Alles kein Problem, solange du den Content-Type (text/plain) richtig setzt.

Das favicon.ico mit dem Arduino webserver behandeln

Ja da war doch noch etwas. Genau das favicon.ico. Aktuell würde der Browser statt dem favicon.ico einen HTTP Fehler 404 bekommen:

Arduino webserver

Das ist schon besser als die ganze Seite noch einmal, aber das geht sicher schöner.
Vor dem else für die Seite 404 ergänzt du noch ein else if und einen neuen Funktionsaufruf:

else if (!strcmp(uri, "/favicon.ico")) // a favicon
  send204(client); 

Eine Seite 204 ist eine leere Seite ohne Content. Das sagt dem Browser "ich habe deinen (GET) Request verarbeitet - Ende und aus". Die Funktion ist daher nur ein Zweizeiler:

void send204(EthernetClient &client)
{
  client.println( ("HTTP/1.0 204 no content\r\n"));
  client.stop();
}

Jetzt sieht es im Network Monitor auch wieder OK aus:

Arduino webserver

Meiner Meinung nach ist Response Code 204 für das favicon.ico ein guter Kompromis für den Arduino Uno webserver - wir haben ja nicht endlos Speicher zur Verfügung.

Weitere Optimierungen

Der hier dargestellte Webserver läuft stabil, dennoch kann man noch weitere Optimierungen vornehmen. Auch die Ausgabe eines echten Favicon.ico am Arduion webserver (en) ist möglich.

Zusammenfassung

Wir haben nicht nur Daten mittels POST am Arduino empfangen, sondern auch gezeigt, wie man einen einfachen Webserver am Arduino UNO programmieren kann. Der Server schickt nur Daten die angefragt wurden, wir behandeln 404 Fehler und behandeln das Favicon für den Browser korrekt.

Fragen zum Webserver stellt man am Besten im deutschsprachigen Forum von arduino.cc.

Links

Protokoll

First upload: 2020-01-19 | Version: 2020-12-03