Statischen HTML-Code für Webserver dynamisch verändern.

Hi,

ich habe eine HTML-Seite für einen kleinen Webserver.
Die Zeichenfolge der HTML-Seite habe ich in einem raw string literal gespeichert.
Ähnlich wie in dem Beispiel:

const char index_html[] PROGMEM = R"=====(
<!DOCTYPE HTML>
<html>
<head><title>ESP8266 Arduino Demo Page</title></head>
<body>ESP8266 power!<p><img src="logo.png"></body>
</html>
)=====";

Damit kann ich eine extern (notepad++) erstellte Webseite easy mit copy/paste in die Arduino IDE bringen, ohne am Ende jeder Zeile noch ein ‘’ einfügen zu müssen.
Für statische Seiten klappt das prima.
Aber ich würde gerne innerhalb der HTML-Seite z.B. Temperaturwerte darstellen.
Bis dato hab ich dann immer den statischen Teil an der entsprechenden Stelle getrennt, die Variable eingesetzt, und dann wieder den statischen Folgeteil der Seite angehängt.
Ungefähr so:

          client.print(F("<tr><td colspan='2'>Durchfluss</td><td background-color:#9EC1C4'>"));
          client.print(Vol,1);
          client.print(F(" l/min</td> </tr>"));

Seht ihr eine machbare Möglichkeit, die komplette Seite vor dem Senden irgendwie zu modifizieren, dass z.B. der aktuelle Durchfluss eingetragen wird, und ich alles in einem Rutsch rausschicken kann?

Ich hoffe ich habe es einigermassen verständlich beschrieben. :confused:

hk007:
Seht ihr eine machbare Möglichkeit, die komplette Seite vor dem Senden irgendwie zu modifizieren, …

Spontan fällt mir dazu nur ein: Programmiere eine Suchen-und-Ersetzen-Methode für Strings (falls es das nicht schon gibt) und platziere in der Vorlage an der richtigen Stelle einen Platzhalter, der dank der S-u-E-Methode durch den Wert ersetzt wird.

HTH

Gregor

Nachtrag: Das String-Objekt hat eine Funktion, die Dir gefallen dürfte. Siehe hier

gregorss:
Nachtrag: Das String-Objekt hat eine Funktion, die Dir gefallen dürfte. Siehe hier

Klappt aber nicht mit nem char-array

Das geht auch. Man könnte erst mal den String (oder einen Teil von) per strcpy_P() aus dem Flash in einen Puffer im RAM kopieren. Dann kann man eine bestimmte Stelle in dem Puffer als Ziel für eine Konvertierungsfunktion verwenden.

Du musst halt dafür sorgen dass deine eingesetzten Werte auf eine konstante Breite formatiert werden. Also wenn du als Platzhalter sagen wie mal “*****” hast, dann passt da ein 5-stelliger Integer rein. Oder ein Float mit 2 Vorkommastellen, 1 Nachkommastelle + Vorzeichen. Da muss man abfragen dass die Zahl, bzw. der erzeuge String nicht größer ist und wenn er kleiner ist mit Nullen oder Leerzeichen auffüllen

Ich bin mal den anderen Weg gegangen, und anstelle des

const char html_page1[] PROGMEM = R"=====(

einen String genommen:

String html_page1 = R"=====(

Aber dann ists nichts mehr mit PROGMEM:

html_page1 causes a section type conflict with __c

Das mit der konstanten Breite ist klar :wink:

Du hast ein 'R' dass da nichts zu suchen hat...

PROGMEM kann schon praktisch sein wenn du mehrere Seiten speichern willst. Man braucht dann immer noch einen RAM Puffer, aber den können sich mehrere Seiten teilen. Kann man wie gesagt ganz einfach mit strcpy_P() oder besser strncpy_P() vom Flash ins RAM kopieren.

Serenifly:
Du hast ein 'R' dass da nichts zu suchen hat...

Sicher? Siehe Anfangspost.

Serenifly:
PROGMEM kann schon praktisch sein wenn du mehrere Seiten speichern willst. Man braucht dann immer noch einen RAM Puffer, aber den können sich mehrere Seiten teilen. Kann man wie gesagt ganz einfach mit strcpy_P() oder besser strncpy_P() vom Flash ins RAM kopieren.

Du hast recht. Egal ob string (char array) oder String-object, ich werde ihn wohl immer ins RAM kopieren müssen, um etwas zu ändern.

EDIT:
Nochmal zur konstanten Breite der Ersetzung.
Nach meinen Tests sieht es so aus, als ob das 'String replace' da dynamisch ist.

Serial.println(html_page1.length());
  html_page1.replace("Hund","HundKatzeMaus");
  Serial.println(html_page1.length());

