Charr-array aus Variablen zusammenbauen

Hi,

die C-strings bringen mich noch um den Verstand.

Meine aktuelle Aufgabestellung ist, einen C-String mit ether.browseUrl (EtherCard-lib) zu übergeben. Der C-String soll aus Text und Variablen-werten zusammengebaut werden.

Grundlegend hab ich es mal so aufgebaut:

  char phpstring[255];
 char chartemp[8];

void makestring() {
    sprintf(phpstring, "%d", timeoffset1);
    sprintf(&phpstring[strlen(phpstring)], ",%d", node1);
    sprintf(&phpstring[strlen(phpstring)], ",");
    dtostrf(DHTTemperature,5,2,chartemp);
    sprintf(&phpstring[strlen(phpstring)], chartemp);
.
.
. 
   sprintf(&phpstring[strlen(phpstring)], "&apikey=xyz);
}

    ether.browseUrl(PSTR("http://192.168.1.80/emoncms/input/bulk.json?data=[["), phpstring, website, my_callback);

Mit Integerwerten geht es ja ganz gut. Allerdings hakt es ja mit sprintf und float-variablen. Daher der Umweg über dtostrf. Aber irgendwie ist das doch etwas hölzern. Oder wie seht ihr das? Da ich einen ganzen Sack von float-Werten übergeben will, wäre ich um eine "einfachere" Programmierung schon sehr froh. Hat da einer von euch C-Gurus eine Idee?

Gruß/hk007

Es gibt einen Patch für 1.0.x:

Der bläht dir allerdings printf() und scanf() noch mehr auf, auch wenn du Float gar nicht verwendest: http://forum.arduino.cc/index.php/topic,124809.msg940922.html#msg940922

Ansonsten ist die dtrostf(), sprintf() Kombination schon ok. Aber halt problematisch wenn man viele Floats hat.

Du hast allerdings printf() überhaupt nicht verstanden. Das ist hochgradiger Schwachsinn:

    sprintf(phpstring, "%d", timeoffset1);
    sprintf(&phpstring[strlen(phpstring)], ",%d", node1);
    sprintf(&phpstring[strlen(phpstring)], ",");
    dtostrf(DHTTemperature,5,2,chartemp);
    sprintf(&phpstring[strlen(phpstring)], chartemp);

Du hast einen Format-String und einen Aufruf für alles und übergibst hinten einfach Parameter für jeden Platzhalter im Format-String. Das ist eine variadische Funktion, d.h. man kann beliebig Parameter übergeben und die werden erst zur Laufzeit ausgewertet.

Siehe hier: http://www.cplusplus.com/reference/cstdio/printf/

z.B. das Standard printf() (geht so auf dem Arduino nicht, aber das Prinzip ist gleich)

printf ("Some different radices: %d %x %o %#x %#o \n", 100, 100, 100, 100, 100);

Ausgabe:

Some different radices: 100 64 144 0x64 0144

Der Formatierungs-Buchstabe für einen C String ist %s. Das hast du am Ende gar nicht gemacht

Hi Serenifly,

danke für deine Antwort. uihhh jetzt hast du mich schön zerlegt =( (Bin dir aber nicht bös)

Serenifly: Du hast allerdings printf() überhaupt nicht verstanden.

Stimmt, aber es kam das richtige raus :*. Ich hab halt C nie richtig gelernt. Wenn ich etwas programmieren will, dann such ich mir immer Codeschnipsel aus dem Internet und passe die dann so lange an, bis das gewünschte Ergebnis rauskommt. Vllt. in manchen Augen nicht die "richtige" Art und Weise, aber ich programmier halt sehr selten. Immer dann, wenn ich in meiner Hütte etwas durch einen Arduino automatisieren oder messen will. Und dann wird es meistens quickanddirty.

Wie würde es denn bei dir aussehen, wenn du z.B. - "Text1" - intvar = 12 - floatvar = 11.12 zusammenfügen willst, damit es so aussieht.: --> "Text1,12,11.12"

War nicht so böse gemeint :)

Angenommen die gepatchten Dateien sind installiert:

char str[] = "Text";
int var1 = 12;
float var2 = 1.23;

char buffer[20];
snprintf_P(buffer, sizeof(buffer), PSTR("%s,%d,%.2f"), str, var1, var2);

%.2f bedeutet dabei zwei Nachkommastellen. Da gibt es auch noch andere Optionen, aber ich denke Mindestbreite u.ä. wird hier nicht gebraucht.

Auch gleich die sicherere n Version, die keinen Puffer-Überlauf produziert und die _P Version die das RAM für den Formatstring spart.

Wenn das alles ist, kannst du aber auch deinen Float mit dtostrf() formattieren und statt %.2f einfach nochmal %s machen und dann hinten den Puffer für den Float String übergeben

OK, das hab ich einigermassen verstanden. (Patchen werd ich meine IDE für das kleine Projekt nicht.)

Hab allerdings zum Verständnis noch Fragen. 1.) Muss ich den Text über ein char-array einbauen? In der C++-Reference wird es so gemacht:

snprintf ( buffer, 100, "The half of %d is %d", 60, 60/2 );

2.) Das PSTR dazwischen: Reicht es nicht, wenn ich vorne schon snprintf_P schreibe, damit ich RAM spare?

3.) Wenn ich den Stringaufbau etwas strukturieren will? 10 Variablen in einem sprintf zusammenbasteln, wird m.E etwas unübersichtlich. Geht dann wohl nur so wie in der Reference?

  cx = snprintf ( buffer, 100, "The half of %d is %d", 60, 60/2 );
  snprintf ( buffer+cx, 100-cx, ", and the half of that is %d.", 60/2/2 );

Oder gibts ne Möglichkeit das einfach hinten dran zu hängen, ohne dass man diesen cx-pointer mitschleppt

Hmmmm... ich sollte mir wohl auch mal strcat ansehen????

gruß/hk007

hk007: Muss ich den Text über ein char-array einbauen?

Nein. Nur wenn er variabel sein soll. Die Kommas sind ja auch Text. Alles was nicht einem % Platzhalter entspricht wird als Text interpretiert und so in den String geschrieben

Das PSTR dazwischen: Reicht es nicht, wenn ich vorne schon snprintf_P schreibe, damit ich RAM spare?

Nein, das geht schief. Dann ist der String immer noch im RAM, printf() interpretiert dessen Adresse als eine Adresse im Flash und liest dann wahrscheinlich Unsinn aus wenn es pgm_read_byte() macht.

der gibts ne Möglichkeit das einfach hinten dran zu hängen, ohne dass man diesen cx-pointer mitschleppt

Kommt darauf an was man macht. Wenn man z.B. client.print() oder Serial.print() macht oder was auf SD schreibt, braucht man printf() nicht unbedingt. Dann kann man den String durch wiederholtes Aufrufen der print() Funktion zusammenstückeln. Die interessiert es nicht ob das alles in einem String steht. Es sei dann man muss Zahlen auf eine konstante Breite formatieren. Dann ist printf() auch da sehr praktisch.

Bei browseurl() sieht es dagegen wieder anders aus. Das braucht wohl den String in einem Rutsch.

cx ist dabei kein Zeiger. Das ist ein einfacher Integer. Der Code oben nutzt die Tatsache aus dass snprintf() die Länge des fertig formatieren Strings zurück gibt. Damit kann man das Ende des Strings schneller berechnen als mit strlen(), welches erst mal über den ganzen String iteriert und die Zeichen zählt. Man kann dann die Anzahl der schon geschriebenen Zeichen zum Zeiger auf den String addieren und hat damit einen Zeiger wo es weitergeht.

Einfacher wird es wohl nicht, wobei man da bei der Länge sizeof() nehmen sollte, statt die Größe per Hand einzutragen

Hmmmm... ich sollte mir wohl auch mal strcat ansehen????

Das ist eine andere Möglichkeit. Du braucht aber dann ein zweites Puffer Array. Das sollte man dann lokal in der Funktion anlegen, damit der Speicher danach wieder freigegeben wird.

Serenifly:

Hmmmm... ich sollte mir wohl auch mal strcat ansehen????

Das ist eine andere Möglichkeit. Du braucht aber dann ein zweites Puffer Array. Das sollte man dann lokal in der Funktion anlegen, damit der Speicher danach wieder freigegeben wird.

