Graphing sensor data using sd card

I am still a beginner at programming so sorry for the basic question. I purchased an adafruit sd card board (MicroSD card breakout board+ : ID 254 : $7.50 : Adafruit Industries, Unique & fun DIY electronics and kits) to log data from sensors and display a graph of the readings. I have been able to record both the temperature and the humidity in different files to be used for graphing on an TFT screen. I am stuck with two problems; how can I have the amount of saved data points be limited so when say the 101st reading is taken it is put as the 1st and erases the 101st to only have the 100 most recent recordings kept, and how can I call to a specific line in a file? For graphing on the screen I want to make an array to draw a line from one reading to the next which will correspond to the next line in each file. The library I am using is the adafruit SD library (GitHub - adafruit/SD: fixes & updates to the Arduino SD library - totally in progress. works but in beta). I appreciate any help.

Sorry I forgot to post my code:

#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <HIH61XX.h>

HIH61XX hih(0x27, 8);
File myFile;

// change this to match your SD shield or module;
//     Arduino Ethernet shield: pin 4
//     Adafruit SD shields and modules: pin 10
//     Sparkfun SD shield: pin 8
const int chipSelect = 4;

void setup()
{
 // Open serial communications and wait for port to open:
  Serial.begin(9600);
  Wire.begin();
   while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }


  Serial.print("Initializing SD card...");
  // On the Ethernet Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin 
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output 
  // or the SD library functions will not work. 
   pinMode(SS, OUTPUT);
   
  if (!SD.begin(chipSelect)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
}

void loop()
{
  //  start the sensor
  hih.start();

  //  request an update of the humidity and temperature
  hih.update();

  Serial.print("Humidity: ");
  Serial.print(hih.humidity(), 5);
  Serial.print(" RH (");
  Serial.print(hih.humidity_Raw());
  Serial.println(")");

  Serial.print("Temperature: ");
  Serial.print(hih.temperature(), 5);
  Serial.println(" C (");
  Serial.print(hih.temperature_Raw());
  Serial.println(")");
  
     myFile = SD.open("Temp.txt", FILE_WRITE);
  
  // if the file opened okay, write to it:
  if (myFile) {
    myFile.println(hih.temperature());
	// close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening temp.txt");
  } 
  
  myFile = SD.open("Humidity.txt", FILE_WRITE);
  
  // if the file opened okay, write to it:
  if (myFile) {
    myFile.println(hih.humidity());
	// close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening humidity.txt");
  }
 
  delay(5000);
}

This is just for logging the humidity and temperature to different files on the sd card.

I am stuck with two problems; how can I have the amount of saved data points be limited so when say the 101st reading is taken it is put as the 1st and erases the 101st to only have the 100 most recent recordings kept

If you write each record so it has exactly the same number of characters, you can determine easily how many records there are in the file.

You could add a new record at the top that contains the record number of the newest or oldest reading. Then, when you read the file, you read from that record to the end and then reposition to the top, and read from record two to that record number, minus 1.

Using variable length records, your job is at least an order of magnitude harder.

Is the temperature and humidity data integer or float?

The temperature and humidity data come back to 5 decimals so I was planning to do float. I guess I don't know what the commands are for reading a specific line in a file as well as deleting a specific line. From the examples I can easily read the entire file with myFile.read() but how do I just read one line? Also I can delete an entire file with SD.remove("unwanted.txt") but am not sure how to erase just one line.

I figured I could just put one entry per file. I made a quick code where it opens two files on the sd card and sets an x and y variable to the value put into each file. Then it puts a dot on the screen at those coordinates. The problem is that although I have the value 420 in the x1.txt file and 210 in the y1.txt file the variables x and y do not get set to these values, they both get set to 42 somehow. Here is my code:

#include <SPI.h>
#include <SD.h>
#include <SPI.h>
#include "Adafruit_GFX.h"
#include "Adafruit_RA8875.h"


// Library only supports hardware SPI at this time
// Connect SCLK to UNO Digital #13 (Hardware SPI clock)
// Connect MISO to UNO Digital #12 (Hardware SPI MISO)
// Connect MOSI to UNO Digital #11 (Hardware SPI MOSI)
#define RA8875_INT 3
#define RA8875_CS 10
#define RA8875_RESET 9

Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RESET);
uint16_t tx, ty;

File myFile;

// change this to match your SD shield or module;
//     Arduino Ethernet shield: pin 4
//     Adafruit SD shields and modules: pin 10
//     Sparkfun SD shield: pin 8
const int chipSelect = 4;