ergibt einen um 9 Zeichen längeren String nach dem replace.

Meiner Ansicht ist der richtige Weg man speichert Stücke des HTML-Code ab, zwischen denen man die variablen teile ausgibt. Alles andere braucht zuviel RAM.

ergibt einen um 9 Zeichen längeren String nach dem replace.

Hast Du den effektiven RAM Verbrauch vor und nach dem replace kontrolliert?

Grüße Uwe

hk007:
Klappt aber nicht mit nem char-array

Richtig. Deshalb nimmt man ja auch ein String-Objekt.

Gruß

Gregor

Oder

gregorss:
Richtig. Deshalb nimmt man ja auch ein String-Objekt.

Oder man nimmt ein char Array:

const int WIDTH = 5;
const int FLOAT_PREC = 1;

const char test[] PROGMEM = "blah ***** blah ***** blah ***** blah";

void setup()
{
  Serial.begin(9600);
  delay(1000);
  char buffer[10];
  memset(buffer, 0, sizeof(buffer));

  format(buffer, -1);
  Serial.println(buffer);
  format(buffer, -12);
  Serial.println(buffer);
  format(buffer, -123);
  Serial.println(buffer);
  format(buffer, -1234);
  Serial.println(buffer);
  format(buffer, 1);
  Serial.println(buffer);
  format(buffer, 12);
  Serial.println(buffer);
  format(buffer, 123);
  Serial.println(buffer);
  format(buffer, 1234);
  Serial.println(buffer);
  format(buffer, 12345);
  Serial.println(buffer);

  Serial.println();

  format(buffer, 0.0);
  Serial.println(buffer);
  format(buffer, 1.5);
  Serial.println(buffer);
  format(buffer, 10.5);
  Serial.println(buffer);
  format(buffer, 100.5);
  Serial.println(buffer);
  format(buffer, -1.5);
  Serial.println(buffer);
  format(buffer, -10.5);
  Serial.println(buffer);
  format(buffer, -100.0);
  Serial.println(buffer);

  Serial.println();

  replace();
}

void loop()
{
}

void replace()
{
  char buffer[50];
  strncpy_P(buffer, test, sizeof(buffer));
  char* ptr = buffer;

  format(findNextPlaceholder(ptr), 123);
  format(findNextPlaceholder(ptr), 30.5);
  format(findNextPlaceholder(ptr), -12);

  Serial.println(buffer);
}

char* findNextPlaceholder(char*& ptr)
{
  char* tmp = strchr(ptr, '*');
  ptr += WIDTH;
  return tmp;
}

bool format(char* buffer, int number)
{
  bool success = false;
  char buf[10];

  itoa(number, buf, 10);
  int length = strlen(buf);

  if (length <= WIDTH)
  {
    int i;
    for (i = 0; i < WIDTH - length; i++)
      buffer[i] = ' ';
    for (int j = 0; i < WIDTH; j++)
      buffer[i++] = buf[j];

    success = true;
  }
  else
    memset(buffer, '0', WIDTH);

  return success;
}

bool format(char* buffer, double number)
{
  bool success = false;
  char buf[10];

  dtostrf(number, WIDTH, FLOAT_PREC, buf);
  int length = strlen(buf);

  if (length <= WIDTH)
  {
    for (int i = 0; i < WIDTH; i++)
      buffer[i] = buf[i];

    success = true;
  }
  else
    memset(buffer, '0', WIDTH);

  return success;
}

Die Ausgaben am Anfang von setup() sind nur zum Test der Format-Funktionen

Geht davon aus dass führende Leerzeichen ok sind. Wenn man führende Nullen braucht, muss man bei negativen Zahlen etwas weiter ausholen. Ist aber auch kein Problem

Nachtrag:
Gesehen dass R für “raw string” steht. Das ist ein C++11 feature. Mit C++11 habe ich mich etwas beschäftigt, aber das hatte ich irgendwie übersehen :slight_smile:

Als Befürworter von String - Objekten sollte man mit Tips vorsichtig sein,
da man sich viele Feinde macht :wink:

Wie du schon richtig gemerkt hast, kann es natürlich keine PROGMEM - Strings geben.

Das mit dem Ersetzen von "#####" durch eine gleichbreite Zahl hat den Nachteil, dass beim Umkopieren vom Flash in RAM-Blöcke dieser Teilstring schonmal über eine Block-Grenze gehen könnte.

Eher: ein einzelnes Sonderzeichen erkennen, abschneiden, dyn. Teil separat ausgeben, mit dem statischen Teil weitermachen.

michael_x:
Das mit dem Ersetzen von "#####" durch eine gleichbreite Zahl hat den Nachteil, dass beim Umkopieren vom Flash in RAM-Blöcke dieser Teilstring schonmal über eine Block-Grenze gehen könnte.