Wieso ein zweites Char-Array?

Geht das nicht?

snprintf_P(buffer, sizeof(buffer), PSTR("%s,%d,%.2f"), str, var1, var2);
strcat (buffer, "Ende Gelände");

oder halt dann strcat_P (wenns das gibt)

Ok, das geht natürlich. Ich dachte du wolltest sowas wie zweimal sprintf() mit jeweils 5 Variablen und das dann zusammenfügen. Wie in dem Code am Ende deines letzten Posts.

Auch da gibt es übrigens die bessere _P Version (wie bei allen anderen String Funktionen die einen konstanten String enthalten):

strcat _P(buffer, PSTR("Ende Gelände"));

Ich glaub jetzt bin ich auf dem richtigen Weg.

Mein String soll am Ende in etwa so aussehen: http://192.168.1.80/emoncms/input/bulk.json?data=[[0,16.22,1137],[2,17.34,1437,3164],[4,19.56,1412,3077]]&apikey=5a0b7a9339e4ed53543545754754

Ich werde mir 3 Teilstrings anlegen. Die beiden blauen Elemente sind immer gleich. Nur der schwarze Teil sind sich ändernde Messwerte. Den Messwert-String werde ich mit snprint aufbauen. Am Schluss dann alles mit strcat zusammenkleben. Wobei ich bei "nur" 3 Teilen wohl besser auf die zusätzliche string.h verzichte und das evtl. besser mit snprint und dem Berechnen der Zielposition mache. (Wie oben diskutiert)

@Serenifly THX für deine geduldige Hilfe.

Edit: Wieso der eine Teil da oben rot ist, obwohl ich ihn mit (color=blue) formatiert habe, wird wohl auch immer ein Geheimnis bleiben.

Könnte man so machen (vereinfacht):

const char start[] PROGMEM = "http://192.168.1.80/emoncms/input/bulk.json?data=";
const char end[] PROGMEM = "&apikey=5a0b7a9339e4ed53543545754754";
char buffer[255];

void setup()
{
    Serial.begin(115200);
}

void loop() 
{
    int var1 = 0;
    float var2 = 16.22;
    int var3 = 1337;

    char floatStr[8];
    dtostrf(var2, 5, 2, floatStr);

    buffer[0] = '\0';      //damit der String für strcat() leer erscheint! Alternativ memset() um ihn wirklich komplett auf 0 zu setzten
    int length = strlcat_P(buffer, start, sizeof(buffer));
    snprintf_P(buffer + length, sizeof(buffer), PSTR("[[%d,%s,%d]]"), var1, floatStr, var3);
    length = strlcat_P(buffer, end, sizeof(buffer));

    Serial.print(F("total length: ")); Serial.println(length);
    Serial.println(buffer);

    delay(5000);
}

strlcat() ist dabei etwas sicherer als strcat(), da es keinen Pufferüberlauf macht und anders als strncat() sicher Null-terminiert (wird aber immer noch kritisiert). Und man bekommt gleich die Länge des Strings zurück! Spart also einen Aufruf von strlen() :) (wobei in diesem speziellem Fall da auch sizeof(start) - 1 die korrekte Zahl liefert, was zur Compile-Zeit bekannt ist).

%u ist vielleicht auch korrekter als %d wenn du nur unsigned hast. Bei so kleinen Zahlen ist es aber egal.

255 Zeichen ist da übrigens etwas übertrieben. So um die 150 dürfte reichen.

Jo, 255 sind zu viel :slight_smile:

Jetzt beutelts mich aber noch wo anders:

  char startphp[] PROGMEM = "http://192.168.1.80/emoncms/input/bulk.json?data=";
  char apikey[] PROGMEM = "&apikey=5a0b7a9339e4e75895634654362";
  char phpstring[200];

    cx = snprintf_P (phpstring, sizeof(phpstring), PSTR("[[%d,%d,11.10,22.20,33.30,44.40,1234]]"), timeoffset1, node1);
    snprintf_P (phpstring+cx, sizeof(phpstring)-cx, apikey); 

    Serial.println (startphp);
    Serial.println (phpstring);

    ether.browseUrl(startphp, phpstring, website, my_callback);

Auf der seriellen Console bekomme ich nur den “phpstring” angezeigt. “startphp” ist nur ne leere Zeile :frowning:
Why?? Ist doch dasselbe. Beides sind char-arrays

startphp existiert nur im Flash da es mit PROGMEM deklariert ist! Das ist kein Speicher im RAM!

Der AVR hat eine Harvard Architektur. Das heißt Flash und RAM haben getrennte Adress-Räume. Funktionen die Adressen im RAM erwarten kann man nicht so direkt Zeiger ins Flash übergeben. Auf Assembler-Ebene greifen praktisch alle Befehle ins RAM und es gibt nur einen Befehl um ein Byte aus dem Flash ins RAM zu kopieren. Deshalb werden am Anfang alle Strings aus dem Flash ins RAM kopiert und es gibt diese ganzen Umwege und Krücken über _P Funktionen und Makros wie pgm_read_xxx() um Daten im Flash zu verarbeiten. Idealerweise würde das der Compiler übernehmen, aber das ist noch im Entwicklungsstadium und man muss sich per Hand um alles kümmern.

Die printf() Version in der avr libc hat da übrigens %S statt %s um PROGMEM Strings zu verarbeiten. Siehe hier: http://www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html#gaa3b98c0d17b35642c0f3e4649092b9f1

S Similar to the s format, except the pointer is expected to point to a program-memory (ROM) string instead of a RAM string.

Das wäre der korrekte Weg:

snprintf_P (phpstring+cx, sizeof(phpstring)-cx, PSTR("%S"), apikey);

Dass das was du oben gemacht hast geht ist kein reiner Zufall, aber es ist auch unberechenbar. Das funktioniert weil die Funktion den apikey String als den Format-String interpretiert und 1:1 in den Puffer schreibt! Und die Parameterliste (varargs) ist leer. Mach das mit der normalen sprintf() Version statt _P und du fliegst auf die Schnauze, da die keinen Format-String im Flash verarbeiten kann. Das hier geht dagegen:

snprintf(phpstring+cx, sizeof(phpstring)-cx, "%S", apikey);

Genauso würdest du wahrscheinlich Probleme bekommen, wenn der String ein Prozentzeichen enthalten würde, da er dann versucht das als Formatierungsplatzhalter zu behandeln.

Das geht nicht, da println() einen Zeiger ins RAM will:

Serial.println(startphp);

browseUrl() dagegen kann einen PROGMEM String als ersten Parameter verarbeiten. Das ist aber nur der Fall weil die Funktion speziell programmiert wurde um das zu machen!! Die macht dann intern in einer Schleife pgm_read_byte() um den String char für char aus dem Flash zu kopieren. PSTR() ist dabei ein Makro hierfür: (const PROGMEM char *)(s) Deshalb spielt es keine Rolle ob man den String global als PROGMEM definiert und dann die Variable übergibt, oder das PSTR() Makro verwendet und ihn erst im Funktionsaufruf schreibt.

Aber wie gesagt, "normalen" Funktionen kann man keine PROGMEM Strings übergeben. Man muss diese anders als Strings im RAM behandeln.

Aktuelle Versionen von AVR gcc (ab 4.7.) haben da ein paar Verbesserungen um diese ganzen Klimmzüge teilweise zu vermeiden: http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Flash_mit_flash_und_Embedded-C In der Arduino Software geht das theoretisch aber erst ab 1.5.7, da man da endlich mal die AVR toolchain aktualisiert hat. Allerdings bekomme ich das da auch nicht zum Laufen. Er erkennt __flash nicht, auch wenn ich -std=gnu99 aktiviert habe. Außerdem geht das anscheinend nur in C und nicht in C++. Da bleibe ich lieber bei PROGMEM und aktiviere statt dessen C++11 :)

HI, sehr gut erklärt. Das hab sogar ich verstanden (Hoff ich)

Serenifly: Das geht nicht, da println() einen Zeiger ins RAM will:

Serial.println(startphp);

m geht das anscheinend nur in C und nicht in C++. Da bleibe ich lieber bei PROGMEM und aktiviere statt dessen C++11 :)

Das heisst ich muss den String erst ins RAM bringen:

  char chartemp[100];
    snprintf (chartemp, sizeof(chartemp),"%S", startphp);
    Serial.print (chartemp);

--> Damit bekomme ich den String auf die Console. Allerdings hab ich dann nichts gewonnen, da ich wieder RAM verschwendet habe. Man müsste vllt. nur einen Puffer für ein Byte nehmen, und den dann in einer Schleife, x von 0-sizeof(), Byte für Byte ausgeben. Ich hätte es mal naiv so versucht:

    chartemp[x] = startphp[x];

Aber das geht nicht.

Das heisst ich muss den String erst ins RAM bringen:

Ja, aber fixiere dich nicht so auf printf(). printf() ist elendig komplex und braucht viel Flash (und temporär RAM während der Bearbeitung der Parameterliste). Und die Laufzeit ist auch recht hoch. Das sollte man nur verwenden wenn man es wirklich braucht.

Der normale Weg einen Flash String ins RAM zu kopieren ist strcpy_P(), bzw. strncpy_P() oder strlcpy_P()

Allerdings hab ich dann nichts gewonnen, da ich wieder RAM verschwendet habe.

Kommt darauf an. Das ist aber vertretbar wenn man sowas oft macht. Du könntest in gewissen Grenzen auch deinen phpstring[255] Puffer dafür verwenden wenn du auf die Reihenfolge achtest. Also erst einen Flash-String ausgeben und danach den Puffer für printf() verwenden.

Ich habe einen globalen Puffer dafür den ich zigfach benutzte. Dann habe ich zwei Makros:

#define P(str) strcpy_P(stringBuffer, PSTR(str))
#define ProgmemCopy(str) strcpy_P(stringBuffer, str)

Die kann man direkt in Funktionen einsetzen, da strcpy() einen Zeiger auf den Puffer zurück liefert. P() kann man dabei wie das Arduino F() Makro verwenden, aber für alle Funktionen und nicht nur print(). Und ProgmemCopy() kopiert einen schon mit PROGMEM definierten String ins RAM.

Das ganze hat natürlich Einschränkungen. Ich habe z.B. Funktionen für die SD-Karte, die einen Flash-String für den Dateinamen übernehmen. Bei der Schreibfunktion kann ich dann nicht den gleichen Puffer für die zu schreibenden String nehmen. Da Dateinamen aber nur 8.3 groß sein können nehme ich dafür einen kleinen lokalen Puffer.

Man müsste vllt. nur einen Puffer für ein Byte nehmen, und den dann in einer Schleife, x von 0-sizeof(), Byte für Byte ausgeben.
Ich hätte es mal naiv so versucht:

Dafür gibt es wie schon kurz erwähnt pgm_read_byte_near(). Kann man z.B. so machen:

const char testString[] PROGMEM = "blahblah1234test";

void setup()
{
	Serial.begin(115200);
}

void readFlashString(const char* str)
{
	int length = strlen_P(str);
	for(int i = 0; i < length; i++)
		Serial.print((char)pgm_read_byte_near(str + i));
	Serial.println();
}

void loop() 
{
	readFlashString(testString);
	
	delay(5000);
}

Den pgm_read_xxx_near() Makros übergibt man eine Adresse (nicht die Daten selbst!) die normalerweise im RAM ist und sie liefert die Daten im Flash. Das gibt es auch mit pgm_read_word_near() für 16-Bit Variablen (ints oder Zeiger), mit pgm_read_dword_near() für 32 Bit (long) und mit pgm_read_float_near() für 4-Byte floats

Das _near kann man auch weglassen.

Siehe auch hier:
http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
Anmerkung dazu: die neueren Versionen von avr gcc erzwingen absolute const-correctness bei PROGMEM Daten. Das merkt man vor allem bei Arrays aus Strings wo plötzlich ein const Zeiger auf const Daten gefordert wird. Da musste ich schon zwei Libs anpassen. Bei der alten Version auf nicht-BETA Arduino Versionen ist das noch optional und im Code auf der Seite wird das auch nur durch das PGM_P Makro gemacht (das steht für const char*), aber nicht bei den Zahlen. Und dann bräuchte man bei String Arrays einen const char* const

Mann oh mann, von dir kann man viel lernen :-)

