Project
This device keeps track of items in the user’s refrigerator as well as their expiry dates, displaying items that are going to expire in 3 or fewer days, and the user can add and delete items with pre-set names and expiry dates, custom dates, or both custom names and dates.
Below is a couple seconds of interaction with the device:
Below is an image of the device with its internal components tucked inside:
Below is an image of the device with its internal components outside:
Below are videos of various device capabilities:
Adding item w pre-set name and expiry date
Adding item w pre-set name and custom expiry date
Adding item w custom expiry date/name
Deleting item
Process Images
Below is a sketch from my ideation process, when I was drawing out different devices I wanted to build:
For my prototype, I constructed a setup with 2 buttons that were each hard coded to different items that I wanted to add (cilantro, spinach), thinking that if I was able to add and remove items through these buttons, I could achieve something similar with pressing buttons on the remote. Below is an image of that prototype
Finally, after I had finished the electrical and software-related portions of my project, I had to design the box that would display my screen and have a window for the infrared receiver to receive a signal from my remote. I had to measure the length and width of the lcd and determine a height at which it could be situated, and to create the window for the IR receiver, I had to determine the height and width at which it was visible when the breadboard was placed inside the box.
A key decision point in my process was using a list of structs instead of a linked list to store all of the items. This was simply because structs were more appropriate: I could index a specific part of the list to delete it, instead of relying on iterating through all of the nodes in the linked list through a while loop, which would cause more complications. Also, since there weren’t many items I would have in my fridge at any one time, I decided that I didn’t need an unbounded linked list so I could infinitely add items, and instead I enabled them to be placed in a 15-struct array. This greatly simplified the process of adding and deleting items, as well as iterating through items that were expiring.
Below was my linked list definition:
class groceryItem { public: int dTE; char* expiryDate; char* name; groceryItem* next; groceryItem* back; };
Below is my struct definition:
struct listItem{ char* item = ""; byte dTE; byte ref; //this is the reference number of days til expiry, this is set during the "setup" stage byte dateAdded[2]; //day, month byte amount; //will be 0, 1, or 2 byte replacementDate[2]; //day, month bool isEmpty; //we just need to check for this one thing byte replacementRef; //we're only gonna use this for custom items };
Another key decision point in my process was choosing to include a T9 system to add some complexity to my project. This idea came to me after I had struggled to get addition and deletion working with linked lists, and once I realized that addition and deletion was going to be made much easier with structs, I decided to increase the complexity of my project and challenge myself to implement a timing system with IR receiver signals that would allow for addition of custom names and expiry dates. Overall, this challenge enabled me to make my project more versatile and suited for my personal use, where I have to keep track of random Indian cooking ingredients and dairy products with pre-set expiries.
Discussion
When I set about completing this project, I anticipated having difficulty in figuring out how to enable adding items with custom dates and names using the t-9 system. What I didn’t realize was that understanding how to use multiple calls to receiver.decode() and enabling custom item name adding would take up most of my time. I had to learn, through trial and error with receiver.decode() and by looking at the (comparatively simple) IR receiver code example, that I should structure my code such that only actions directly using the decoded signal should be inside the if(receiver.decode(&results)) statement, and all other actions (i.e. resetting timers so as to move to next letter and listen to signals) should be outside of the statement. I’d also forgotten how strings worked in C++, and it took some time to figure out where and how to update a string in my code letter by letter without completely overwriting the previous letters.
Overall, I’m pleased that I reached my goal of building the functioning T-9 system that could be used to add custom names and dates, having the expiring items loop across the screen, and enabling adding and deletion of items. However, next time I work with a new electronic component, I’ll look for more complicated code examples in order to get a better understanding of the corresponding commands, and I’ll space my programming out across several days so I don’t run into several technical bugs the day before a deadline. I also agree with my classmate’s critique (regarding my physical setup) that “instead of a white box, have a clear acrylic box or even a black box to store the electronic components? Just for aesthetic purposes”. In the future, I’d learn laser cutting and plan my physical design early on, so I could use the Hunt laser cutter during the weekdays and not have to rely on using the materials I have in my apartment the night before.
As for working on the project in the future, one classmate had an interesting idea of including a “scrolling system to move through each item manually if you just want to look at one expiration date”. This would be very useful for pinpointing a specific item’s expiry date without having to dig through the fridge to find the item and determine if it’s expired. I also liked another classmate’s idea of including a distance sensor so the screen was only active when it was being viewed.
Technical Information
Block Diagram
Schematic
Code
/* 60-223, Project 2 * Sunjana Kulkarni (sunjanak) * time spent: 20 hours * No Collaboration * Project description: Information about each item * in your refrigerator is stored through an array of structs * You can add items that have pre-set names and expiry dates * by using the "up" arrow on the remote and pressing a number from 0 to 4, * you can add items with pre-set names and a custom expiry date by * using the "up" arrow and pressing a number from 5-9, and you can add a completely * custom item by pressing the "up" arrow and pressing "EQ", and then typing in * the name and date of your item. You can delete any item * with a pre-set name by pressing the corresponding number button of that item. The code * calculates expiry dates using an RTC module, which gets the current date. * * PinMapping * * Arduino Pin | type | description * --------------------------------- * 11 input connecting to IR reciever * A4 input connecting to RTC * A5 input connecting to RTC * SDA output lcd * SCL output lcd */ /* I utilized rtc module and IR reciever syntax from introductory source code found on both the Ideate website and within * Arduino library, in my setup function. */ #include <IRremote.h> #include "RTClib.h" #include <LiquidCrystal_I2C.h> using namespace std; DateTime now; RTC_DS3231 rtc; //taken from this site byte RECV_PIN = 11; bool addMode; bool deleteMode; bool custom = false; byte addNum; byte deleteNum; //void showDate(void); unsigned long addTimer = 0; unsigned long deleteTimer = 0; IRrecv receiver(RECV_PIN); // Create a new receiver object that would decode signals to key codes decode_results results; // A variable that would be used by receiver to put the key code LiquidCrystal_I2C lcd(0x27, 20, 4); const byte list_length = 15; struct listItem{ char* item = ""; byte dTE; byte ref; //this is the reference number of days til expiry, this is set during the "setup" stage byte dateAdded[2]; //day, month byte amount; //will be 0, 1, or 2 byte replacementDate[2]; //day, month bool isEmpty; //we just need to check for this one thing byte replacementRef; //we're only gonna use this for custom items }; listItem groceryList[list_length]; //have some builtins where u have to input the expiry date (still needs nokia system) //builtins with expiry date: milk, cheddar, sour cream, yogurt, eggs byte pointer = 0; byte totalCustom = 0; //total amount of custom items in our list byte freeCustom = 10; //the next free spot to put a custom item byte freeCD = -1; //we set this to whatever custom date item we're adding bool wasReset = false; //sets to true every time appears on screen, sets to false right after unsigned long lrdt = 0; bool nokiaMode = false; unsigned long nok = millis(); byte cP = 0; //cursor position byte presses = 0; char* item = ""; //how wide the board is char* letter; bool finishedItem = false; //this is set to true when we finish inputting the item, we still gotta input the date byte dateItemsAdded = 0; //once this hits 3, we've added everything byte expiryDay = 0; byte expiryMonth = 0; byte currNum = 0; bool buttonWasPressed = false; void setup() { lcd.init(); lcd.backlight(); Serial.begin(9600); //flash items about to expire in a loop receiver.enableIRIn(); // Enable receiver so that it would start processing infrared signals lcd.home(); lcd.print("hello, world!"); delay(100); lcd.clear(); if (! rtc.begin()) { while (1); } if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); for(byte i = 0; i < list_length; i++){ groceryList[i].amount = 0; switch(i){ case 0: groceryList[i].isEmpty = true; groceryList[i].item = "cilantro"; groceryList[i].ref = 3; break; case 1: groceryList[i].isEmpty = true; groceryList[i].item = "spinach"; groceryList[i].ref = 3; break; case 2: groceryList[i].isEmpty = true; groceryList[i].item = "jalapenos"; groceryList[i].ref = 7; break; case 3: groceryList[i].isEmpty = true; groceryList[i].item = "daal"; groceryList[i].ref = 4; break; case 4: groceryList[i].isEmpty = true; groceryList[i].item = "curry"; groceryList[i].ref = 3; break; //everything 5 onwards doesn't have a reference expiry, so just put dummy numbers case 5: groceryList[i].isEmpty = true; groceryList[i].item = "milk"; groceryList[i].ref = 2000; break; case 6: groceryList[i].isEmpty = true; groceryList[i].item = "cheese"; groceryList[i].ref = 2000; break; case 7: groceryList[i].isEmpty = true; groceryList[i].item = "sour crm"; groceryList[i].ref = 2000; break; case 8: groceryList[i].isEmpty = true; groceryList[i].item = "yogurt"; groceryList[i].ref = 2000; break; case 9: groceryList[i].isEmpty = true; groceryList[i].item = "eggs"; groceryList[i].ref = 2000; break; } } } bool copied = false; byte regularHasher(String hex){ if(hex == "ff6897"){ return 0; } else if(hex == "ff30cf"){ return 1; } else if(hex == "ff18e7"){ return 2; } else if(hex == "ff7a85"){ return 3; } else if(hex == "ff10ef"){ return 4; } else if(hex == "ff38c7"){ return 5; } else if(hex == "ff5aa5"){ return 6; } else if(hex == "ff42bd"){ return 7; } else if(hex == "ff4ab5"){ return 8; } else if(hex == "ff52ad"){ return 9; } else{ return -1; } } char* convertFromPress(byte presses, byte ind){ switch(ind){ case 1: switch(presses % 3){ case 1: return "a"; break; case 2: return "b"; break; case 0: return "c"; break; } break; case 2: switch(presses % 3){ case 1: return "d"; break; case 2: return "e"; break; case 0: return "f"; break; } break; case 3: switch(presses % 3){ case 1: return "g"; break; case 2: return "h"; break; case 0: return "i"; break; } break; case 4: switch(presses % 3){ case 1: return "j"; break; case 2: return "k"; break; case 0: return "l"; break; } break; case 5: switch(presses % 3){ case 1: return "m"; break; case 2: return "n"; break; case 0: return "o"; break; } break; case 6: switch(presses % 3){ case 1: return "p"; break; case 2: return "q"; break; case 0: return "r"; break; } break; case 7: switch(presses % 3){ case 1: return "s"; break; case 2: return "t"; break; case 0: return "u"; break; } break; case 8: switch(presses % 3){ case 1: return "v"; break; case 2: return "w"; break; case 0: return "x"; break; } break; case 9: switch(presses % 2){ case 1: return "y"; break; case 0: return "z"; break; } } } void deleteIt(byte ind){ //we're only going to delete one item from this, so there could still be an item left if(ind > 9){ freeCustom = ind; } if(groceryList[ind].amount == 0){ lcd.setCursor(0, 0); lcd.print("There is no"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); delay(500); lcd.clear(); } else if(groceryList[ind].amount == 1){ groceryList[ind].amount -= 1; groceryList[ind].isEmpty = true; groceryList[ind].dTE = 0; groceryList[ind].dateAdded[0] = 0; groceryList[ind].dateAdded[1] = 0; lcd.setCursor(0, 0); lcd.print("Just deleted"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); delay(500); lcd.clear(); } else{ groceryList[ind].amount -= 1; if(ind > 4){ groceryList[ind].ref = groceryList[ind].replacementRef; groceryList[ind].replacementRef = 0; } groceryList[ind].dTE = groceryList[ind].ref; groceryList[ind].dateAdded[0] = groceryList[ind].replacementDate[0]; groceryList[ind].dateAdded[1] = groceryList[ind].replacementDate[1]; groceryList[ind].replacementDate[0] = 0; groceryList[ind].replacementDate[1] = 0; lcd.setCursor(0, 0); lcd.print("Just deleted one of"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); delay(500); lcd.clear(); } } void addItem(byte ind){ if(ind < 5){ if(groceryList[ind].amount == 2){ lcd.setCursor(0, 0); lcd.print("Can't add more"); lcd.setCursor(0, 1); lcd.print("Sorry :("); delay(500); lcd.clear(); } else if(groceryList[ind].amount == 1){ //not changing dTE just yet groceryList[ind].amount += 1; groceryList[ind].replacementDate[0] = now.day(); groceryList[ind].replacementDate[1] = now.month(); //adding the replacement date lcd.setCursor(0, 0); lcd.print("Just added another"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); delay(500); lcd.clear(); } else{ //filling in the dateAdded groceryList[ind].dateAdded[0] = now.day(); groceryList[ind].dateAdded[1] = now.month(); groceryList[ind].dTE = groceryList[ind].ref; groceryList[ind].isEmpty = false; groceryList[ind].amount += 1; lcd.setCursor(0, 0); lcd.print("Just added"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); delay(500); lcd.clear(); } } else{ freeCD = ind; nokiaMode = true; } } //this is for custom date or custom item and date, either way, we've set the ref void addCustom(byte ind){ if(groceryList[ind].amount == 2 or (groceryList[ind].amount == 1 and custom)){ lcd.clear(); lcd.setCursor(0, 0); lcd.print("Can't add more"); lcd.setCursor(0, 1); lcd.print("Sorry :("); delay(500); lcd.clear(); } else if(groceryList[ind].amount == 1 and !custom){ //not changing dTE just yet lcd.clear(); groceryList[ind].amount += 1; groceryList[ind].replacementDate[0] = now.day(); groceryList[ind].replacementDate[1] = now.month(); //adding the replacement date lcd.setCursor(0, 0); lcd.print("Just added another"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); delay(500); lcd.clear(); } else{ //filling in the dateAdded lcd.clear(); groceryList[ind].dateAdded[0] = now.day(); groceryList[ind].dateAdded[1] = now.month(); groceryList[ind].dTE = groceryList[ind].ref; groceryList[ind].isEmpty = false; groceryList[ind].amount += 1; lcd.setCursor(0, 0); lcd.print("Just added"); lcd.setCursor(0, 1); lcd.print(groceryList[ind].item); // lcd.setCursor(0, 2); // lcd.print(groceryList[ind].ref); delay(500); lcd.clear(); } } //returns 1 when done bool deleteAll(){ for(byte i = 0; i < 20; i++){ if(!groceryList[i].isEmpty){ groceryList[i].amount = 0; groceryList[i].isEmpty = true; groceryList[i].dTE = 0; groceryList[i].dateAdded[0] = 0; groceryList[i].dateAdded[1] = 0; groceryList[i].replacementDate[0] = 0; groceryList[i].replacementDate[1] = 0; } } return true; } //we flash that something is expired the whole day it is expired void loop() { now = rtc.now(); if(!nokiaMode and !custom){ //once we've entered nokia mode, we're not leaving until we explicitly press the necessary button now = rtc.now(); if(pointer >= list_length){ pointer = 0; } //once the groceryList item stops being empty, we give it a ref that we calculated already if(!groceryList[pointer].isEmpty){ //recalculating the days til expiry if(now.month() == groceryList[pointer].dateAdded[1]){ groceryList[pointer].dTE = groceryList[pointer].ref - (now.day() - groceryList[pointer].dateAdded[0]); } else{ //we're just giving every month a default value of 31 days cuz we're in march now groceryList[pointer].dTE = groceryList[pointer].ref - ((now.day() + (31 - groceryList[pointer].dateAdded[0])) - groceryList[pointer].dateAdded[0]); } } //not bothering with the year conversion yet if(wasReset == false){ lcd.setCursor(0, 0); if(groceryList[pointer].isEmpty == false and groceryList[pointer].dTE <= 3 and !addMode and !deleteMode){ wasReset = true; // lcd.print(pointer); lcd.print(groceryList[pointer].item); lcd.print(" expires in "); lcd.setCursor(0, 1); lcd.print(groceryList[pointer].dTE); lcd.print(" days"); } else{ pointer = pointer + 1; //skipping over the empty ones } lrdt = millis(); } if(millis() - lrdt >= 1000 and wasReset){ wasReset = false; lcd.clear(); if(pointer + 1 < list_length){ pointer = pointer + 1; } else{ pointer = 0; } } if (receiver.decode(&results)) { // Decode the button code and put it in "results" variable String result = String(results.value, HEX); if(result == "ff906f"){ //entering adding case lcd.clear(); lcd.setCursor(0, 0); lcd.print("adding.."); addMode = true; addTimer = millis(); } if(result == "ffe01f"){ //entering deletion case lcd.clear(); lcd.setCursor(0, 0); lcd.print("deleting.."); deleteMode = true; deleteTimer = millis(); } if(addMode and millis()-addTimer < 3000 and regularHasher(String(results.value, HEX)) > -1 and regularHasher(String(results.value, HEX)) <= 9){ byte res = regularHasher(String(results.value, HEX)); addItem(res); addMode = false; } if(addMode and millis()-addTimer < 3000 and result == "ff9867"){ custom = true; nokiaMode = true; addMode = false; } if(deleteMode and millis()-deleteTimer < 3000 and regularHasher(String(results.value, HEX)) > -1 and regularHasher(String(results.value, HEX)) <= 9){ byte res = regularHasher(String(results.value, HEX)); deleteIt(res); deleteMode = false; } receiver.resume(); // Continue listening for new signals } if(addMode and millis()-addTimer >= 3000){ lcd.setCursor(0, 0); lcd.print("no add? ok"); if(millis()-addTimer >= 4000){ addMode = false; lcd.clear(); } } if(deleteMode and millis()-deleteTimer >= 3000){ lcd.setCursor(0, 0); lcd.print("no delete? ok"); if(millis()-addTimer >= 4000){ deleteMode = false; lcd.clear(); } } //you can only add one of each custom item delay(100); //this adds a space at the end of the list } else{ now = rtc.now(); //we press 0 to submit a string // will add these lines back in after figuring out nokia // if(custom and !finishedItem){ lcd.setCursor(0, 0); lcd.print("type your item"); lcd.setCursor(0, 2); lcd.print("press stop to clear"); lcd.setCursor(0, 3); lcd.print("finished? press up"); if(receiver.decode(&results) and !finishedItem){ Serial.println(String(results.value, HEX)); if(String(results.value, HEX) == "ffe21d"){ lcd.clear(); cP = 0; presses = 0; receiver.resume(); } if(regularHasher(String(results.value, HEX)) > 0 and regularHasher(String(results.value, HEX)) <= 9 and !(millis()-nok >= 1000 and buttonWasPressed)){ buttonWasPressed = true; nok = millis(); //we're resetting this every time we press the button presses += 1; letter = convertFromPress(presses, regularHasher(String(results.value, HEX))); lcd.setCursor(cP, 1); lcd.print(letter); //this'll keep updating as long as you keep pressing receiver.resume(); } if(String(results.value, HEX) == "ff906f"){ Serial.println(item); finishedItem = true; lcd.clear(); cP = 0; presses = 0; receiver.resume(); } else{ receiver.resume(); } } if(!finishedItem and millis()-nok >= 1000 and buttonWasPressed){ //moving to the next letter, should only happen after you've stopped pressing groceryList[freeCustom].item = strcat(groceryList[freeCustom].item, letter); if(cP < 19){ cP += 1; } else{ cP = 0; } presses = 0; nok = millis(); buttonWasPressed = false; } } if((!custom and dateItemsAdded < 2 and nokiaMode) or (custom and finishedItem)){ lcd.setCursor(0, 0); if(dateItemsAdded == 0){ lcd.print("type expiry day"); } if(dateItemsAdded == 1){ lcd.print("type expiry month"); } lcd.setCursor(0, 2); lcd.print("press stop to clear"); lcd.setCursor(0, 3); lcd.print("finished? press up"); Serial.println(expiryDay); Serial.println(expiryMonth); if(receiver.decode(&results) and dateItemsAdded < 2){ String res = String(results.value, HEX); if(res == "ff906f"){ dateItemsAdded += 1; lcd.clear(); cP = 0; presses = 0; buttonWasPressed = false; receiver.resume(); } if(res == "ffe21d"){ lcd.clear(); cP = 0; presses = 0; if(dateItemsAdded == 0){ expiryDay = 0; } if(dateItemsAdded == 1){ expiryMonth = 0; } receiver.resume(); } if(!(millis()-nok >= 1000 and buttonWasPressed) and cP <= 2){ if(regularHasher(res) > -1 and regularHasher(res) <= 9){ buttonWasPressed = true; nok = millis(); //we're resetting this every time we press the button presses += 1; currNum = regularHasher(String(results.value, HEX)); lcd.setCursor(cP, 1); lcd.print(currNum); //this'll keep updating as long as you keep pressing receiver.resume(); } receiver.resume(); } } if(millis()-nok >= 1000 and buttonWasPressed){ if(dateItemsAdded == 0){ expiryDay = expiryDay * 10; expiryDay += currNum; } if(dateItemsAdded == 1){ expiryMonth = expiryMonth * 10; expiryMonth += currNum; } currNum = 0; if(cP < 2){ cP += 1; } presses = 0; nok = millis(); buttonWasPressed = false; } } if(dateItemsAdded == 2 and nokiaMode){ //now adding the ref for the custom items if(custom and totalCustom < list_length-9){ //adding it to freeCustom if(expiryMonth == (byte)now.month() or (expiryMonth > 12 or expiryMonth < 1)){ groceryList[freeCustom].ref = expiryDay - (byte)now.day(); addCustom(freeCustom); } else{ groceryList[freeCustom].ref = 30 * (expiryMonth - ((byte)now.month() + 1)) + expiryDay + (31 - (byte)now.day()); addCustom(freeCustom); } } else{ Serial.println((byte)now.day()); Serial.println((byte)now.month()); Serial.println(expiryDay); Serial.println(expiryMonth); delay(100); if((byte)expiryMonth == (byte)now.month() or (expiryMonth > 12 or expiryMonth < 1)){ if(groceryList[freeCD].amount > 0){ groceryList[freeCD].replacementRef = (byte)expiryDay - (byte)now.day(); addCustom(freeCD); } else{ groceryList[freeCD].ref = (byte)expiryDay - (byte)now.day(); addCustom(freeCD); } } else{ if(groceryList[freeCD].amount > 0){ groceryList[freeCD].replacementRef = 30 * ((byte)expiryMonth - ((byte)now.month() + 1)) + (byte)expiryDay + (31 - (byte)now.day()); addCustom(freeCD); } else{ groceryList[freeCD].ref = 30 * ((byte)expiryMonth - ((byte)now.month() + 1)) + (byte)expiryDay + (31 - (byte)now.day()); addCustom(freeCD); } } } } if(dateItemsAdded == 2 and nokiaMode){ //we've just added our thing if(custom){ freeCustom += 1; totalCustom += 1; } custom = false; dateItemsAdded = 0; nok = millis(); cP = 0; //cursor position presses = 0; finishedItem = false; //this is set to true when we finish inputting the item, we still gotta input the date dateItemsAdded = 0; //once this hits 3, we've added everything expiryDay = 0; expiryMonth = 0; currNum = 0; buttonWasPressed = false; copied = false; nokiaMode = false; } } }