Falls du meinst, dass die zu Zahl die eingesetzt wird länger als der Platzhalter ist. Das frage ich ab. Und schreibe dann "00000" rein. Der Wert geht dann natürlich verloren, aber der String wird nicht zerstört.

Praktisch sollte man aber auch wissen welchen Wertebereich man erwartet.

Eher: ein einzelnes Sonderzeichen erkennen, abschneiden, dyn. Teil separat ausgeben, mit dem statischen Teil weitermachen.

Das ist natürlich die bessere Lösung, da man keinen temporären Puffer braucht und sich nicht um die Formatierung sorgen muss. Und auch nicht sehr kompliziert. :slight_smile:

Einfach per pgm_read_byte() das nächste Zeichen auslesen, vergleichen und dann entweder das Zeichen senden oder eine Variable.

Ich habe mal auf die Schnelle eine variadische Funktion dafür geschrieben. Damit kann man bequem eine beliebige Anzahl an Parametern mit verschiedenen Typen übergeben.

Als Platzhalter ist hier * für int und # für float/double

Wenn die Platzhalter nicht mit den Typen übereinstimmen kommt aber Murks raus. Wie bei printf() auch

const char test[] PROGMEM = "blah * blah # blah * blah";

void replace(const char* str, ...);

void setup()
{
  Serial.begin(9600);
  delay(1000);

  replace(test, 123, 45.5, -100);
}

void loop()
{
}

void replace(const char* str, ...)
{
  va_list args;
  va_start(args, str);

  while (char c = pgm_read_byte(str++))
  {
    if (c == '*')
    {
      Serial.print(va_arg(args, int));
    }
    else if (c == '#')
    {
      Serial.print(va_arg(args, double));
    }
    else
    {
      Serial.print(c);
    }
  }

  va_end(args);
}

Nachtrag:

ist allerdings kein guter Platzhalter für HTML. Da muss man natürlich Zeichen nehmen die sonst nicht vorkommen.

Hi,

danke für die Anregungen. Bin mom etwas busy. Werd mir das bei Gelegenheit genauer anschauen.

Mit C++11 und variadischen templates kann man das auch typsicher schreiben :slight_smile:

Das hier in einen Header namens “Replace.h”:

#include "Arduino.h"

template<typename T, typename U>   //Typen ungleich, gibt 0 zurück
struct is_same
{
  enum { value = 0 };
};

template<typename T>   //Typen gleich, gibt 1 zurück
struct is_same < T, T >
{
  enum { value = 1 };
};

void replace(const char* str)    ///Rest des Strings ausgeben wenn keine Parameter mehr vorhanden sind
{
  while (char c = pgm_read_byte(str++))
    Serial.print(c);
  Serial.println();
}

template<typename T, typename... Targs>
void replace(const char* str, T arg, Targs... Fargs)
{
  while (char c = pgm_read_byte(str))
  {
    if (c == '*')
    {
      if (is_same<int, T>::value || is_same<long, T>::value)
        Serial.print(arg);     //Parameter ausgeben wenn er vom erwarteten Typ ist
      else
        Serial.print(c);    //ansonsten Zeichen ausgeben

      replace(str + 1, Fargs...);  //Funktion rekursiv aufrufen um den nächsten Parameter zu behandeln
      return;
    }
    else if (c == '%')
    {
      if (is_same<double, T>::value)
        Serial.print(arg);
      else
        Serial.print(c);

      replace(str + 1, Fargs...);
      return;
    }
    else
    {
      Serial.print(c);
      str++;
    }
  }
}

Test Code:

#include "Replace.h"

const char test1[] PROGMEM = "blah * blah % blah * blah";
const char test2[] PROGMEM = "blah % blah % blah";
const char test3[] PROGMEM = "blah % blah % blah *";

void setup()
{
  Serial.begin(9600);
  delay(1000);

  replace(test1, 100, 45.2, 200);
  replace(test2, -5.5, -1.0);
  replace(test3, 5.0, 1.0, 10);
}

void loop()
{
}

Braucht etwas mehr Flash als die reine C Version oben. Außerdem verbraucht jede Version des Templates etwas mehr Flash, da z.B. <int, float, int> und <int, int> zwei verschiedene Funktionen sind. Aber es ist nicht viel. Ein paar hundert Bytes insgesamt.

Das Ergebnis sieht auf den ersten Blick gleich aus, aber schreib mal wo du einen Integer erwartest einen double rein. Oder übergebe weniger Parameter als vom String bearbeitet werden.