Das readFlashString hab ich mir gleich eingebaut. Funzt super.

Allerdings hab ich jetzt noch ein Problem mit der dtostrf-Funktion.

dtostrf (DHTTemp_1,5,2,DHTTemp_1_str);

Bei 27,00 Grad bekomme ich "27.00" Gut so Bei 5,00 Grad bekomme ich " 5.00" Also noch ein Leerzeichen davor. Und das nimmt mir der Empfänger übel. Ist ja auch klar. Ich hab als Parameter auch die 5 vorgegeben. Kann man den Parameter irgendwie dynamisch vorgeben?

Du könntest vielleicht den float auf int casten und dann abfragen ob er > 10 ist:

if((int)DHTTemp_1 > 10)
{
}
else
{
}

Und dann je nachdem die Breite einstellen. Das würde ich dann in einer Funktion kapseln, der man den Wert und den Puffer übergibt und die einen Zeiger auf den Puffer zurück liefert:

char* formatFloat(char* buffer, float value)
{
     ....

     return buffer;
}

Man kann aber auch nachträglich den String anfassen:

char* removeSpaces(char* str)
{
  while(*str == ' ')
     str++;
  return str;
}

Das entfernt die Leerzeichen natürlich nicht wirklich , sondern liefert nur einen Zeiger auf weiter hinten im String.

Hmm…

hab mal selber etwas rumgespielt.
dtostrf (DHTTemp_1, 1, 2, DHTTemp_1_str);
ergibt trotzdem “22.70” oder “2.20” oder “-22.80”. Je nach Eingangsvariable. So wie ich es brauche.
Hab ich da wieder eine undokumentierte Funktion gefunden?
Eigentlich sollte der 2 Parameter ja die Anzahl der Zeichen inkl. Dezimalpunkt sein. Und ich bekomm definitv mehr als 1 Zeichen

So 100%ig kenne ich mich da nicht aus. Das ist keine Standard C Funktion, sondern existiert nur auf dem AVR. Da das ist ist die "minimale Breite" ist nehme ich mal an dass er es dann mindestens 1 Zeichen breit macht und den Rest nach Bedarf.

http://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html#ga060c998e77fb5fc0d3168b3ce8771d42

Conversion is done in the format "[-]d.ddd". The minimum field width of the output string (including the '.' and the possible sign for negative values) is given in width, and prec determines the number of digits after the decimal sign. width is signed value, negative for left adjustment.

Sicherlich einfacher als was ich oben geschrieben habe :)

Serenifly: http://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html#ga060c998e77fb5fc0d3168b3ce8771d42

Conversion is done in the format "[-]d.ddd". The minimum field width of the output string (including the '.' and the possible sign for negative values) is given in width, and prec determines the number of digits after the decimal sign. width is signed value, negative for left adjustment.

Ha, genau so eine Info habe ich gesucht. Hab aber im Internet nix dazu gefunden. Na ja, wenn man auf der C++ Referenzseite sucht, und die Funktion ist nur für AVR, dann kann man es auch nicht finden. Dann ist es ja nicht undokumentiert 8)

So, jetzt läuft meine Software, und ist auch für spätere IDEs kompatibel. Noch mal einen großen Dank für deine Super-Hilfe.

hk007: Na ja, wenn man auf der C++ Referenzseite sucht, und die Funktion ist nur für AVR, dann kann man es auch nicht finden

Auf die C++ Seite verlinke ich gerne oder schaue selbst nach, weil das da gut erklärt ist und Beispiele dabei sind. Aber das ist für Computer/PCs. Und selbst da gibt es bei den C Bibliotheken (cstring.h ist genau genommen C und nicht C++) Unterschiede, je nachdem welchen Compiler man hat. In Visual C++ gibt es z.B. kein snprintf(). Das heißt da sprintf_s() (für "secure")

Da sind auch Funktionen drin, die es auf dem Arduino nicht gibt. Zum Teil weil es halt keine 64 Bit Datentypen gibt. Was in der avr libc wirklich vorhanden ist, ist hier gezeigt: http://www.nongnu.org/avr-libc/user-manual/modules.html Da ist vor allem string.h und stdlib.h relevant. Aber die Erklärungen könnten etwas besser sein.