[Risolto] Lettura e scrittura di un record, come struttura dati, da e su SD

Eccomi qua.
Al momento sono molto intenzionato ad imparare, e per imparare bisogna avere qualcosa da realizzare, quindi ho preso la palla al balzo (questo post) e ho cercato di scrivere una procedura per gestire un file di dati strutturati su SD (scrittura, lettura, ricerca).
Questo mi ha dato modo di lavorare molto con le stringhe C (ho cercato di evitare la classe String), che per me sono un vero cruccio (abitualmente lavoro con C#, VB.Net).

Posto qui le due versioni (file binario, file di testo), con la speranza che qualcuno esperto possa verificare il codice ed eventualmente indicarmi come migliorarlo/ottimizzarlo.
Naturalmente questo aiuterebbe anche l'OP :smiley:

Ho pensato di non aprire un'altro topic, in quanto strettamente legato al post originale, se non è prassi, ditemelo.

TIA
Federico

Versione file binario:

#include <SD.h>
#define pinSD 10

char fileName[12] = "data.dat";

//Definisce la struttura (record)
struct dataStore {
  char a[10];
  unsigned int b;
  double c;
};

//Variabile per il record cercato
struct dataStore data;

//Variabile di appoggio per un record
struct dataStore dataRow;

//Stringa di appoggio per la stampa
char dataLine[50];

//Stringa da cercare nel db
char stringaDaCercare[10] = "D6";

void setup() {
  Serial.begin(9600);
  Serial.println(__FILE__);

  //Verifica la presenda della card
  if (!SD.begin(pinSD)) {
    Serial.println("SD card failed!");
    while (1);
  }
  Serial.println("SD card ready");

  //Se il file esiste lo elimina
  if (SD.exists(fileName)) {
    sprintf(dataLine, "Delete file: %s", fileName);
    Serial.println(dataLine);
    SD.remove(fileName);
  }

  //Apre il file per la scrittura
  File fileW = SD.open(fileName, FILE_WRITE);
  if (fileW) {
    sprintf(dataLine, "Write file: %s", fileName);
    Serial.println(dataLine);    
    //Numero massimo di record da scrivere
    int maxSize = 10;
    for (int i = 0; i < maxSize; i++) {
      //Valorizza il record
      sprintf(dataRow.a, "%s%d", "D", i);
      dataRow.b = i;
      dataRow.c = (double)i / 2.0;
      //Scrive il record su file
      fileW.write((const byte *)&dataRow, sizeof(dataRow));

      //Scrive il record sulla seriale
      //Nota: sprintf ha problemi con i double/float, ho trovato due alternative
      // 1: dtostrf per la conversione in stringa e poi sprintf
      char c[5];
      dtostrf(dataRow.c, 2, 2, c);
      sprintf(dataLine, "%s;%d;%s", dataRow.a, dataRow.b, c);
      // 2: conversione del double in due interi (parte intera+decimale)
      //sprintf(dataLine, "%s;%d;%d.%02d", data.a, data.b, (int)data.c, (int)(data.c*100)%100);
      Serial.println(dataLine);
    }
    fileW.close();
  }
  else {
    sprintf(dataLine, "ERROR Write file: %s", fileName);
    Serial.println(dataLine);        
  }

  //Valorizza il primo capo con un valore di errore  
  strncpy(data.a, "NULL", 10);
  //Apre il file per la lettura
  File fileR = SD.open(fileName, FILE_READ);
  if (fileR) {
    sprintf(dataLine, "Read file: %s", fileName);
    Serial.println(dataLine);     
    //Cicla tutti i bytes leggendo un record alla volta
    while (fileR.available()) {
      //Legge un record (legge tanti bytes quanti ne servono per riepire il record)
      fileR.read((byte *)&dataRow, sizeof(dataRow));
      //Se il primo campo è uguale a stringaDaCercare (quello che cerco), setta data ed esce
      if (strncmp(dataRow.a, stringaDaCercare, 10) == 0) {
        data = dataRow;
        break;
      }
    }
    fileR.close();
  }
  else {
    sprintf(dataLine, "ERROR Write file: %s", fileName);
    Serial.println(dataLine);        
  }

  //Se il primo campo è diverso da "NULL" stampo il record trovato
  if (strncmp(data.a, "NULL", 10) != 0) {
    char c[5];
    dtostrf(data.c, 2, 2, c);
    sprintf(dataLine, "Trovato: %s;%d;%s", data.a, data.b, c);
    Serial.println(dataLine);
  } else {
    Serial.println("Non trovato");  
  }


}

void loop() {
// vuoto
}

Versione file testo:

#include <SD.h>
#define pinSD 10

char fileName[12] = "data.txt";

struct dataStore {
  char a[10];
  unsigned int b;
  double c;
};

//Variabile per il record cercato
struct dataStore data;

//Variabile di appoggio per un record
struct dataStore dataRow;

//Stringa di appoggio
char dataLine[50];

//Stringa da cercare nel db
char stringaDaCercare[10] = "S8";

void setup() {
  Serial.begin(9600);
  Serial.println(__FILE__);

  //Verifica la presenda della card
  if (!SD.begin(pinSD)) {
    Serial.println("SD card failed!");
    while (1);
  }
  Serial.println("SD card ready");

  //Se il file esiste lo elimina
  if (SD.exists(fileName)) {
    sprintf(dataLine, "Delete file: %s", fileName);
    Serial.println(dataLine);
    SD.remove(fileName);
  }

  //Apre il file per la scrittura
  File fileW = SD.open(fileName, FILE_WRITE);
  if (fileW) {
    sprintf(dataLine, "Write file: %s", fileName);
    Serial.println(dataLine);
    //Numero massimo di record da scrivere
    int maxSize = 10;
    for (int i = 0; i < maxSize; i++) {
      //Valorizza il record
      sprintf(dataRow.a, "%s%d", "S", i);
      dataRow.b = i;
      dataRow.c = (double)i / 2.0;

      //Crea la stringa delimitata
      //Nota: sprintf ha problemi con i double/float, ho trovato due alternative
      // 1: dtostrf per la conversione in stringa e poi sprintf
      char c[5];
      dtostrf(dataRow.c, 2, 2, c);
      sprintf(dataLine, "%s;%d;%s", dataRow.a, dataRow.b, c);
      // 2: conversione del double in due interi (parte intera+decimale)
      //sprintf(dataLine, "%s;%d;%d.%02d", data.a, data.b, (int)data.c, (int)(data.c*100)%100);

      //Scrive il record su file
      fileW.println(dataLine);

      //Scrive il record sulla seriale
      Serial.println(dataLine);
    }
    fileW.close();
  }
  else {
    sprintf(dataLine, "ERROR Write file: %s", fileName);
    Serial.println(dataLine);
  }

  //Valorizza il primo capo con un valore di errore
  strncpy(data.a, "NULL", 10);
  //Apre il file per la lettura
  File fileR = SD.open(fileName, FILE_READ);
  if (fileR) {
    sprintf(dataLine, "Read file: %s", fileName);
    Serial.println(dataLine);
    //Cicla il file leggendo una riga alla volta (fino a \n, fine riga)
    while (fileR.available()) {
      String line = fileR.readStringUntil('\n');
      //Converte la stringa (riga) in un char array
      line.toCharArray(dataLine, line.length());
      //Estrae il primo campo
      char * strtokIndx;
      strtokIndx = strtok(dataLine, ";");
      strcpy(dataRow.a, strtokIndx);
      //Se il primo campo è uguale a stringaDaCercare (quello che cerco), setta data ed esce
      if (strncmp(dataRow.a, stringaDaCercare, 10) == 0) {
        strncpy(data.a, dataRow.a, 10);
        strtokIndx = strtok(NULL, ";");
        data.b = atoi(strtokIndx);
        strtokIndx = strtok(NULL, ";");
        data.c = atof(strtokIndx);
        break;
      }
    }
    fileR.close();
  }
  else {
    sprintf(dataLine, "ERROR Read file: %s", fileName);
    Serial.println(dataLine);
  }

  //Se il primo campo è diverso da "NULL" stampo il record trovato
  if (strncmp(data.a, "NULL", 10) != 0) {
    char c[5];
    dtostrf(data.c, 2, 2, c);
    sprintf(dataLine, "Trovato: %s;%d;%s", data.a, data.b, c);
    Serial.println(dataLine);
  } else {
    Serial.println("Non trovato");
  }

}

void loop() {
//VUOTO
}

Scusate il ritardo, non sono sparito ;D .
Voglio ringraziare tutti quanti ed ognuno singolarmente per il contributo dato al post.
Mi fa piacere che sia stato utile a molti.
Un ringraziamento particolare a Federico66 il cui codice funziona perfettamente, sia in scrittura che in lettura.
Il LF che mettevo a fine record era senzaltro un errore usando dbFile.readBytesUntil(LF, &Record, 8); ma dovendo essere parte di una esercitazione didattica, volevo mostrare il file creato ai ragazzi nella maniera più ordinata possibile, tant'è che l'ho reintrodotto nella struttura come Filler.
Ho modificato anche un po' la struttura
Devo ammettere che i puntatori sono #potentementecomplicatiepericolosi
Con questo Vs. contributo il mio progetto ha avuto una svolta molto positiva.
Grazie a tutti.

struct ABX {									// Struttura del record di database
	char	 DBTipo;							// A=amministratore / U=utente / X=revocato
	unsigned long DbCard;						// ID impronta
	byte	 DbImp;								// ID Card RFID
	char	 DbNome[20];						// Nominativo utente
	char	 Filler;							// filler 
} Record;
/* prova di scrittura */
		Record.DBTipo = 'A';
		Record.DbCard = 3134562064;
		Record.DbImp = 1;
		copyC(Admin, Record.DbNome, 20);
		Record.Filler = LF;
		dbFile = SD.open(NomeDb, FILE_WRITE);
		dbFile.write((const uint8_t *)&Record, sizeof(Record));
		dbFile.flush();
/* prova di lettura */
		dbFile.read((uint8_t *)&Record, sizeof(Record));
		Serial.print(Record.DBTipo);
		Serial.print(F("-"));
		Serial.print(Record.DbCard);
		Serial.print(F("-"));
		Serial.print(Record.DbImp);
		Serial.print(F("-"));
		Serial.print(Record.DbNome);
		Serial.print(Record.Filler);

crc57:
Mi fa piacere che sia stato utile a molti.

A me sicuramente, quindi grazie a te, fino a qualche giorno fa per me il C era solo la terza lettera dell'alfabeto :smiley:

Federico

PS
non mi è chiara la differenza uint8_t e byte, visto che comunque indicano il byte, ma visto che nel reference di Arduino uint8_t non viene segnalato, alla fine ho usato byte

Federico66:
A me sicuramente, quindi grazie a te, fino a qualche giorno fa per me il C era solo la terza lettera dell'alfabeto :smiley:

Federico

PS
non mi è chiara la differenza uint8_t e byte, visto che comunque indicano il byte, ma visto che nel reference di Arduino uint8_t non viene segnalato, alla fine ho usato byte

Sono delle definizioni di tipo per evitare ambiguità
uint_8 è sicuro. sta per unsigned intero a 8 bit, anche un byte lo è (su tutte le architetture)
ma ad esempio uint_16 è unsigned intero a 16 bit (2 byte). NON è detto si la stessa cosa con int in quanto la dimensione in byte di un int dipende dall'architettura, può essere 2 o 4 byte.

nid69ita:
Sono delle definizioni di tipo per evitare ambiguità
uint_8 è sicuro. sta per unsigned intero a 8 bit, anche un byte lo è (su tutte le architetture)
ma ad esempio uint_16 è unsigned intero a 16 bit (2 byte). NON è detto si la stessa cosa con int in quanto la dimensione in byte di un int dipende dall'architettura, può essere 2 o 4 byte.

Chiarissimo come sempre, grazie

Federico66:
Chiarissimo come sempre, grazie

A riprova di quello che dice nid69ita a proposito di uint16_t, in Arduino UNO è di 2 byte quindi l'ID della card RFID l'ho dovuta definire unsigned long che invece è 4 byte anche su Arduino UNO