Preferences: String Array lesen und speichern

Hallo zusammen,

ich nutze beim ES32-S3 die Preferences.h, um während der Laufzeit sich ändernde Werte und Strings dauerhaft zu sichern und beim Start auszulesen.

Bei Integern funktioniert das auch einwandfrei, aber bei Strings bekomme ich es nicht hin. Die Strings werden in der Loop jeweils in einem Array gespeichert (diese kommen über die serielle Schnittstelle herein). Definiert ist das Array als Startwert z.B. wie folgt:

char savedvolumePST1[10] = {"VOL:10;"};

Im Setup wird der Inhalt aus den Prefs eingelesen:

  prefs.begin("Prefs", false);
  prefs.getString("savedvolumePST1", savedvolumePST1);
  prefs.end();

In der Loop soll alle paar Minuten der Inhalt eines zweiten Arrays actualvolumePST1 (was z.B. mit "VOL:20;" gefüllt ist und initial exakt wie das erste Array definiert ist) in dieses Array dauerhaft abgespeichert werden für den nächsten Neustart:

  prefs.begin("Prefs", false);
  prefs.putString("savedvolumePST1", actualvolumePST1);
  prefs.end(); 
            

Das funktioniert leider nicht, der Inhalt das Arrays savedvolumePST1 enthält beim nächsten Start immer noch den Initialwert "VOL:10;". Kompilieren zeigt jedoch keine Fehler.

Es liegt (vermutlich) am Thema char* und char array, zwischenzeitlich hatte ich beim Herumprobieren mit prefs.putstring() auch Kompilerfehler wie z.B. "incompatible types in assignment of 'String' to 'char [10]". Ich weiß im Moment leider nicht weiter.

Hat jemand einen Tipp? Dankeschön vorab!

Du musst doch den gespeicherten String in eine entsprechende Variable einlesen. Hier ein Beispiel:

password = preferences.getString("password", "");

Hier zum nachlesen.

Danke, ich hatte das aus einem anderen Beispiel.

Wenn ich das so wie vorgeschlagen mache:

savedvolumePST1 = prefs.getString("savedvolumePST1", "");

Dann meckert der Compiler, wie oben beschrieben:

Compilation error: incompatible types in assignment of 'String' to 'char [10]'

Das liegt sicher daran, dass ein char* erwartet wird aber ein array char[] vorliegt. Aber wie ich das entsprechend umwandle oder im best case mit einem einfachen Befehl hinbekomme (mit .c_str()...?)...soweit bin ich noch nicht, bin noch Anfänger.

Also ich würde nie Einstellungen als Strings speichern. Du brauchst doch nur die Werte speichern.

Nicht wirklich.
Es wird ein Objekt der Klasse String erwartet. char* und char[] sind eigentlich gleichwertig.
Mögliche Abhilfen:

  • savedvolumePST1 als String deklarieren
  • Statt putString/getString putBytes/getBytes verwenden - dann kann das ein char[] bleiben. Länge ist dann sizeof(savedvolumePST1)+1 bzw. 11 (eins mehr wg. Null-Byte am Ende)
    Edit (s. #6): Länge ist dann strlen(savedvolumePST1)+1 (eins mehr wg. Null-Byte am Ende), was nur geht, wenn die Zeichenkette maximal neun Zeichen lang ist.

Damit bewegst du dich um 1 Byte außerhalb des Arrays...

Ja, da hast Du recht.
Eigentlich meinte ich auch strlen(), da braucht es das Byte mehr.
sizeof() zählt alle Bytes.

abgesehen davon dass du Char Arrays mit Arduino String Objekten bunt würfelst. Warum musst du die Lautstärke als "String" oder Char Array persistieren? Speichere in deinem Beispiel einfach die 10.
Das "VOL:" ist ja sicher nur ein Fixtext und den Fixtext kannst du ergänzen wenn du es benötigst.

Was schickst du schlussendlich "VOL:10;" wieder hin?

Vielleicht hilft ja folgendes Beispiel weiter:

#include <Preferences.h>
#include <iostream>

constexpr unsigned maxBufferLen {10};

Preferences prefs;
char buffer[maxBufferLen];

void setup() {

  prefs.begin("Prefs", false);

  // Wenn Key noch nicht vorhanden ist, lege ihn an
  if (!prefs.isKey("savedvolumePST1")) {
    std::cout << "Create key savedvolumePST1" << std::endl;
    prefs.putString("savedvolumePST1", "VOL:00");

  }

  // Lese Daten aus Preferences
  prefs.getString("savedvolumePST1", buffer, maxBufferLen);
  unsigned int counter = prefs.getUInt("counter", 0);

  std::cout << std::endl << "Current saved volume: " << buffer << std::endl;
  std::cout << "Current volume as number: " << counter << std::endl;
  ++counter;

  // Wenn Counter > 99, dann gehe in eine Enlosschleife um das Programm anzuhalten
  if (counter > 99) {
    std::cout << "Program stopped" << std::endl;
    while (1) { ; }
  }

  // Baue neuen "String" zum abspeichern
  sprintf(buffer, "VOL:%02d", counter);

  // Beide Werte abspeichern
  prefs.putUInt("counter", counter);
  prefs.putString("savedvolumePST1", buffer);
  prefs.end();

  // Warte etwas und starte den Cotroller neu
  std::cout << "Restarting in 10 seconds..." << std::endl;
  delay(10000);

  // // Restart ESP
  ESP.restart();
}
void loop() {}

Die Frage nach dem Sinn, das immer gleiche "VOL:" abzuspeichern und deswegen String oder char zu verwenden wurde ja schon gestellt. Wie aus dem Beispiel indirekt hervorgeht sollte es ja reichen, einfach nur den Wert zu speichern, der sich auch ändert.

Ich habe dir ja auch nur gezeigt, wie der String auszulesen ist.
Ein Beispiel, wie es mit char geht, ist im übrigen auch in dem Beitrag.
Du musst nur alles lesen und evtl. auch ein wenig selbst kreativ mitwirken.

Ich gebe gerne Hilfestellungen, aber alles vorkauen tue ich nicht, da bin ich zu faul, oder anders gesagt, ich habe selbst noch genug zu tun.

weil @Kai-R da eine so schöne Vorlage gebaut hat, habe ich das mal verwendet und daraus enen MVP gemacht.

Ich denke es braucht

  • eine Funktion um die Lautstärke zu erhöhen (im MVP hab ich mich für einen Button entschieden)
  • eine Funktion um die Lautstärke an das Ziel zu senden (im MVP hab ich mich für die Ausgabe auf Serial entschieden)
  • Optional: habe ich noch einen Timer gemacht um den aktuellen Wert periodisch auszugeben.

Wie gesagt reicht aus meiner Sicht der unsigned Integer in den Preferences.

schau's dir mal an.

MVP
/*
   save an unsigned integer in Preferences instead of a String/string
   de: uint statt string speichern und ausgeben.
   
   based on https://forum.arduino.cc/t/preferences-string-array-lesen-und-speichern/1444522/8 by Kai-R
   2026-05-17 by noiasca https://forum.arduino.cc/t/preferences-string-array-lesen-und-speichern/1444522/11 

*/

#include <Preferences.h>
#include <iostream>
#include "button.h"   // simplified class to debounce a button (see separate tab)

Preferences prefs;
constexpr unsigned maxBufferLen {10};
char buffer[maxBufferLen];

Button btnIncreaseVolume(18);

uint16_t volume;                 // @todo tbd if needed as global, value is anyway in the preferences.

// sends the volume to the destination
void volumeSend() {
  std::cout << std::endl;        // debug only (otherwise you don't see the last line here in the demo imidiately)

  sprintf(buffer, "VOL:%02d;", volume);
  std::cout << buffer;           // output to destination - this MVP just uses the Serial

  std::cout << std::endl;        // debug only (otherwise you don't see the last line here in the demo imidiately)
}

// increase volume, persist to preferences and send to destination
void volumeIncrease() {
  std::cout << std::endl << "vol inc" << std::endl;        // debug only
  volume++;
  if (volume > 99) volume = 99;
  prefs.putUInt("volume", volume);
  volumeSend();
}

// timer to check if volume needs to be sent to destination
void volumeTimer(uint32_t currentMillis = millis()) {
  static uint32_t previousMillis = 0;
  if (currentMillis - previousMillis > 5 * 1000UL) {
    previousMillis = currentMillis;
    volumeSend();
  }
}

void setup() {
  Serial.begin(115200);
  prefs.begin("Prefs", false);
  btnIncreaseVolume.begin();

  // Wenn Key noch nicht vorhanden ist, lege ihn an
  if (!prefs.isKey("volume")) {
    std::cout << "Create key volume" << std::endl;
    prefs.putUInt("volume", 0);
  }

  // Lese Daten aus Preferences
  prefs.getUInt("volume", volume);

  std::cout << "Current volume as number: " << volume << std::endl;
}

void loop() {
  if (btnIncreaseVolume.wasPressed()) volumeIncrease();
  volumeTimer();
}
//

Wenn du Fragen hast - frage.

P.S.: Ob man die Lautstärke dann noch parallel in einer globalen Variable hält oder einfach nur wenn benötigt aus den Preferences ausliest, kann man entscheiden wenn man den ganzen Kontext wüsste.

Hallo zusammen,

vielen lieben Dank für Eure Antworten und Hilfestellungen!

Ich muss etwas ausholen: das mit dem "VOL:10;" war nur ein "Wert" unter vielen, der persistent gespeichert werden soll. Das mit dem Speichern von Integern funktioniert auch einwandfrei, aber die meisten Inhalte bzw. Variablen sind tatsächlich Zeichenketten.

Hintergrund ist die Steuerung eines Streamers, der per UART mit dem ESP32 kommuniziert. Dieser liefert Statusinformationen und nimmt Befehle entgegen.

Das Einlesen erfolgt hiermit:

const byte numChars = 32;
char receivedChars[numChars];   // an array to store the received data
boolean newData = false;
String mystring = "";

void recvWithEndMarker() {
    static byte ndx = 0;
    char endMarker = '\n';
    char rc;
    
    while (Serial1.available() > 0 && newData == false) {
        rc = Serial1.read();
//        Serial.print(rc, HEX);   // <--- Testausgabe
//        Serial.print(' ');       // Optik, erleichtert das Lesen
        if (rc != endMarker) {
            receivedChars[ndx] = rc;
            ndx++;
            if (ndx >= numChars) {
                ndx = numChars - 1;
            }
        }
        else {
            receivedChars[ndx] = '\0'; // terminate the string
            if (receivedChars[ndx-2] == ';')  (receivedChars[ndx-2] = '\0'); //-2 entfernt die Sonderzeichen am Ende vor der LCD Ausgabe
            ndx = 0;
            newData = true;
        }
    }
}


void showNewData() {
    if (newData == true) {
      if (DEBUG == 1) { Serial.println(receivedChars);
      }
        mystring = (receivedChars);

        if (mystring.startsWith("ART"))
         {
         mystring.remove(0, 4);
         mystring.toCharArray(artist, 32);
         }

Im Auszug oben kommt z.B. "ART:Michael Jackson" über die UART herein. Aktuell speichere ich das in einem Array. Habe ich hier schon Bockmist gebaut und es wäre alles im Hinblick auf die Preferences einfacher, das direkt als string zu speichern?, z.B. so:

String artist = "";

void showNewData() {
    if (newData == true) {
      if (DEBUG == 1) { Serial.println(receivedChars);}
        mystring = (receivedChars);

        if (mystring.startsWith("ART"))
         {
         mystring.remove(0, 4);
         artist = mystring;
         }

Damit müsste ich dann bei den Preferences prefs.getString() und prefs.putString() ohne Umwege nutzen können, oder ist das zu einfach gedacht?

Es geht beides. Ich frage mich gerade nur, wo du bei der Nutzung von Char-Arrays "Umwege" gesehen hast?!

Da scheint ein grundsätzliches Verständnisproblem zu herrschen. Im Übrigen könnten auch putBytes oder getBytes verwendet werden.

Ich komme auch nach mehrmaligem Lesen Deines Beispiels oben nicht drauf, wo der Unterschied zu meinem Code liegt (vom Zusammenbau Integer und Text abgesehen).

Mit dem

prefs.putString("savedvolumePST1", "VOL:00");

bekomme ich den Compilerfehler, wenn ich das initial wie folgt deklariert habe:

char savedvolumePST1[10] = {"VOL:10;"};

Liegt dort der Fehler?

char * ist kein String. Versuch mal das:
prefs.putString("savedvolumePST1", String( savedvolumePST1 ));

Danke Dir!

Müsste ich das auch beim Lesen der Prefs so einbauen?

Also wie folgt:

prefs.getString("savedvolumePST1", String(savedvolumePST1));

Wenn man den Beispielcode nimmt und "buffer" durch "savedvolumePST1" ersetzt, dann funktioniert das auch einwandfrei:

#include <Preferences.h>
#include <iostream>

constexpr unsigned maxStrLen {10};

Preferences prefs;
char savedvolumePST1[maxStrLen] {"VOL:00;"};

void setup() {

  prefs.begin("Prefs", false);

  // Wenn Key noch nicht vorhanden ist, lege ihn an
  if (!prefs.isKey("savedvolumePST1")) {
    std::cout << "Create key savedvolumePST1" << std::endl;
    prefs.putString("savedvolumePST1", savedvolumePST1);

  }

  // Lese Daten aus Preferences
  prefs.getString("savedvolumePST1", savedvolumePST1, maxStrLen);
  unsigned int counter = prefs.getUInt("counter", 0);

  std::cout << std::endl << "Current saved volume: " << savedvolumePST1 << std::endl;
  std::cout << "Current volume as number: " << counter << std::endl;
  ++counter;

  // Wenn Counter > 100, dann gehe in eine Enlosschleife um das Programm anzuhalten
  if (counter > 99) {
    std::cout << "Program stopped" << std::endl;
    while (1) { ; }
  }

  // Baue neuen "String" zum abspeichern
  sprintf(savedvolumePST1, "VOL:%02d", counter);

  // Beide Werte abspeichern
  prefs.putUInt("counter", counter);
  prefs.putString("savedvolumePST1", savedvolumePST1);
  prefs.end();

  // Warte etwas und starte den Cotroller neu
  std::cout << "Restarting in 10 seconds..." << std::endl;
  delay(10000);

  // // Restart ESP
  ESP.restart();
}
void loop() {}

Von daher würde ich mal sagen, dass der Fehler den Du schilderst seine Ursache woanders hat.

Versuch mal das:

auto tmp = prefs.getString("savedvolumePST1", String(savedvolumePST1));
strcpy(savedvolumePST1, tmp.c_str());

Hmm.... Schön ist das nicht.

Danke Dir für das Beispiel und die Hilfe!

Ich versuche das nochmal nachzubauen, der einzige Unterschied bei mir ist, dass ich im Setup nicht per

if (!prefs.isKey("savedvolumePST1")) {
    prefs.putString("savedvolumePST1", savedvolumePST1);
  }

den Key anlege, sondern sofort mit

prefs.getString("savedvolumePST1", savedvolumePST1);

beginne. Soweit ich das verstanden habe, wird der Key dann automatisch angelegt und mit dem Inhalt "VOL:00;" vorbelegt.

Statt:

constexpr unsigned maxStrLen {10};

Preferences prefs;
char savedvolumePST1[maxStrLen] {"VOL:00;"};

ist bei mir die Stringlänge fest:

Preferences prefs;
char savedvolumePST1[10] {"VOL:00;"};

Und ich verwende dann statt:

prefs.getString("savedvolumePST1", savedvolumePST1, maxStrLen);

nur folgendes ohne die Stringlänge:

prefs.getString("savedvolumePST1", savedvolumePST1);

Das müsste doch passen, oder?

Das wird nie einen Key anlegen, sondern es will von einem lesen. putString() erledigt das Erstellen des Keys.
Kai prüft ja die Existenz des Keys mit isKey() und legt ihn ggf. mit einem putString() an - und das steht exakt im Kommentar über den Zeilen.

Bei Kai auch - er hat nur den Namen maxStrLen für die magic number 10 vergeben (was m.E. die bessere Lösung ist, falls die magische 10 noch woanders im Code vorkommt).