float x = 0;
float y = 0;

void setup()
{
 // Open serial communications and wait for port to open:
  Serial.begin(9600);
   while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }

  Serial.begin(9600);
  Serial.println("RA8875 start");

  /* Initialise the display using 'RA8875_480x272' or 'RA8875_800x480' */
  if (!tft.begin(RA8875_800x480)) {
    Serial.println("RA8875 Not Found!");
    while (1);
  }

  Serial.println("Found RA8875");

  tft.displayOn(true);
  tft.GPIOX(true);      // Enable TFT - display enable tied to GPIOX
  tft.PWM1config(true, RA8875_PWM_CLK_DIV1024); // PWM output for backlight
  tft.PWM1out(255);

  Serial.print("Initializing SD card...");
  // On the Ethernet Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin 
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output 
  // or the SD library functions will not work. 
   pinMode(SS, OUTPUT);
   
  if (!SD.begin(chipSelect)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
  
  pinMode(RA8875_INT, INPUT);
  digitalWrite(RA8875_INT, HIGH);
  
  tft.touchEnable(true);
}

void loop()
{
  //open the file for reading:
  myFile = SD.open("x1.txt");
  if (myFile) {
    Serial.println("x1.txt:");
    
    // read from the file until there's nothing else in it:
    while (myFile.available()) {
    	x = (myFile.read());
    }
    // close the file:
    myFile.close();
  } else {
  	// if the file didn't open, print an error:
    Serial.println("error opening x1.txt");
  }
  myFile = SD.open("y1.txt");
  if (myFile) {
    Serial.println("y1.txt:");
    
    // read from the file until there's nothing else in it:
    while (myFile.available()) {
    	y = (myFile.read());
    }
    // close the file:
    myFile.close();
  } else {
  	// if the file didn't open, print an error:
    Serial.println("error opening y1.txt");
  }
  
  float xScale = 1024.0F/tft.width();
  float yScale = 1024.0F/tft.height();
  /* Wait around for touch events */
  if (! digitalRead(RA8875_INT)) 
  {
    if (tft.touched()) 
    {
      tft.touchRead(&tx, &ty);
      /* Draw a circle */
      tft.fillCircle((uint16_t)(tx/xScale), (uint16_t)(ty/yScale), 4, RA8875_WHITE);
    } 
  }
  
  Serial.println(x);
  Serial.println(y);
  
  tft.fillCircle(x, y, 4, RA8875_GREEN);
  delay (1000);
}

Another issue I am running into is the SD card and the screen frequently have problems when in the same sketch forcing me to unplug and reset the arduino to get them working again. It might be because they both use SPI but I am not sure.

From the examples I can easily read the entire file with myFile.read()

No, you can't. read() reads one character. There are other overloads that take arguments that allow you to read more than one character. If you have fixed length records, you could read an entire record in one shot.

    while (myFile.available()) {
    	x = (myFile.read());
    }

The parentheses around myFile.read() are not needed. The result of this while loop is that the last character from the file is stored in x. That hardly seems like what you want to do.

    while (myFile.available()) {
    	y = (myFile.read());
    }

Similarly, the last character from this file is stored in y. Again, not likely what you want.

I figured I could just put one entry per file.

So, you plan to have 200 files? Not a good plan, if speed is any kind of issue.

PaulS:

From the examples I can easily read the entire file with myFile.read()

No, you can't. read() reads one character. There are other overloads that take arguments that allow you to read more than one character. If you have fixed length records, you could read an entire record in one shot.

    while (myFile.available()) {

x = (myFile.read());
    }



The parentheses around myFile.read() are not needed. The result of this while loop is that the last character from the file is stored in x. That hardly seems like what you want to do.

I am a little confused by this. If I understand correctly, that portion of the code will read one character at a time starting with the first and going to the end. So it is overwriting the x value each time so it should end with the value of the last character in the file. Why do both x and y end up being equal to 42 when the only information in the x1.txt file is 420 and 210 in the y1.txt file? Shouldn't that end up with x and y equal to 0? How can I set my variables, x and y, to be equal to one entire line instead of one character?

PaulS:

From the examples I can easily read the entire file with myFile.read()

So, you plan to have 200 files? Not a good plan, if speed is any kind of issue.

I guess that was a stupid Idea but that leads me back to reading one line at a time. I imagine it can be done with a for loop instead of a while loop. Does the SD.remove("unwanted.txt") function for deleting files work in a similar way so that it only deletes one character at a time or does it simply delete the entire file at once? If it does it all at once, how can I overwrite one line or delete one line at a time when the number of entries gets to a desired level?
Thanks for the help

Why do both x and y end up being equal to 42 when the only information in the x1.txt file is 420 and 210 in the y1.txt file?

I can't see your file, so I can't answer that. What I suspect, though, is that both files have the same last character, whatever that character is (asciitable.com says that 42 is a '*', so look for that in your files).

How can I set my variables, x and y, to be equal to one entire line instead of one character?

x and y have to be char arrays. You need to write to the next element of the array each time you read a character, and append a NULL after that character. Then, use atof() to convert the array contents to a float value.

I imagine it can be done with a for loop instead of a while loop.

Actually, it needs to be done with two while loops - one reading the whole file, and the other reading each character, ending when it finds an end of record marker. Process the data read after the inner while loop ends.

Does the SD.remove("unwanted.txt") function for deleting files work in a similar way so that it only deletes one character at a time or does it simply delete the entire file at once?

It deletes the file.

If it does it all at once, how can I overwrite one line or delete one line at a time when the number of entries gets to a desired level?

That depends on whether the file has fixed length records, or not. Writing the nth record of a file when the records are all the same length is a matter of positioning to the start of the nth record, based on the length of the preceding n-1 records (all the same) and writing the new record.

For variable length records, you need to open the file that contains the data to keep, read the part to keep, writing it to another file, then closing the first file. Then, you write the new data to the second file, and close it. Delete the first file, and rename the second file.

By now, I'm sure you see the advantage of fixed length records.

I'm gonna have to spend some time studying how to use the character array and nested while function and I will post beck here if I get stuck again with that. What would be the function used inside the second while loop for reading the ith to the nth character in the file? I assume it is with the myFile.read() function but I'm not sure how to tell it to read a certain character. So, I guess the library has a code for every character so I need to save the readings as a character and convert later? My plan is to record the data at certain rate and keep 24 hr of data so the file can be fixed length however, I am unsure of how to create a fixed length .txt file. Can you explain to me, or point me in the direction of how to limit my text files to only allow a certain number of recordings? I have attached a copy of the humidity and temperature files.

HUMIDITY.TXT (1.39 KB)

TEMP.TXT (1.39 KB)

I am unsure of how to create a fixed length .txt file.

You control the number of characters in a record. If you are using dtostrf() to convert the float to a string, you can then pad that string to a consistent number of characters. If you are using File::print() to directly print the float to the file, you'll need to stop doing that.

Do you mean that you control the number of characters in each record so that the total number of entries is equal to the total number or charters divided by the number of characters per record? If so, I am still unsure of how to delete specific entries once the total number of entries gets to a desired number. Why is it bad to use myFile.print()? Printing the sensor values directly to the sd card keeps them the same number of characters each entry, so I am confused why I should not do it this way. Check out the files attached to my last post, they show that each entry is the same number of characters. Here is my poorly commented code that records the humidity and temperature from a sensor to the sd card:

#include <SPI.h>
#include <SD.h>
#include "Adafruit_GFX.h"
#include "Adafruit_RA8875.h"
#include <Wire.h>
#include <HIH61XX.h>


// Library only supports hardware SPI at this time
// Connect SCLK to UNO Digital #13 (Hardware SPI clock)
// Connect MISO to UNO Digital #12 (Hardware SPI MISO)
// Connect MOSI to UNO Digital #11 (Hardware SPI MOSI)
#define RA8875_INT 3
#define RA8875_CS 10
#define RA8875_RESET 9

Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RESET);
uint16_t tx, ty;

HIH61XX hih(0x27);

File myFile;

// change this to match your SD shield or module;
//     Arduino Ethernet shield: pin 4
//     Adafruit SD shields and modules: pin 10
//     Sparkfun SD shield: pin 8
const int chipSelect = 4;

float x = 0;
float y = 0;
int temp = 0;
char string[8] = "0.00000";

void setup()
{
 // Open serial communications and wait for port to open:
  Serial.begin(9600);
   while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }

  Serial.begin(9600);
  Wire.begin();
  Serial.println("RA8875 start");

  /* Initialise the display using 'RA8875_480x272' or 'RA8875_800x480' */
  if (!tft.begin(RA8875_800x480)) {
    Serial.println("RA8875 Not Found!");
    while (1);
  }

  Serial.println("Found RA8875");

  tft.displayOn(true);
  tft.GPIOX(true);      // Enable TFT - display enable tied to GPIOX
  tft.PWM1config(true, RA8875_PWM_CLK_DIV1024); // PWM output for backlight
  tft.PWM1out(255);

  Serial.print("Initializing SD card...");
  // On the Ethernet Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin 
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output 
  // or the SD library functions will not work. 
   pinMode(SS, OUTPUT);
   
  if (!SD.begin(chipSelect)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
  
  pinMode(RA8875_INT, INPUT);
  digitalWrite(RA8875_INT, HIGH);
  
  tft.touchEnable(true);
  tft.textMode();
}

void loop()
{ 
  //  start the sensor
  hih.start();
  //  request an update of the humidity and temperature
  hih.update();

  Serial.print("Humidity: ");
  Serial.print(hih.humidity(), 5);
  Serial.print(" RH (");
  Serial.print(hih.humidity_Raw());
  Serial.println(")");

  Serial.print("Temperature: ");
  Serial.print(hih.temperature(), 5);
  Serial.println(" C (");
  Serial.print(hih.temperature_Raw());
  Serial.println(")");
  
    myFile = SD.open("HUMIDITY.txt", FILE_WRITE);
  
  // if the file opened okay, write to it:
  if (myFile) {
    myFile.println(hih.humidity(), 3);
	// close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening HUMIDITY.txt");
  }
  
    myFile = SD.open("TEMP.txt", FILE_WRITE);  
  // if the file opened okay, write to it:
  if (myFile) {
    myFile.println(hih.temperature()*1.8+32);
	// close the file:
    myFile.close();
  } else {
    // if the file didn't open, print an error:
    Serial.println("error opening TEMP.txt");
  }
  
  //open the file for reading:
  myFile = SD.open("x1.txt");
  if (myFile) {
    Serial.println("x1.txt:");
    
    // read from the file until there's nothing else in it:
    while (myFile.available()) {
    	x = (myFile.read());
    }
    // close the file:
    myFile.close();
  } else {
  	// if the file didn't open, print an error:
    Serial.println("error opening x1.txt");
  }
  myFile = SD.open("y1.txt");
  if (myFile) {
    Serial.println("y1.txt:");
    
    // read from the file until there's nothing else in it:
    while (myFile.available()) {
    	y = (myFile.read());
    }
    // close the file:
    myFile.close();
  } else {
  	// if the file didn't open, print an error:
    Serial.println("error opening y1.txt");
  }
  
  float xScale = 1024.0F/tft.width();
  float yScale = 1024.0F/tft.height();
  /* Wait around for touch events */
  if (! digitalRead(RA8875_INT)) 
  {
    if (tft.touched()) 
    {
      tft.touchRead(&tx, &ty);
      /* Draw a circle */
      tft.fillCircle((uint16_t)(tx/xScale), (uint16_t)(ty/yScale), 4, RA8875_WHITE);
    } 
  }
  
  Serial.println(x);
  Serial.println(y);
  
  tft.fillCircle(x, y, 4, RA8875_GREEN);
  
  tft.textSetCursor(10, 10);
  tft.textColor(RA8875_WHITE, RA8875_RED);
  tft.textWrite("Temperature: ");
  tft.textWrite(string);

  delay (1000);
}
char string[8] = "0.00000";

The compiler can count. If you are going to define what to initialize the array to, don't define the length. If you are going to define the length, the compiler will happily initialize all the elements to NULL for you.

Do you mean that you control the number of characters in each record so that the total number of entries is equal to the total number or charters divided by the number of characters per record?

Too many undefined or misspelled terms in that statement for me to follow.

By "fixed length records" I mean that every record contains the same (KNOWN) number of characters. The Print::print() function does NOT ensure that the output is a consistent length.

char stuff[16];
dtostrf(hih.humidity(), 8, 3, stuff);

ensures that stuff contains not less than 8 characters. There MAY be more.

char fixed[16];
sprintf(fixed, "%15s", stuff);

ensures that fixed contains EXACTLY 15 characters - with leading spaces as needed.

Why is it bad to use myFile.print()? Printing the sensor values directly to the sd card keeps them the same number of characters each entry, so I am confused why I should not do it this way.

Stick that probe in a pot of boiling water. What happens when it goes over 99.999 degrees?

Stick it outside in Alaska in the middle of winter. Say, around June. What happens when the temperature goes below 0?