Rothschopf Werner

rowex ganz privat

Angebote

Arduino Webserver: POST Daten empfangen

Wenige Beispiele findet man zum Thema: "Empfang von Daten die mittels POST gesendet wurden", da sich die meisten Arduino Beispiele auf GET (also dem Anhängen von Parametern mittels ? an den URI) gehen.

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.htm?variable=1&nochein=Text HTTP/1.1
HOST: 172.18.67.93
Content-Length: 0

<!doctype html>
<html>

Bei POST werden die übermittelten Daten im http-body übertragen. Der http-header ist vom http-body durch eine Leerzeile getrennt.

GET /form.htm HTTP/1.1
HOST: 172.18.67.93
Content-Length: 24

variable=1&nochein=Text

Die Übertragung erfolgt gleich, nur das ? ist nicht notwendig.

Für unseren Sketch heißt das, wir müssen nicht nur die erste Zeile lesen, sondern den ganzen http-header durchgehen. Wir müssen erkennen wann eine Leerzeile kommt und erst die folgenden Daten interpretieren.

Das Beispiel Ethernet | Webserver

... und warum es problematisch 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, sich 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.

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:

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 }; // you can change the MAC and IP addresses to suit your network
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}; // define the LED pins. Don't use 0 (RX), 1(TX), 10 (SS ETH), 11 (MOSI), 12(MISO), 13(CLK). Be carefull with 4(SS SD).

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(F("HTTP/1.0 200 OK\r\n" // \r\n Header Fields are terminated by a carriage return (CR) and line feed (LF) character sequence.
    "Content-Type: text/html\r\n" // The Media type of the body of the request (used with POST and PUT requests).
    "\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" // a minimum style to avoid serifs
    "<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(F("<p>"));
    client.print(pin);
    client.print(F(" <input name='pinD"));
    client.print(pin);
    client.print(F("' type='submit' value='On'>"));
    client.print(F("<input name='pinD"));
    client.print(pin);
    client.print(F("' type='submit' value='Off'>"));
    if (digitalRead(pin)) client.print(F(" active"));
    client.print(F( "<br>\n"));
  }
  client.print (F("</form>\n"));
  client.print (F("</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:

Zum Auslesen des HTTP Headers und Bodies setzen wir eine state machine ein. Um magic number zu vermeiden verwenden wir 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;

und dann lesen wir Zeichen für Zeichen in unseren lineBuffer ein. Nach jeder Zeilenschaltung behandeln wir 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. Uns interessieren nur die Methode, der URI und eventuelle Parameter. Zum Zerteilen verwenden wir 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 erkennen wir an einer 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 kann man 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 unsere 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 von uns. In unseren konkreten Beispiel senden wir ihm 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 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 behandeln wir besser ordnungsgemäß mit einem http Fehler 404:

else 
  send404(client);

und eine ganz einfache Seite dazu:

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

Beispielhaft senden wir eine reine Textseite ohne HTML zurück. Alles kein Problem, solange wir den Content-Type richtig setzen.

Das favicon.ico mit dem Arduino webserver behandeln

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

Das ist schon besser als die ganze Seite noch einmal, aber das geht sicher schöner.
Vor dem else für 404 noch ein else if und eine weiteren Funktionsaufruf hinzugefügt:

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(F("HTTP/1.0 204 no content\r\n"));
  client.stop();
}

Jetzt siehts im Network Monitor auch wieder OK aus:

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.

Ein echtes favicon.ico für den Arduino webserver

Gehts besser? Klar, geht immer besser. Eine Variante ist es die Bilddaten in Base64 zu konvertieren und im HTML Header mit der Seite mit auszuliefern. Beispiele dazu findet man auch für den Arduino.

Man könnte es auch auf die Spitze treiben und ein Favicon am Arduino Webserver zur Verfügung stellen. Da man für die Bildvariante nur wenige Beispiele findet, hier die Bilddatei-Variante für das Favicon.

Zunächst brauchen wir beim URI vergleich wieder einen separaten else if und einen Funktionsaufruf:

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

Kennen wir schon.

Dann brauchen wir noch eine neue Funktion sendFavicon(). Der HTTP Content-Type für ein favicon.ico ist

Content-Type: image/x-icon

Nur wie komme ich zu den Bilddaten? Mein Workflow ist folgender

  • Zunächst erstelle ich online ein Favicon und lade es runter
  • Anschließend nutze ich einen online hexeditor und lasse mir die Bilddaten in hex konvertieren.
  • Die Bilddaten kopiere ich in notepad++ und ersetze alle blanks durch Beistriche.

Links zu beispielhaften Tools gibts am Seitenende.

Die in Hex Konvertierten Bilddaten kann man nun in ein byte Array am Arduino ablegen. Bitte darauf achten, dass in der kleinsten Auflösung von 16x16 Bildpunkten bereits 257 Bytes notwendig sind und da kann es im RAM eines Arduino UNO schon knapp werden. Daher verschieben wir das ganze Array in den PROGMEM (flash) und lesen im Bedarfsfall die Daten aus dem Programmspeicher aus.

void sendFavicon(EthernetClient &client)
{
  const static byte tblFavicon[] PROGMEM = {
  0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x10, 0x10, 0x10, 0x00, 0x01, 0x00, 0x04, 0x00, 0x28, 0x01,
  0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00,
  0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0xa8, 0xa8, 0xa8, 0x00, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11,
  0x11, 0x11, 0x11, 0x11, 0x11, 0x10, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
  0x12, 0x22, 0x12, 0x22, 0x11, 0x11, 0x11, 0x11, 0x11, 0x12, 0x11, 0x12, 0x11, 0x11, 0x11, 0x11,
  0x12, 0x22, 0x11, 0x22, 0x11, 0x11, 0x11, 0x11, 0x12, 0x12, 0x11, 0x12, 0x11, 0x11, 0x11, 0x11,
  0x12, 0x22, 0x12, 0x22, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
  0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x11, 0x21, 0x12, 0x11, 0x22, 0x11, 0x12, 0x11,
  0x21, 0x21, 0x12, 0x12, 0x11, 0x21, 0x12, 0x11, 0x21, 0x21, 0x22, 0x12, 0x11, 0x21, 0x12, 0x11,
  0x21, 0x22, 0x12, 0x12, 0x11, 0x21, 0x12, 0x11, 0x21, 0x21, 0x12, 0x11, 0x22, 0x11, 0x11, 0x11,
  0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x01, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x10, 0x80, 0x01,
  0x00
  };

  client.print(F("HTTP/1.0 200 OK\r\n"
  "Content-Type: image/x-icon\r\n"
  "\r\n"));
  for (uint16_t i = 0; i < sizeof(tblFavicon); i++)
  {
    byte p = pgm_read_byte_near(tblFavicon + i);
   client.write(p);
  }
  client.stop();
}

So passt das favicon auch in einen Arduino UNO rein! Auch der Firefox zeigt das Favicon nun an:

und im Network Monitor ist auch alles ok:

Da das Icon als separate Datei vorliegt, könnte ein Browser das favicon auch cachen und müsste nicht bei jedem Request wieder angefordert werden - nur machen das die wenigsten Browser. Am Verlauf sieht man auch, das Favicon wird nach Erhalt der Seite "geladen". Die HTML Seite ist daher schlanker und kann schneller im Browser angezeigt werden.

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 können sogar dem Browser ein Favicon zur Verfügung stellen.

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


Links


Protokoll

First upload: 2020-01-19 | Version: 2020-07-18