Calculates the average time it takes to use a limited item, like cereal, so I know when I’m going to run out/when to replace it.

(The video is too long to be uploaded as 1 piece, so I split it into three pieces.)

Initial view averages.

Add today’s date.

View updated averages.

Overall photo for proportion and scale.

The circuitry, from left to right: Arduino, SD card module, RTC (Real Time Clock) DS1307, rotary encoder.

View average amount of time between entries (AKA how long it takes to deplete/use that thing).

Add date entry to be used in future averaging calculations (first line gives the Unix time).

Process images and review

Original idea of being able to select an item to view/add date to (being able to keep track of multiple items at a time).

At first, I wanted to be able to save multiple categories of items and select which one I wanted to add/view dates from. I planned to have each row represent an item, with columns representing the data entries. However, there wasn’t a good way for the SD card reader to be able to parse to a specific point in the csv file to insert data: it only appends at the end, which may not be the correct place in the grid for the data. So unfortunately, I reduced it to one item, and thought that perhaps I should just switch out the SD Card if I wanted a different item logged. In hindsight, perhaps I could’ve tried creating a different file in the same SD card and selecting between those, or Zach said that apparently there is a way for the Arduino to remember values after turning off/on.

Storing dates in Unix (seconds since Jan 1, 1970).

I initially planned to store values as MM/DD/YYYY format in the csv for user readability if I look at the csv file directly. At first I thought storing/retrieving data was going to be straightforward and that I was going to be able to convert between data types. But that would also mean more parsing on the Arduino end because I’d have to separate all the values and then pack them together for storage, which is inconvenient. Furthermore, Arduino wouldn’t cooperate with converting between String/int types properly. So now everything is stored as Unix (seconds since Jan 1, 1970), so it’s just one number. I don’t need to do much parsing, and it’s easier to subtract without worrying about the lengths of months…etc.

Rotary encoder interface.

I decided to use a rotary encoder for a few reasons. First, I only need to move in 2 directions in a list of options, which can be coded in the direction you turn the knob. A joystick, for example, would have too many directions of movement that I’d have to reduce to 2 directions anyways. In contrast to a potentiometer, the rotary encoder allows you to keep turning in the same direction for as long as you’d like. Also, the rotary encoder also comes with a built-in button, so the option selection aspect is also taken care of. If I had used multiple buttons to move around and select, that would be more moving parts, which could mean more sources of error, and make things unnecessarily complicated.

I also learned how to make a nice interface for the knob by measuring the diameter with a caliper and designing those dimensions in Fusion. I hadn’t really thought about designing a surface to hide all the circuitry before, but it does seem like a good idea for future projects to both protect wires from damage and to make it look cleaner. If I had more time, I would’ve made an interface for the LCD as well. While construction paper wasn’t the most sturdy, it was more durable than I thought, probably because of all the solid parts underneath.

Make index selector loop around so you don’t just get stuck at the end of the list.

I wanted the knob selection to loop back around when it reached the end of the list, because that would be convenient instead of needing to scroll all the way up again, so I used some modular arithmetic to “loop” to the beginning of the list of options. Some shenanigans came up where the pointer of the selected item disappeared from the entire list completely, and apparently this was an Arduino thing, so adding +arrLen (the length of the list) to the equation apparently fixes things, even though it is mathematically the same as before. Arduino math is slightly different sometimes. 😐

1 tick felt by human is 4 ticks as measured by rotary encoder.

At first, I thought that each tick of the rotary encoder would have a difference of 1. So I used that raw difference to calculate the index of the list. However, I noticed that although I felt 1 “tick”, the difference on the Serial monitor was between 2 to 4. This was strange. After running some tests with the example code, I realized that each tick is actually a difference of 4, and the rotary encoder was reading quickly enough to register half a tick, which isn’t what I wanted. Thus, I wrote code to make sure the difference was a multiple of 4, so that it would “wait” until I was done turning a full “tick” before calculating the index.

Make sure only 1 button press is measured per time.

As with the rotary encoder, I know the Arduino measures things very quickly, but I only want it to register 1 button press at a time, instead of seeing it “pressed” multiple times when it was just one long press. So I had a global variable to help it remember whether it was in the middle of being pressed, and to only register the press once the button was released (completed button press).

Nice easy logic to print out instead of having a bunch of different String options.

I think my method of using the > as indicator of what’s selected was simple and effective, since you can still see all the other options in the list at the same time. In addition, instead of creating a bunch of varying Strings just to only use 1 of them (since the > location varies), I can just build one String. This is done 1 option at a time, and I insert the > in the correct place within the concatenation loop, so the logic is clean and simple. I also only cleared the LCD screen whenever the options changed in order to eliminate the “fuzziness” of the screen from clearing too rapidly.

Discussion

Some of the critiques were similar to ideas I already had, such as “Add a functionality to track the average use for multiple items”, which I was my original plan if I had learned some alternative ways of getting around the SD reading/writing challenges earlier, such as storing data directly in Arduino, or using multiple csv files. For the “multiple csv files” idea however, I could have a list of the titles of each file and use the index selection to pick a file to open. That would mean hardcoding the options, which is fine because if I wanted to add a new item to track, I’d probably want to set up the csv file properly via computer instead of through the Arduino, and then it wouldn’t be much harder to change the list to accommodate the new name while I’m at it. At first, I was against hardcoding options because it didn’t seem very flexible, but I realized there wouldn’t be too many changes needed, and could make the coding logic easier later.

A critique I hadn’t really thought about was having “a warning system based on the previous statistics”, such as a message that pops up before going to the Add/View options. I was just planning on using the date tracker in a passive way, by only consulting the data when I was curious. However, I think having a message pop up would be helpful in case I forgot something was going to run out sooner than I thought.

I had to learn 2 modules for this: the RTC and SD card, as well discover some more subtle things about the rotary encoder tick measuring as described earlier. The RTC was much simpler to understand since the library documentation was well-written with comments, and also because there were less options for what kind of functionality is needed for my project: I just need to know the current date, and don’t care about using it to set alarms…etc. Unfortunately, the SD card was much harder. First, there was no one library that was the top result, but a combination of SD and SPI stuff. The example code was usually for txt files. The ones for csv just logged data as a single column, but I wanted to add a value to this line of the csv, not the other lines. Searching up the question gave forum results that were similar, but not quite the same as what I wanted. We got so close as to be able to put the “writing head” in the middle of the file and overwrite 1 character, but when I tried to replicate that with where the data was actually going to be, it went back to appending things to a newline at the end of the file. So I tried for many days to get the SD read/write to work with the csv format I wanted, but in the end I decided it would probably be easier to keep the csv file simpler so it would definitely work with example/suggested code, instead of trying to get it to work with something that even online forums didn’t give a straight answer to.

I think I did a good job of breaking the project down into different tasks, such as focusing on each module working separately before combining the code. I also worked around a lot of challenges that I hadn’t thought of, such as all of these SD card writing issues, by reducing the complexity of what was needed of the module (people said “The software complexity is a very admirable task to have taken on” and “I think it would have been very difficult to manipulate the sd card and keep track of the time and number, so nice job :)”), although it would’ve been nice to have known about those potential alternative solutions earlier. I was able to achieve the main goal of the project, which is to add and average dates. So it shouldn’t be too hard to scale this up to add more functionality, such as multiple items, using the ideas I have described above. If I have time, I may also try the technique of storing data on the Arduino instead of the SD card, although I’m not sure how much reworking of the code that would take, vs my hypothetical technique of keeping most of the code and SD card but adding a new file for each item, and scrolling through the list of (hard-coded) items to determine which time to view/add date.

Technical Information

Schematic.

/*
Date Calc
Freda Su
Goal: calculate running average of how long it takes to use an item ex: cereal
Whenever you run out/start cereal box, add date.
Check the average time it takes to use cereal so know when to replace.
Add date vs view time selected with rotating knob/button.

https://create.arduino.cc/projecthub/electropeak/sd-card-module-with-arduino-how-to-read-write-data-37f390
https://forum.arduino.cc/t/a-simple-function-for-reading-csv-text-files/328608
http://www.pjrc.com/teensy/td_libs_Encoder.html
https://github.com/cvmanjoo/RTC
https://github.com/GyverLibs/UnixTime/blob/main/examples/test/test.ino
https://courses.ideate.cmu.edu/60-223/f2022/tutorials/I2C-lcd

 Arduino pin | role   | description
 ------------|--------|-------------
 2             input    rotary encoder pin A
 3             input    rotary encoder pin B
 4             input    rotary encoder pin SWITCH
 10            output   CS pin
 11            output   MOSI
 12            input    MISO
 13            output   SCK
 SDA           both     LCD serial pin, RTC serial pin
 SCL           output   LCD clock pin, RTC clock pin
 GND           input    ground
 5V/VCC        output   5V
*/

#include <Encoder.h>
#include <Wire.h>
#include <I2C_RTC.h>
#include <UnixTime.h>
#include <LiquidCrystal_I2C.h>
#include <SD.h>
#include <SPI.h>
#define CS_PIN 10
//------------------------------------------------------------------------------
#define errorHalt(msg) {Serial.println(F(msg)); while(1);}  //something goes wrong w sd card
//------------------------------------------------------------------------------
File file;

const int SWITCH = 4;

long oldPosition  = -999;
bool flag = false; //not in the middle of being pressed
unsigned int index = 0; //menu option index
unsigned int oldIndex = 3;  //helps remember if rotated knob, dummy value, will change later
const int optArrLen = 2;  //length of the menu options array
unsigned int option = 0;  //menu option chosen

/* Create an LCD display object called "screen" with I2C address 0x27
  which is 20 columns wide and 4 rows tall. You can use any name you'd like. */
LiquidCrystal_I2C screen(0x27, 20, 4);

UnixTime stamp(-4);  //specify GMT
//not sure if this accounts for daylight savings??

static DS1307 RTC;

Encoder myEnc(2, 3);  //pins 2, 3 have interrupt capability


///////////////////////////////////// SET UP ///////////////////////////////////////////////////////
void setup() {
  Serial.begin(9600);
  pinMode(SWITCH, INPUT);
  if (!SD.begin(CS_PIN)) errorHalt("begin failed");

  // Create or open the file.
  file = SD.open("data.csv", FILE_READ);
  if (!file) errorHalt("open failed");
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB
  }
  RTC.begin();
  //RTC.setDateTime(__DATE__, __TIME__);  //only need to do once
  //RTC.startClock();
  RTC.setHourMode(CLOCK_H12);

  Serial.println();
  Serial.println("*** RTC 1307 ***");
  Serial.print("Is Clock Running : ");
  if (RTC.isRunning())
    Serial.println("Yes");
  else
    Serial.println("No. Time may be Inaccurate");
  Serial.println("Month-Day-Year");

  screen.init();

  // turn on the backlight to start
  screen.backlight();

  // set cursor to home position, i.e. the upper left corner
  screen.home();
  screen.clear();
  screen.print("righttext");
}

/////////////////////////////VOID LOOP////////////////////////////////////////////////////////////////////////

void loop() {
  //Screen 0: Log or check items?
  printOptions(index);
  while (!buttonPressDone())
  {
    index = chooseIndex(optArrLen, index);
    if (index != oldIndex)
    {
      screen.clear();
      printOptions(index);
      oldIndex = index;
    }
  }
  option = index; //index 0 = add date, 1 = check avg dates

  if (option == 0)  //added date for chooseItem = write to csv
  {
    unsigned long data = (unsigned long)(RTC.getEpoch());
    writeCSV(data);
    screen.clear();
    screen.home();
    screen.print("Writing ");
    screen.print(data);
    screen.setCursor(0,1);
    screen.print("Month: ");
    screen.print(RTC.getMonth());
    screen.setCursor(0, 2);
    screen.print("Day: ");
    screen.print(RTC.getDay());
    screen.setCursor(0, 3);
    screen.print("Year: ");
    screen.print(RTC.getYear());
    Serial.println(String(data));
    delay(5000);
    screen.clear();
  }
  else  //view dates = read line of csv, calculate average
  {
    float res = readCSV();
    screen.clear();
    screen.home();
    screen.print("Avg sec: ");
    screen.print(res);
    screen.setCursor(0, 1);
    screen.print("Avg min: ");
    res /= 60;
    screen.print(res/60);
    screen.setCursor(0, 2);
    screen.print("Avg hr: ");
    res /= 60;
    screen.print(res);
    screen.setCursor(0, 3);
    screen.print("Avg days: ");
    res /= 24;
    screen.print(res);
    delay(5000);
    screen.clear();
  }
}

///////////////////////////HELPER FUNCTIONS////////////////////////////////////////////////////////////////////////

//helps read each field of the csv
size_t readField(File* file, char* str, size_t size, char* delim) {
  char ch;
  size_t n = 0;
  while ((n + 1) < size && file->read(&ch, 1) == 1) {
    // Delete CR.
    if (ch == '\r') {
      continue;
    }
    str[n++] = ch;
    if (strchr(delim, ch)) {
        break;
    }
  }
  str[n] = '\0';
  return n;
}

//write date in unix (data) to csv
void writeCSV(unsigned long data)
{
  file = SD.open("data.csv", FILE_WRITE);
  file.println(data);
  file.close(); //save changes
}

//read dates for csv and calculate running average
float readCSV()
{
  file = SD.open("data.csv", FILE_READ);
  file.seek(0);
  float first = 0;
  float second = 0;
  float avg = 0;
  float numItems = 0;
  String temp = "";

  size_t n;      // Length of returned field with delimiter.
  char str[20];  // Must hold longest field with delimiter and zero byte.
  
  // Read the file and print fields.
  
  while (true) {
    n = readField(&file, str, sizeof(str), ",\n");
    // done if Error or at EOF.
    if (n == 0) break;

    // Print the type of delimiter.
    if (str[n-1] == ',') {
      str[n-1] = 0;
      Serial.print(str);
      Serial.print(",");
      
      // Remove the delimiter.
    }
    else if (str[n-1] == '\n')
    {
      str[n-1] = 0;
      Serial.println(str);
      if (first == 0)
      {
        temp = (String)(str);
        first = temp.toFloat();
      }
      else if (second == 0)
      {
        temp = (String)(str);
        second = temp.toFloat();
        numItems += 1.0;
        avg += abs(second - first);
      }
      else
      {
        avg += abs(second - first);
        second = first;
        temp = (String)(str);
        first = temp.toFloat();
        numItems += 1.0;
      }
    }
    else {
      // At eof, too long, or read error.  Too long is error.
      Serial.print(file.available() ? F("error: ") : F("eof:   "));
    }
    // Print the field.
  }
  file.close();

  return avg;

}

//want to only register press when "released" so doesn't pick up multiple presses for 1
bool buttonPressDone()
{
  if (digitalRead(SWITCH) == LOW)
  {
    flag = 1; //in the middle of being pressed...
  }
  else
  {
    if (flag) //was being pressed, but not anymore = full press
    {
      flag = 0; //reset
      return true;
    }
  }
  return false;
}

//use knob (rotary encoder) to select an option from menu (array)
unsigned int chooseIndex(unsigned int arrLen, unsigned int index)
{
  long newPosition = myEnc.read();

  //now want to have it cycle thru "array"
  //clockwise = decreasing value, CCW = increase
  if (newPosition != oldPosition)  //CCW
  {
    if ((newPosition - oldPosition) % 4 == 0) //want to measure whole ticks, dont measure too fast
    {
      //index movement calc
      long numTicks = (oldPosition - newPosition)/4; //each tick is 4 apart apparently?
      index = (index + numTicks + arrLen) % arrLen;
      oldPosition = newPosition;
    }
  }
  return index;
}

//used in early stages to print date to serial monitor to check if work properly
void printDate()
{
  switch (RTC.getWeek())
  {
    case 1:
      Serial.print("SUN");
      break;
    case 2:
      Serial.print("MON");
      break;
    case 3:
      Serial.print("TUE");
      break;
    case 4:
      Serial.print("WED");
      break;
    case 5:
      Serial.print("THU");
      break;
    case 6:
      Serial.print("FRI");
      break;
    case 7:
      Serial.print("SAT");
      break;
  }
  Serial.print(" ");
  Serial.print(RTC.getMonth());
  Serial.print("-");
  Serial.print(RTC.getDay());
  Serial.print("-");
  Serial.print(RTC.getYear());

  Serial.print(" ");

  Serial.print(RTC.getHours());
  Serial.print(":");
  Serial.print(RTC.getMinutes());
  Serial.print(":");
  Serial.print(RTC.getSeconds());
  if (RTC.getHourMode() == CLOCK_H12)
  {
    switch (RTC.getMeridiem())
    {
      case HOUR_AM :
        Serial.print(" AM");
        break;
      case HOUR_PM :
        Serial.print(" PM");
        break;
    }
  }
  Serial.print("Epoch: ");
  Serial.println(RTC.getEpoch());
  Serial.println();
  Serial.print("Epoch convert back: ");
  stamp.getDateTime(RTC.getEpoch());
  Serial.println(stamp.year);
  Serial.println(stamp.month);
  Serial.println(stamp.day);

  screen.clear();
  screen.autoscroll();
  // therefore, set the cursor one square off of right side of the screen
  screen.setCursor(16, 0);
  screen.print("righttext");
  screen.noAutoscroll();

}

//print menu options with arrow so easy to know which one it's on
void printOptions(unsigned int index)
{
  screen.home();
  String options[] = {"Add date", "View"};
  for (int i = 0; i < 2; i++)
    {
      String toPrint = "";
      if (i == index)
      {
        toPrint += ">";
      }
      toPrint += String(options[i]);
      screen.setCursor(0, i); //col 0, row i
      screen.print(toPrint);
    }
}