Außerdem hat man absolute Typ-Sicherheit, da die Funktion weiß von welchem Typ die Parameter sind. Bei der variadischen Funktion in C bekommst du z.B. Ärger wenn der String einen long erwartet und du einen int übergibst, da dann mehr Bytes konsumiert werden. Und umgekehrt. Mit einem template kann man dagegen abfragen welchen Typ der Parameter hat.

Hier werden int und long dann zusammen behandelt:

if (is_same<int, T>::value || is_same<long, T>::value)

Und wenn die Typen nicht übereinstimmen wird statt dessen der Platzhalter ausgegeben. Das könnte man auch anders machen. z.B. “0” oder “0.0” ausgeben.

Ich habe übrigens double verwendet weil Integer-Literale double sind, nicht float. Variablen deklariert man dann auch am besten als double. Auf dem AVR sind das auch 4 Bytes.

Man kann da natürlich auch sämtliche Integer-Typen abfragen, dann geht es mit allem:

if (is_same<unsigned char, T>::value || is_same<char, T>::value || is_same<unsigned int, T>::value ||
    is_same<int, T>::value || is_same<unsigned long, T>::value || is_same<long, T>::value)
   Serial.print(arg);
else
   Serial.print(c);

Erzeugt kein einziges Byte mehr Code bis die jeweiligen Typen auch wirklich verwendet werden.

Variadische templates sind erst mal sehr verwirrend, da die Anleitungen dazu schnell extrem kryptisch werden, aber bei so einer einfachen Anwendung ist es nicht so kompliziert wenn man mal dahinter gestiegen ist. Die Parameter zusammen werden “parameter pack” genannt. Fargs ist ein parameter pack vom Typ Targs. Dann braucht man zwei Versionen der Funktion. Eine ohne parameter pack und eine mit. Letztere wird dann rekursiv aufgerufen und bei jeder Rekursionsstufe wird ein Parameter entfernt. Den aktuellen Parameter hat man dann als “arg” vom Typ T. Das ist aber keine echte Rekursion zur Laufzeit, sondern wird alles vom Compiler aufgelöst.

Um C++11 zu aktivieren (wer es noch nicht gemacht hat) diese Datei öffnen:
x:\Arduino\hardware\arduino\avr\platform.txt

Und bei compiler.cpp.flags hinten -std=gnu++11 anhängen

Serenifly:
Mit einem template kann man dagegen abfragen welchen Typ der Parameter hat.

[quote author=hk007, möchte ich mich anschliessen]
Danke für die Anregungen. [/quote]

++

Kein Problem. Mit variadischen Templates wollte ich schon länger mal rumspielen. Aber außer ein typ-sicheres printf() gibt es nicht so viele einfache sinnvolle Anwendungen, es sei den man macht Firlefanz wie eine beliebige Anzahl von Zahlen zu addieren. In Standard C++ ist das dagegen u.a. für bestimmte Container Klassen wichtig, aber das spielt wiederum auf dem Arduino keine Rolle.

Auf die Unterscheidung zwischen Integer und Float könnte man eigentlich verzichten, da man die Werte ja nur direkt ausgibt und nicht irgendwie unterschiedlich formatieren muss. Also würde theoretisch ein Platzhalter für alles reichen. Dadurch dass der Typ bekannt ist wird schon automatisch die korrekte print() Version aufgerufen.
Es ist aber eventuell nicht schlecht, da man dann eine Überprüfung hat, dass man nicht aus Versehen die Variablen völlig durcheinander bringt. Und man verhindert, dass man z.B. Strings oder irgendwelche beliebigen Objekte übergibt. Geschmackssache.

Oder man will vielleicht eine Formatierung und Floats mit einer bestimmten Anzahl an Nachkommastellen übertragen. Dann braucht man es schon wieder. Es würde aber auch da ein Platzhalter für alle Typen reichen und nur eine Abfrage welchen Typ man gerade hat :slight_smile:

So:

template<typename T, typename... Targs>
void replace(const char* str, T arg, Targs... Fargs)
{
  while (char c = pgm_read_byte(str))
  {
    if (c == '%')
    {
      if (is_same<unsigned char, T>::value || is_same<char, T>::value || is_same<unsigned int, T>::value ||
          is_same<int, T>::value || is_same<unsigned long, T>::value || is_same<long, T>::value)
      {
        Serial.print(arg);
      }
      else if (is_same<float, T>::value || is_same<double, T>::value)
      {
        Serial.print(arg, 1);
      }
      else
        Serial.print(c);

      replace(str + 1, Fargs...);
      return;
    }
    else
    {
      Serial.print(c);
      str++;
    }
  }
}

Oder man schreibt sich gleich einen Parser mit printf()-ähnlicher Syntax :stuck_out_tongue: