The Programmable Workout Timer is a device that times a user inputed workout and alerts the user when rest and reps should start.
The user inputs workout information before the timing process begins.
During the workout, the device uses a countdown and lights to inform the user when to begin and end exercises.
A speaker alerts the user at the end of a rep or rest period.
The back of the device can be opened to change the battery and perform maintenance.
An example transition from rep in progress to rest.
The original state transition planning for the timer.
One of the biggest challenge of this project was coordinating the input and outputs of the device. This task was particularly difficult, because the same component could interact with different variables at different times in the program. For example, the keypad is used to enter the information for multiple variables, and the display screen will display a variety of different texts at different times. Clearly, it was necessary to create some sort of organizational model to better coordinate these processes.
In order to achieve this task, I implemented a multi state system, where the functions of the device were dependent on the state. Each mode would have a different input, output, and functionality. The state choices were very natural, as each state could align with a different screen that was displayed to the user. This decision ultimately made the programming process much less chaotic and more organized.
The code for the “Set Entry” state, where the user enters the number of sets in their workout.
Another important design choice was made later in the process after testing my original prototype. I found it frustrating that when I typed a wrong number, I could not delete it like on a normal phone or laptop keypad. Therefore, I decided to turn the ‘B’ button into a backspace functionality using string manipulation. While adding the ability to backspace, I also restricted the user from inputing non number values or blank inputs. These simple changes to the keypad made the project much more user friendly and resistant to errors.
The original electronic prototype. When testing this system, I realized not being able to delete inputs was frustrating.
This project challenged me to do many tasks that I had never done before, and as a result I made many important realizations about larger scale electronics projects. The largest of these realizations is the importance of planning the physical enclosure of a circuit. I struggled to fit my circuit inside a reasonably portable box, because I assumed that if all components could fit then the circuit could fit. However, I did not account for the fact that wires took up significant space and could limit the rotation of the components. This was a large road block in developing my final product, as I had to redo the wiring for some components. If I had planned better from the start, I could have saved a lot of time. Another takeaway from the project is that I really enjoy the coding aspect of circuits involving Arduino’s. This project is the first time I’ve written Arduino code of this length and magnitude, and naturally I had to learn along the way to overcome obstacles. Learning more about the Arduino language (and C based languages in general) was a really fun and rewarding part of the process for me. In particular, learning to manipulate strings and chars was a really useful task that I would like to use again for another project.
Overall, I am very happy with the outcome of this project. It fulfills the task that I created it for, and I have used it multiple times when working out. The casing has been durable enough to withstand travel in my backpack, and it is also portable enough to fit easily. As far as the electronics go, the device effectively tracks and communicates the timing that I need it to do, and it has been a big improvement on using a conventional stopwatch in my workouts.
However, after using the device I’ve realized that there are a few additions that could be beneficial. One drawback of the current system is that it assumes all rests in a set are the same length, but occasionally I will do a sprint workout where the rests in-between reps are different. An additional mode which can program more complicated workout structures would be a useful. Furthermore, I occasionally want to extend a rest period or cut it short, so an “add 30 seconds” or “start now” feature may be a good addition. Lastly, when the track is wet I worry about damaging the electronics, so waterproofing the case could be beneficial.
When looking at written critiques, it is apparent there are other possible improvements to consider. One classmate commented, “A good modification would be having a selection of preset workouts.” I think this is a very interesting idea, but I do not think I will implement it. Almost every week I slightly modify the workouts I am doing. Therefore, while I often do similar workouts, I rarely do identical workouts, so this change would not be very beneficial for me personally. However, if I were to make this product for mass distribution, adding a preset workout feature could be more useful. Another classmate suggested “making the indicators or sounds louder or bigger,” and I agree that this should be changed in the next iteration. In the prototype, the speaker was much louder, but when using the Arduino Nano and battery power it became fainter. Using an amplifier or a louder speaker could certainly improve the quality of the project.
Schematic:
Code:
/* * Programmable Workout Timer * Justin Kiefel (jkiefel) * * Description: This project allows a user to input workout data using a * 4x3 keypad and tracks the time of the workout. It gives alerts through * a speaker, LED's, and an LCD display when a rep or rest is beginning, ending, * or almost finished. This project was designed specifically for sprint workouts, * but its functionality can be transferred to many other workouts. * * Credit: * Keypad and LCD Setup Code From - http://www.circuitbasics.com/how-to-set-up-a-keypad-on-an-arduino/ * Pitch Code From - http://www.arduino.cc/en/Tutorial/Tone * Libraries Used - LiquidCrystal_I2C, Keypad * * Summary: * * Inputs: * Arduino pin | input * 2-9 keypad * A4 LCD SDA * A5 LCD SCL * * Outputs: * Arduino pin | output * 10 speaker * 11 green LED * 12 blue LED * 13 red LED */ // keypad/lcd setup code from // #include "pitches.h" #include <Wire.h> #include<LiquidCrystal_I2C.h> #include<Keypad.h> // keypad setup const byte ROWS = 4; const byte COLS = 4; const byte rowPins[ROWS] = {9, 8, 7, 6}; const byte colPins[COLS] = {5, 4, 3, 2}; const char hexaKeys[ROWS][COLS] = { {'1', '2', '3', 'A'}, {'4', '5', '6', 'B'}, {'7', '8', '9', 'C'}, {'*', '0', '#', 'D'} }; Keypad userKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS); // lcd setup LiquidCrystal_I2C lcd(0x27, 16, 2); // pin setup const int SPEAKER_PIN = 10; const int REP_PIN = 11; const int SOON_PIN = 12; const int REST_PIN = 13; // speaker note data const int repNoteDuration[] = {8,8}; const int repNoteMelody[]{NOTE_B3, NOTE_B5}; const int restNoteDuration[] = {8, 8}; const int restNoteMelody[]{NOTE_B5, NOTE_B3}; const int soonDuration[]{4}; const int soonMelody[]{NOTE_B3}; // initializing variables int state = 0; // current device state String sets = ""; // user input number of sets String reps = ""; // user input number of reps String tpr = ""; // user input time per rep String rpr = ""; // user input rest per rep String rps = ""; // user input rest per set int setsInt = 0; // number of sets int repsInt = 0; // number of reps int tprInt = 0; // time per rep int rprInt = 0; // rest per rep int rpsInt = 0; // rest per set int copyOfReps = 0; // used to remember number of reps int startingCountdown = 4; // length of the starting countdown int lcdTimer = 0; // the current integer to be displayed on the LCD screen int prevLcdTimer = 0; // the previous integer on the LCD screen unsigned long timeMarker = 0; // used to track time at the start of a state bool sound1Done = 0; // indicates the completion of the 'starting state' outputs bool sound2Done = 0; // indicates the completion of the 'almost done' outputs void setup() { lcd.backlight(); lcd.init(); lcd.home(); pinMode(SPEAKER_PIN, OUTPUT); pinMode(REP_PIN, OUTPUT); pinMode(SOON_PIN, OUTPUT); pinMode(REST_PIN, OUTPUT); } void loop() { char userInput = userKeypad.getKey(); // gets user input if (state == 0){ // opening screen lcd.home(); lcd.print("press * to start"); if (userInput == '*'){ state = 1; lcd.clear(); } } else if (state == 1){ // set entry screen lcd.home(); lcd.print("enter # of sets"); lcd.setCursor(0,1); if (isDigit(userInput)){ // typing functionality sets = sets+String(userInput); } else if (userInput == 'B'){ // backspace functionality sets = sets.substring(0,(sets.length()-1)); lcd.clear(); Serial.println(sets); } else if (userInput == '*' and sets != ""){ // next state functionality state = 2; lcd.clear(); } lcd.print(sets); } else if (state == 2){ // rep entry screen lcd.home(); lcd.print("enter # of reps"); lcd.setCursor(0,1); if (isDigit(userInput)){ // typing functionality reps = reps+String(userInput); } else if (userInput == 'B'){ // backspace functionality reps = reps.substring(0,(reps.length()-1)); lcd.clear(); } else if (userInput == '*' and reps != ""){ // next state functionality state = 3; lcd.clear(); } lcd.print(reps); } else if (state == 3){ // time per rep entry screen lcd.home(); lcd.print("time per rep (s)?"); lcd.setCursor(0,1); if (isDigit(userInput)){ // typing functionality tpr = tpr+String(userInput); } else if (userInput == 'B'){ // backspace functionality tpr = tpr.substring(0,(tpr.length()-1)); lcd.clear(); } else if (userInput == '*' and tpr != ""){ // next state functionality state = 4; lcd.clear(); } lcd.print(tpr); } else if (state == 4){ // rest per rep entry screen lcd.home(); lcd.print("rest per rep (s)"); lcd.setCursor(0,1); if (isDigit(userInput)){ // typing functionality rpr = rpr+String(userInput); } else if (userInput == 'B'){ // backspace functionality rpr = rpr.substring(0,(rpr.length()-1)); lcd.clear(); } else if (userInput == '*' and rpr != ""){ // next state functionality state = 5; lcd.clear(); } lcd.print(rpr); } else if (state == 5){ // rest per rep entry screen lcd.home(); lcd.print("rest per set (s)"); lcd.setCursor(0,1); if (isDigit(userInput)){ // typing functionality rps = rps+String(userInput); } else if (userInput == 'B'){ // backspace functionality rps = rps.substring(0,(rps.length()-1)); lcd.clear(); } else if (userInput == '*' and rps != ""){ // next state functionality state = 6; lcd.clear(); } lcd.print(rps); } else if (state == 6){ // countdown to start startingCountdown = startingCountdown - 1; lcd.home(); lcd.print("starting in"); lcd.setCursor(0,1); int noteDuration = 1000 / soonDuration[0]; tone(SPEAKER_PIN, soonMelody[0], noteDuration); setsInt = sets.toInt() - 1; // to account for first set repsInt = reps.toInt() - 1; // to account for first rep tprInt = tpr.toInt(); rprInt = rpr.toInt(); rpsInt = rps.toInt(); copyOfReps = repsInt; delay(1000); lcd.print(startingCountdown); if (startingCountdown == 0){ state = 7; startingCountdown = 4; lcd.clear(); } } else if (state == 7){ // rep in progress if (not sound1Done){ // rep start light and sound digitalWrite(REP_PIN, HIGH); timeMarker = millis(); sound1Done = 1; for (int i = 0 ; i < 2 ; i++){ int noteDuration = 1000 / repNoteDuration[i]; tone(SPEAKER_PIN, repNoteMelody[i], noteDuration); int pause = noteDuration * 1.25; delay(pause); noTone(10); } } // updates countdown lcdTimer = (tprInt - (millis() - timeMarker)/1000); if (prevLcdTimer != lcdTimer){ lcd.clear(); prevLcdTimer = lcdTimer; } lcd.home(); lcd.print("rep in progress"); lcd.setCursor(0,1); lcd.print(lcdTimer); if (((millis() - timeMarker)/1000 > (tprInt*.75)) and (not sound2Done)) { // almost done warning sound2Done = 1; digitalWrite(SOON_PIN, HIGH); int noteDuration = 1000 / soonDuration[0]; tone(SPEAKER_PIN, soonMelody[0], noteDuration); } if ((millis() - timeMarker)/1000 > tprInt){ // next state functionality digitalWrite(REP_PIN, LOW); digitalWrite(SOON_PIN, LOW); sound1Done = 0; sound2Done = 0; lcd.clear(); if (repsInt > 0){ // if more reps in set, then go to rep rest state = 8; } else if (setsInt > 0){ repsInt = copyOfReps; state = 9; } else{ state = 0; sets = ""; reps = ""; tpr = ""; rpr = ""; rps = ""; setsInt = 0; repsInt = 0; tprInt = 0; rprInt = 0; rpsInt = 0; copyOfReps = 0; } } } else if (state == 8){ // rest between reps if(not sound1Done){ // rest start light and sound timeMarker = millis(); digitalWrite(REST_PIN, HIGH); sound1Done = 1; for (int i = 0 ; i < 2 ; i++){ Serial.print(i); int noteDuration = 1000 / restNoteDuration[i]; tone(SPEAKER_PIN, restNoteMelody[i], noteDuration); int pause = noteDuration * 1.25; delay(pause); noTone(10); } } // updates countdown lcdTimer = (rprInt - (millis() - timeMarker)/1000); if (prevLcdTimer != lcdTimer){ lcd.clear(); prevLcdTimer = lcdTimer; } lcd.home(); lcd.print("rest between reps"); lcd.setCursor(0,1); lcd.print(lcdTimer); if (((millis() - timeMarker)/1000 > (rprInt*.75)) and (not sound2Done)) { // almost done warning sound2Done = 1; digitalWrite(SOON_PIN, HIGH); int noteDuration = 1000 / soonDuration[0]; tone(SPEAKER_PIN, soonMelody[0], noteDuration); } if ((millis() - timeMarker)/1000 > rprInt){ // next state functionality digitalWrite(REST_PIN, LOW); digitalWrite(SOON_PIN, LOW); sound1Done = 0; sound2Done = 0; lcd.clear(); repsInt = repsInt - 1; state = 7; } } else if (state == 9){ // rest between sets if(not sound1Done){ // rest start light and sound timeMarker = millis(); digitalWrite(REST_PIN, HIGH); sound1Done = 1; for (int i = 0 ; i < 2 ; i++){ Serial.print(i); int noteDuration = 1000 / restNoteDuration[i]; tone(SPEAKER_PIN, restNoteMelody[i], noteDuration); int pause = noteDuration * 1.25; delay(pause); noTone(10); } } // updates countdown lcdTimer = (rpsInt - (millis() - timeMarker)/1000); if (prevLcdTimer != lcdTimer){ lcd.clear(); prevLcdTimer = lcdTimer; } lcd.home(); lcd.print("rest between sets"); lcd.setCursor(0,1); lcd.print(lcdTimer); if (((millis() - timeMarker)/1000 > (rpsInt*.75)) and (not sound2Done)) { // almost done warning sound2Done = 1; digitalWrite(SOON_PIN, HIGH); int noteDuration = 1000 / soonDuration[0]; tone(SPEAKER_PIN, soonMelody[0], noteDuration); } if ((millis() - timeMarker)/1000 > rpsInt){ // next state functionality digitalWrite(REST_PIN, LOW); digitalWrite(SOON_PIN, LOW); sound1Done = 0; sound2Done = 0; lcd.clear(); setsInt = setsInt - 1; state = 7; } } }
]]>
My project was an alarm clock that forces the user to get up and get active – the alarm will only stop ringing when the user goes to certain locations in their room in a specific, randomized order.
I noticed that I can only really wake up if I’m forced to get out of my bed and get moving. A lot of people also feel similarly, so it isn’t uncommon to place a phone alarm across the room to force one to get up and turn it off. But after that, it quite easy to slip back into bed and drift into sleep again. What if you had to go to multiple locations in your room, maybe in hard to reach nooks and crevices, in a specific order that is generated by the alarm? That way, you have to really get moving in order to truly turn it off. By that point, ideally, you won’t feel as strong of an urge to slip into bed.
A front view of the alarm
A close up of the LCD that will show the commands
A close up of the button and dial the user will use to set the alarm
A look inside at all the electronics
Demo of setting the time I want the alarm to go off
A demo of how the alarm looks when it is set off and prompts the user to scan specific tags
How I decided to represent locations in my room – simple tags on cards
Getting a prototype “dial” and button to work
Getting the RFID scanner to work was the first hurdle I needed to pass. At first, I merely used female to male jumper cables to connect the scanner to the Arduino, which I expected to work perfectly. However, I was constantly getting error messages that no scanner was actually connected. Sometimes I got lucky and I would get a stray couple of bytes to show that something is hooked up before the same error messages would pop up again. I checked the wiring over and over again and replaced the scanner module multiple times. After a couple of hours, I was at my wit’s end and finally decided to solder the wires instead of just using jumper cables. After soldering, it worked perfectly. I still don’t know what the problem was, but if I didn’t decide to take the soldering approach, perhaps I would have opted to not use an RFID scanner at all. I’m really glad that it ended up working because the scanner ended up being a key part of this project’s appeal.
RFID lit up and working – This guy gave me a lot of trouble
Testing how I would mount the pieces onto the front, I ended up cutting out the wrong dimensions for the LCD, so I would have to redo this box
I didn’t wire my components with the understanding that I would later be putting in some kind of housing, I just wired it in the way that seemed the easiest and fastest at the moment. When it came to actually placing it in the box I cut out, I realized I would have to do a huge overhaul of the way I had everything organized, essentially tearing out all the wires and starting all over again. In future projects, I’ll keep in mind the final presentation while I design the hardware layout to save myself a lot of pain.
Getting everything into the box proved to be a big hassle, lots of wires came undone in the process. I pretty much had to rewire the entire project. Good learning experience.
“The LED strips look a little messy on the outside. Maybe put something on top of them to diffuse the light?“
I totally agree. Currently, my LED strip is held on by its own stickiness, extra double-sided tape, and prayers. I tried to make it look as straight as possible, but it definitely could be a lot straighter and neater. My original hope was that I could use an LED strip that was wrapped in white/clear silicone, so it would nicely diffuse the lights while maintaining brightness, but I didn’t know if we had this in the Physical Computing Lab. Of course, it is an alarm so we do want the lights to be a little jarring, so maybe diffusing may not necessarily be the way to go. However, I think maybe it could be in a sort of housing so that the LED strip isn’t just nakedly sitting on the box.
“If the tags are placed permanently in [a] different location, maybe you could put the RFID scanner on the opposite side of your interface so the buttons don’t interfere with you holding the box against the tags.“
I also agree with this critique. I realized as I was putting all the wires and components into the box that maybe it would be hard to scan the tags from the front, given there are buttons and knobs. I tried it out by sticking a tag on the wall and trying to scan it, and it did work after I tilted the box a little bit. This is definitely a design flaw that I should have kept in mind. This is not something I actively thought about while building because I simulated the walls by just bringing the tags to the RFID scanner location. Maybe it would have been better to put the RFID facing the back, which is flat and can be brought up to the wall.
I’m pretty happy with how the project turned out. Visually, it looks very similar to my original drawings/concepts. However, I would probably not have a hole for the RFID sensor in a more refined project. I put it in this prototype because I thought that I would want to see the exact place of the sensor for debugging purposes, and I was unsure if it could scan through the wood. Now that I know it actually has quite a considerable range, I would not put a hole there and would merely etch a symbol to let the user know that this is the location to scan at. This would look a lot cleaner than the version I have now. I also think that some of my code is a little hacky, for lack of a better word, and that its efficiency can be improved considerably. If given more time, I would consider cleaning up my code so that maybe the project would run smoother.
There were points in the project that if I decided to fully pivot there, I would have gotten it to a functioning state much faster. An example is when I noticed that the RTC wasn’t showing the correct time, and in fact, would generate random times at bootup. For almost a week, this issue didn’t improve because the library I was using simply wasn’t capable of properly flashing the correct time to the RTC. It wasn’t until after I finally used another library did it fix itself. However, if I was just more willing to switch libraries earlier on, I could have fixed this and focused on other stuff. So advice to both past and future self: don’t be afraid to have to redo things to get something to function, sometimes you’re going to have to pivot from what you are currently using to a totally new tool, and you’re going to have to be okay with that.
My original vision for this project was that the RFID would just be one component of many. I’d also have to play with other sensors to turn off the alarm as well. For example, the alarm would need a photoresistor to read values above a certain threshold, either by bringing the alarm to the light or by binging my phone flashlight up to it. This would end up as almost like a “bop-it” alarm clock, as you have to complete certain commands in rapid succession to turn it off. I still think this is a fun idea and is probably the approach I would take if I decide to keep working on it. Also, currently, the only way to “cancel” an alarm a user has already set up is by ripping out the battery. I would definitely make a reset/off/cancel button in an improvement of this project.
I would also like to integrate this with a way to measure and display information sleep as well, perhaps with a mobile/web app. It would be great to record the number of hours slept, probably by adding another button to set when the user goes to sleep and then tracking until the alarm is turned off. I think displaying this information on a graph and running some analytics on it could be extremely useful to the user when deciding how to change their sleeping habits.
/* Active Alarm * Neeharika Vogety (nvogety) * * * Summary: An alarm will ring, vibrate, and light up at a time that a user sets * to wake up at. The only way to turn off the alarm is to scan RFID tags set around * the room in a specific, randomly-generated order. The idea is to get the user up * and moving to turn off the alarm, rather than just hitting a button and going back to * sleep. * * Inputs: * * Arduino Pin | Input * 5 LED Strip Pin * 2 Button Pin * 9 RFID Pin * A1 Potentiometer Pin * * Outputs: * * Arduino pin | Output * 7 Vibrating Motor Pin * 4 Piezo Buzzer Pin */ #include <Wire.h> #include <Time.h> #include <LiquidCrystal_I2C.h> #include <RTClib.h> #include <SPI.h> #include <MFRC522.h> #include <FastLED.h> // Define important pins for RFID #define RST_PIN 9 #define SS_PIN 10 #define TASKSLEN 5 // Define length of tasks array, used throughout code // Define important constants for LED strip #define LED_PIN 5 #define NUM_LEDS 45 #define BRIGHTNESS 200 #define LED_TYPE WS2811 #define COLOR_ORDER GRB CRGB leds[NUM_LEDS]; #define UPDATES_PER_SECOND 100 CRGBPalette16 currentPalette; TBlendType currentBlending; typedef struct{ char *tagName; char *tagUID; } Tag; // An array of Tag structs that links together // the name of the tag and the ID of the tag. // If this is changed, must change TASKLEN at // the top! Tag tasks[TASKSLEN] = { {"A", "437B172845C81"}, {"B", "450B072845C80"}, {"C", "42FB172845C81"}, {"D", "461DF72845C80"}, {"E", "49ED972845C80"}, }; int nums[TASKSLEN]; // Create nums array int numOrder[TASKSLEN]; // Create num Order array int nOindex = 0; // Index to traverse num Order // Bools that we will use to execute certain parts of the code bool startGeneration = false; bool readRFID = false; bool setAlarm = true; bool alarmSet = false; // Hour and Minute, will set with RTC int alarmHour; int alarmMin; LiquidCrystal_I2C screen(0x27, 16, 2); //Create Screen object MFRC522 mfrc522(SS_PIN, RST_PIN); // Create MFRC522 instance DS1307 RTC; // Create RTC object // Initialize constant i/o pins const int BUTTONPIN = 2; const int POTPIN = A1; const int VIBRPIN = 7; const int BUZZERPIN = 4; void setup() { Serial.begin(9600); // Set up INPUTS AND OUTPUTS pinMode(BUTTONPIN, INPUT_PULLUP); pinMode(POTPIN, INPUT); pinMode(VIBRPIN, OUTPUT); pinMode(BUZZERPIN, OUTPUT); // Set up RFID SPI.begin(); // Init SPI bus mfrc522.PCD_Init(); // Init MFRC522 delay(4); mfrc522.PCD_DumpVersionToSerial(); // Make sure RFID is up and running // Start RTC RTC.begin(); //Set up LED String FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip ); FastLED.setBrightness( BRIGHTNESS ); currentPalette = RainbowStripeColors_p; currentBlending = NOBLEND; clearLEDs(); // Clear LCD screen and signal set up is over screen.init(); screen.backlight(); screen.home(); screen.print("Booting up.."); delay(1000); screen.clear(); } void loop() { // If we still need to set an alarm time, // display the current time and wait for button press // to set the hour, then the minute of the alarm, and // set the alarm if(setAlarm){ // Display time until button is pressed while(digitalRead(BUTTONPIN) == LOW){ DateTime now = RTC.now(); screen.clear(); screen.setCursor(0, 0); screen.print("Current Time:"); screen.setCursor(0, 1); if(now.hour() <= 9){ screen.print("0"); } screen.print(now.hour()); screen.print(":"); if(now.minute() <= 9){ screen.print("0"); } screen.print(now.minute()); screen.print(":"); if(now.second() <= 9){ screen.print("0"); } screen.print(now.second()); delay(1000); } delay(1000); // Allow user to set up hour of alarm until button is pressed again while(digitalRead(BUTTONPIN) == LOW){ screen.clear(); screen.setCursor(0, 0); screen.print("Set Alarm Hour:"); // Set alarm hour with potentiometer alarmHour = map(analogRead(POTPIN), 0, 1023, 0, 24); screen.setCursor(0, 1); if(alarmHour <= 9){ screen.print("0"); } screen.print(alarmHour); delay(250); } delay(1000); // Allow user to set up minute of alarm until button is pressed again while(digitalRead(BUTTONPIN) == LOW){ screen.clear(); screen.setCursor(0, 0); screen.print("Set Alarm Minute:"); // Set alarm minute with potentiometer alarmMin = map(analogRead(POTPIN), 0, 1023, 0, 60); screen.setCursor(0, 1); if(alarmMin <= 9){ screen.print("0"); } screen.print(alarmMin); delay(250); } delay(1000); // Show when alarm is set for screen.clear(); screen.setCursor(0, 0); screen.print("Alarm set for:"); screen.setCursor(0, 1); if(alarmHour <= 9){ screen.print("0"); } screen.print(alarmHour); screen.print(":"); if(alarmMin <= 9){ screen.print("0"); } screen.print(alarmMin); setAlarm = false; alarmSet = true; } // Keep checking if current hour and minute is what we set for the alarm if(alarmSet){ DateTime now = RTC.now(); if(now.hour() == alarmHour && now.minute() == alarmMin){ startGeneration = true; alarmSet = false; screen.clear(); screen.print("ALARM STARTED"); // Set off buzzer, will make a humming sound tone(BUZZERPIN, 100); vibrate(); vibrate(); vibrate(); Serial.print("ALARM STARTED!!!!!"); } } // If alarm went off, start to create the random list of Tags if(startGeneration){ // Fill nums in with just numbers counting from 0 up for(int i = 0; i < TASKSLEN; i++){ nums[i] = i; } // Form new numOrder array by grabbing different parts of numArray for(int currlen = TASKSLEN; currlen > 0; currlen--){ // Seed differently everytime randomSeed(analogRead(A0)); // Get random number, which will be the index for the number to grab from num int randi = random(currlen); numOrder[nOindex] = nums[randi]; nOindex++; // "Remove" the number we just picked from num by shifting over the other numbers // This will guaruntee we don't pick that number again for(int k = randi; k < currlen; k++){ nums[k] = nums[k+1]; } } // Loop through the tasks in the order that we generated for(int i = 0; i < TASKSLEN; i++){ screen.clear(); screen.print("GO TO "); screen.print(tasks[numOrder[i]].tagName); readRFID = true; while(readRFID){ // Keeping sounding the alarm, even while user is still scanning tags soundAlarm(); // Keep checking if a new tag has been shown if ( ! mfrc522.PICC_IsNewCardPresent()) { continue; } if ( ! mfrc522.PICC_ReadCardSerial()) { continue; } // Print the tag info scanned for debugging purposes Serial.print("CARD SCANNED!!!!!: "); String readtagID = ""; // The MIFARE PICCs that we use have 4 byte UID for ( uint8_t i = 0; i < 7; i++) { readtagID.concat(String(mfrc522.uid.uidByte[i], HEX)); // Adds the 4 bytes in a single String variable } readtagID.toUpperCase(); mfrc522.PICC_DumpToSerial(&(mfrc522.uid)); // If the tag scanned is the correct one in the order specified. // move on to checking for the next tag if(readtagID == tasks[numOrder[i]].tagUID){ screen.clear(); if(i < TASKSLEN - 1){ screen.print("Good! Next one.."); vibrate(); screen.clear(); } readRFID = false; } } } // Show that user has finished screen.print("FINISHED"); noTone(BUZZERPIN); vibrate(); screen.clear(); clearLEDs(); startGeneration = false; setAlarm = true; loopCounter++; } } // What the user should see while alarm is going off, // start filling the LEDs with color void soundAlarm(){ static uint8_t startIndex = 0; startIndex = startIndex + 1; /* motion speed */ FillLEDsFromPaletteColors( startIndex); FastLED.show(); FastLED.delay(1000 / UPDATES_PER_SECOND); } // Fill LED strip based off of variables set at the beginning void FillLEDsFromPaletteColors( uint8_t colorIndex) { uint8_t brightness = 255; for( int i = 0; i < NUM_LEDS; i++) { leds[i] = ColorFromPalette( currentPalette, colorIndex, brightness, currentBlending); colorIndex += 3; } } // Clear all the LEDs void clearLEDs(){ for( int i = 0; i < NUM_LEDS; i++) { leds[i] = CRGB::Black; } FastLED.show(); } // Pattern of vibration when alarm first goes off void vibrate(){ digitalWrite(VIBRPIN, HIGH); delay(1000); digitalWrite(VIBRPIN, LOW); delay(500); }
]]>
An alarm clock that reminds you to get ready for bed, and gives advice for how to improve your sleep.
The alarm clock after you turn off the morning alarm. It displays the date, time, and a message based on the amount of sleep you got that night.
The inside of the alarm clock
The light turns on to signify that the user should get ready for bed. It can be turned off with a button, that also tells the clock the user is going to bed.
Pressing the button again turns off the morning alarm and tells the clock the user has woken up. It then displays a message based on the amount of sleep it tracked for that night.
One of the big decisions I made was not having a way to change the alarm times. I originally planned to have two buttons that when pressed, would allow you to change the alarms with a potentiometer. I quickly realized, however, that I wouldn’t have enough time to implement this and instead decided to just have the alarms hard coded.
Another decision I made was to use an lcd screen to display the messages for how to improve your sleep. I was debating whether to use an lcd screen or receipt printer, and decided to use the lcd screen since it was big enough to display the messages I had in mind and I knew how to use one so I knew it wouldn’t take much time to add.
The final breadboard version on my clock, with two lcd screens.
This was the first version of my alarm clock. It had a real time clock, LED, piezo buzzer, a button, and one lcd screen that displayed the time.
The final version, with everything soldered to a protoboard. The final version had a second lcd screen, another button, a barrel jack, a speaker instead of a buzzer, and used an arduino nano instead of an uno.
The laser cut box I made for the clock, before I but the electronics in. The original top had holes that were too small for the buttons, so I had to recut the top.
One of the critiques I received was, “I think that it would be more effective if you could change the sleep interval w/o having to reprogram the entire device.” I agree that this is something that would greatly improve the clock, since when you need to wake up or go to bed changes over time. As I mentioned earlier, I originally planned to include a way to change the alarms but didn’t have enough time to implement it.
Another critique was, “It might be cool to have this track sleep statistics over the course of the week.” I also agree with this critique. I think having advice given based on a pattern of sleep rather than just one night of sleep would be more useful.
I think it did satisfy my main goals, and I am happy with how it came out. All of the features work the way I intended them to. I did the best I could in the time given, but there is still a lot of room for improvement. I think that the box is a bit big and bulky. I had designed it that way to make sure everything fit inside it, but it is bigger than it needs to be.
I learned a lot from this project, like that I don’t know how to use interrupts. I ran into a problem where the alarms stopped working correctly all of a sudden, so I tried switching to a different library to see if there was a problem with the library I was using. However, I couldn’t figure out how to use the interrupt that the library relied on to set alarms. There are still a lot of things you can do with the arduino that I have yet to learn.
I really enjoyed being able to just make something on my own. This is the first time I’ve made a device, so it’s really satisfying to have taken an idea and made it into a reality. Since I had come up with the project idea and it was something I could actually use, I was really motivated and excited to work on the project.
If I could have done anything differently, I would have spent more time researching libraries for the real time clock. I went the library used in the first tutorial I found, but there might have been better libraries out there that I could have worked better for my project that I just didn’t know about.
If I have time this semester, I would like to make another iteration. I would add a way to change the time and the alarms, probably with buttons to indicate you want to change an alarm and a potentiometer to change the time. I would also like to make a more complicated sleep tracker that gives advice after a week of tracking your sleep. I would also make the physical clock smaller, since it’s bulky and takes up a lot of room when it isn’t necessary for it to be so big.
/* * Project 2 * Karen Abruzzo (kabruzzo) * Time: two weeks * * Collaboration: * I used code from the example SetAlarms from the DS3231M library as a staring point. * https://github.com/SV-Zanshin/DS3231M * * Summary: The code sets two alarms that can be turns off with a button. One alarm turns on * an LED to remind you to go to bed, the other plays a sound to wake you up in the morning. * There is also a snooze button. The code also tracks your sleep and outputs the current date, * time, and advice onto lcd screens. * * Inputs: * real time clock | PIN A4, A5 * button | PIN 2 * button | PIN 5 * * Outputs: * lcd screen | PIN A4, A5 * lcd screen | PIN A4, A5 * speaker | PIN 4 * LED | PIN 3 */ #include <DS3231M.h>//library for the rtc #include <Wire.h>//library for i2c communications on analog pins #include <LiquidCrystal_I2C.h>//library for the lcd screens //setup the screens LiquidCrystal_I2C screen1(0x27, 16, 2); LiquidCrystal_I2C screen2(0x20, 20, 4); const uint32_t SERIAL_SPEED = 115200; //set up pin numbers const int ALARM_BUT_PIN = 2; const int SNOOZE_BUT_PIN = 5; const int LED_PIN = 3; const int BUZ_PIN = 4; //set up global variables //time you go to bed int SLEEP_HOUR; int SLEEP_MIN; int SLEEP_SEC; //time you wake up int WAKE_HOUR; int WAKE_MIN; int WAKE_SEC; //# of snoozes int SNOOZES = 0; //amount of time you slept int HOURS_SLEPT; int MINS_SLEPT; int SECS_SLEPT; DS3231M_Class DS3231M; const uint8_t SPRINTF_BUFFER_SIZE = 32; //this is for displaying the time and date properly char inputBuffer[SPRINTF_BUFFER_SIZE]; bool ALARM = true; //this variable keeps track of which alarm (day or night) is set to go off next. True means the night alarm is set up. DateTime SETUP_TIME = DateTime(2019, 10, 10, 11, 23, 0);//current time DateTime NIGHT_ALARM = DateTime(0, 0, 0, 23, 0, 0);//time you want to go to bed DateTime NIGHT_ALARM2 = DateTime(0, 0, 0, NIGHT_ALARM.hour() - 1, 30, 0);//time nigh alarm will go off DateTime DAY_ALARM = DateTime(0, 0, 0, 8, 20, 0);//time day alarm will go off void setup() { // put your setup code here, to run once: //setup pins pinMode(ALARM_BUT_PIN, INPUT); pinMode(LED_PIN, OUTPUT); pinMode(BUZ_PIN, OUTPUT); Serial.begin(SERIAL_SPEED); //setup lcd screens screen1.init(); screen2.init(); // turn on the backlight to start screen1.backlight(); screen2.backlight(); // set cursor to home position, i.e. the upper left corner screen1.home(); while (!DS3231M.begin()) { Serial.println(F("Unable to find DS3231MM. Checking again in 3s.")); delay(3000); } //DS3231M.adjust(SETUP_TIME); //uncomment this line to set the time on the rtc DS3231M.setAlarm(minutesHoursMatch, NIGHT_ALARM2); //start the alarm //display the date and time DateTime now = DS3231M.now(); sprintf(inputBuffer, "%04d-%02d-%02d", now.year(), now.month(), now.day()); screen1.clear(); screen1.home(); screen2.clear(); screen2.home(); screen1.print(inputBuffer); Serial.print(inputBuffer); Serial.print(" "); sprintf(inputBuffer, "%02d:%02d", now.hour(), now.minute()); screen1.setCursor(0, 1); screen1.print(inputBuffer); Serial.println(inputBuffer); } void loop() { // put your main code here, to run repeatedly: DateTime sleepTime; //time you go to bed DateTime wakeTime; // time you wake up DateTime timeSlept; //how long you slept DateTime now = DS3231M.now(); static uint8_t mins; if (mins != now.minute()) //every minute, change display so it displays current time { sprintf(inputBuffer, "%04d-%02d-%02d", now.year(), now.month(), now.day()); screen1.clear(); screen1.home(); screen1.print(inputBuffer); Serial.print(inputBuffer); Serial.print(" "); sprintf(inputBuffer, "%02d:%02d", now.hour(), now.minute()); screen1.setCursor(0, 1); screen1.print(inputBuffer); Serial.println(inputBuffer); mins = now.minute(); } if (digitalRead(ALARM_BUT_PIN)) //if the alarm off button is pressed { delay(200); //debounces button SNOOZES = 0; //reset snoozes if (ALARM) { //if the alarm that went off was the night alarm //turn off alarm, set alarm time to the morning alarm, turn off the LED, //set the sleep time and turn off backlight on second lcd screen DS3231M.clearAlarm(); DS3231M.setAlarm(minutesHoursMatch, DAY_ALARM); ALARM = !ALARM; digitalWrite(LED_PIN, 0); sleepTime = DS3231M.now(); SLEEP_HOUR = sleepTime.hour(); SLEEP_MIN = sleepTime.minute(); SLEEP_SEC = sleepTime.second(); screen2.clear(); screen2.noBacklight(); } else { //if morning alarm just went off //turn off alarm, set alarm time to the night alarm, turn off the speaker, //set the wakeup time, calculate and display time slept, //display advice depending on amount of time slept DS3231M.clearAlarm(); DS3231M.setAlarm(minutesHoursMatch, NIGHT_ALARM2); sprintf(inputBuffer, "%02d:%02d", NIGHT_ALARM.hour(), NIGHT_ALARM.minute()); ALARM = !ALARM; noTone(BUZ_PIN); wakeTime = DS3231M.now(); WAKE_HOUR = wakeTime.hour(); WAKE_MIN = wakeTime.minute(); WAKE_SEC = wakeTime.second(); HOURS_SLEPT = WAKE_HOUR - SLEEP_HOUR; MINS_SLEPT = WAKE_MIN - SLEEP_MIN; SECS_SLEPT = WAKE_SEC - SLEEP_SEC; //account for any negative numbers from the subtraction if (SECS_SLEPT < 0) { MINS_SLEPT -= 1; SECS_SLEPT += 60; } if (MINS_SLEPT < 0) { HOURS_SLEPT -= 1; MINS_SLEPT += 60; } if (HOURS_SLEPT < 0) { HOURS_SLEPT += 12; } Serial.println("time slept:"); screen2.clear(); screen2.backlight(); screen2.home(); screen2.print("time slept: "); sprintf(inputBuffer, "%02d:%02d:%02d", HOURS_SLEPT, MINS_SLEPT, SECS_SLEPT); Serial.println(inputBuffer); screen2.print(inputBuffer); if (HOURS_SLEPT < 8) { if (HOURS_SLEPT < 5) { screen2.setCursor(0, 1); screen2.print("Try going to bed"); screen2.setCursor(0, 2); screen2.print("early today, you got"); screen2.setCursor(0, 3); screen2.print("very little sleep."); Serial.println("Try going to bed early today, you got very little sleep."); } else { screen2.setCursor(0, 1); screen2.print("You got less than"); screen2.setCursor(0, 2); screen2.print("8 hours, try to get"); screen2.setCursor(0, 3); screen2.print("more sleep."); Serial.println("You slept less than 8 hours, try to get more sleep."); } } else if (SLEEP_HOUR != NIGHT_ALARM.hour()) { screen2.setCursor(0, 1); screen2.print("You didn't go to bed"); screen2.setCursor(0, 2); screen2.print("on time, try to stay"); screen2.setCursor(0, 3); screen2.print("on schedule."); Serial.println("You didn't go to bed on time, try to stay on schedule."); } else { screen2.setCursor(0, 1); screen2.print("You got a good"); screen2.setCursor(0, 2); screen2.print("amount of sleep,"); screen2.setCursor(0, 3); screen2.print("good job!"); Serial.println("You got a good amount of sleep, good job!"); } } } if (digitalRead(SNOOZE_BUT_PIN)) //if snooze button is pressed { if (DS3231M.isAlarm()) //if an alarm is going off { if (SNOOZES < 3) //if snooze button has been hit less than 3 times { delay(200); //debounce button //turn off alarm for ten seconds DS3231M.clearAlarm(); DS3231M.setAlarm(minutesHoursMatch, (0, 0, 0, now + TimeSpan(0, 0, 10, 0))); digitalWrite(LED_PIN, 0); noTone(BUZ_PIN); SNOOZES += 1; } else //if snooze button has been hit 3 or more times { if (ALARM) { screen2.clear(); screen2.home(); screen2.print("You need to go to"); screen2.setCursor(0, 1); screen2.print("bed!"); } else { screen2.clear(); screen2.backlight(); screen2.home(); screen2.print("You need to go get"); screen2.setCursor(0, 1); screen2.print("up!"); } } } } if (DS3231M.isAlarm()) //if the alarm is going off { if (ALARM) { digitalWrite(LED_PIN, 1); //turn on led } else { tone(BUZ_PIN, 1000); //play a loud annoying sound } } }
]]>
This device effectively reminds you to put in your contacts lenses when you wake up.
Back View: One port to reprogram and another for power
Sample LEDs that blink to remind the user
Switch that detects presence of phone
One decision point during the process was the choice between using a real time clock or simply another switch to determine when the reminder would be able to activate. If I were to use a switch that the user would have to turn on and off, the device would be more accurate in determining when the user goes to bed, but it’s manual and not automatic which runs the risk of the user forgetting to flip the switch. The real time clock allows the device to automatically know when the user should be asleep based on the time it is set, but the accuracy of the timing relies on the consistency of the user’s sleep schedule. In the end, I decided that the clock has enough accuracy to pinpoint when the user would go to bed, and it’s automatic quality would be more desired.
Here is an early prototype with the switch
This is part of the final build with the DS3231 module (Clock) attached to protoboard
Another decision point was how the phone button would be optimally implemented into the final build of the device without creating flaw in the design. This was significant because if the button wasn’t efficient in detecting the phone, the whole device would be compromised. I luckily discovered that I could use the inter-structure of cardboard to have space for wires and a place for the button to show to be pressed. This allowed the button to be fully functional without any wires showing.
Very early build of phone button
One side of the fed through wire for phone button
Another side of wire fed through cardboard for phone button
Backside of protoboard
Front side of protoboard
Responses:
“I think that the form of the box could be modified to fit the shape and size of the phone more. Is there a way to set the way that the LEDs blink on the fly? The rapid flashing seems like it would be a lot at 8AM.”
Now that I’m looking back on it, a holder for the phone would have been extremely useful and convenient. This would help prevent the phone from somehow falling off or moved enough where the button isn’t triggered anymore. The point of the presence of a phone triggering the LEDs rather than simply at a set time was to increase accuracy in determining when the LEDs turn on. My thinking was that everyone goes everywhere with their phone, and we often have varying schedules, so your phone would act as a better key when you wake rather than time. The purpose of the clock was implemented to automatically “set” the device and to “unset”. Also, the rapid flashing of the LEDs was intended to cause as much annoyance as possible to force the user to remember to put in their contact lenses.
“The minimalistic interface does keep it simple for users, which is always a plus, especially if the users are tired, college students.”
The key characteristics I wanted to highlight in this device were simplicity and effectiveness, so I’m glad that this response and many others indicated the key characteristics. The purpose of implementing these key aspects was because I myself am a tired college student, and lets be honest, I probably wouldn’t need a device to remind me of self-care if I wasn’t a tired college student. That being said I wanted a device where I could roll out of bed and without using my brain, be able to remember to put my contact lenses in, and I think this device does that pretty well.
This device is simple, and it works. I’m satisfied in the aspect that it does everything I wanted it to do. However, after going through the process and gaining feedback, there are many things about my device that could be improved. The core design of the device is good, but each aspect that make up the core design can be improved to produce a much better overall device. I learned how useful peer feedback is. As I went through the process, I had an idea of how to improve parts, but I couldn’t quite make the connection in my head. When I heard feedback from others, I was able to finalize exactly how to improve certain parts. On my next project, I’ll be sure to reach out to more peers during the process of building the device to catch more flaws. The most difficult part of making the device was the design part. This was especially true on the placement of the phone button which I was lucky enough to find a creative spot to put it. I felt like that the design was where I put most of my time into, and in the end, it was the area that could have used the most improvements. I think next time I should make more frequent and less time consuming prototype design builds in order to eliminate more flaws. I don’t plan on building another finalization of this device, but I know how I would improve it if I did. First, I would design a holder to keep the phone in place when placed onto the button. Also, I would change the button placement to be under the middle of the phone rather than on one of its sides. Finally, some of the parameters should be modified in the software to more accurately determine when the device needs to be set. Right now, it only takes in account the hour, but it could also consider the minutes of the hour.
// Contact Lens Reminder // Leland Mersky // // This code runs a reminder system in the morning (or any desired // time) that will blink LED's once you take your phone off it to // get up and on with your day. It'll shut off at a set time when // you're not expected to be at home and have no use for it. // // Digital // Red LED Pin 2 // Green LED Pin 3 // Blue LED Pin 4 // Phone Button Pin 5 #include <DS3231.h> DS3231 rtc(SDA, SCL); const int RED_LED_PIN = 2; const int GREEN_LED_PIN = 3; const int BLUE_LED_PIN = 4; const int PHONE_BUTTON_PIN = 5; void setup() { pinMode(RED_LED_PIN, OUTPUT); pinMode(GREEN_LED_PIN, OUTPUT); pinMode(BLUE_LED_PIN, OUTPUT); pinMode(PHONE_BUTTON_PIN, INPUT); Serial.begin(9600); Serial.begin(115200); rtc.begin(); //starts the real time clock rtc.setDOW(TUESDAY); //set the day of the week rtc.setTime(0,20,00); //set the time in hours:minutes:seconds rtc.setDate(10,9,2019); //set the date months/days/year } void loop() { String mytime = rtc.getTimeStr(); //gets the time in a string mytime = mytime.substring(0,2); //only gets the hours part of string int hour = mytime.toInt(); //turns hours part into an int type int PhoneButtonState; PhoneButtonState = digitalRead(PHONE_BUTTON_PIN); if (((hour<=7) and (PhoneButtonState == LOW))){ //set hour to when you want it to be activated //this is set to turn on no later than 7:59 AM //because hours can be from 0 to 7 digitalWrite(RED_LED_PIN,HIGH); delay(100); digitalWrite(RED_LED_PIN,LOW); digitalWrite(GREEN_LED_PIN,HIGH); delay(100); digitalWrite(GREEN_LED_PIN,LOW); digitalWrite(BLUE_LED_PIN,HIGH); delay(100); digitalWrite(BLUE_LED_PIN,LOW); } }
]]>
This device helps the user develop habits by allowing the user to track the strength of habits via RFID tags. Each time the user performs an activity, they can scan the associated RFID tag/key card on the device as a tally of how many times they’ve performed the activity. When this happens a certain amount of time, the activity is considered a developed habit.
Front view of the device with two types of compatible RFID-embedded objects.
A fully “developed” habit will trigger the light ring embedded within the dial to display a rainbow.
Front view of device showing the size of the dial and scanning surface
The interface of the device is a cylindrical dial with an embedded button and LED ring, as well as a RFID reader underneath the flat surface.
The first decision point came from an aesthetic standpoint and occurred early on in the process. My original idea was using a potentiometer to adjust the strength of a habit, and have an LED strip near the RFID scanner to display the strength.
Original proof of concept with a potentiometer, linear Neopixel strip, and separate button.
However, I decided to move to a ring and encoder setup because the LED also needs to be responsive to dial adjustments. By using a ring, the motion of the rotation is coupled with the “motion” of the LED lights. Furthermore, using an encoder allows the user to turn the dial freely, but figuring out what the rotations are translated to is relegated to software.
Though this appeared to be a minor change, the encoder required major restructure of the main program loop. Originally, I wanted to check if a RFID tag is detected by the scanner every cycle. But the time this takes interferes with the encoder’s ability to detect rotation. This led me to introduce a “time out” system for setting a new tag’s strength. Pushing down the encoder while rotating will set the strength, and after releasing the encoder, the user has three seconds to flash a new tag. Otherwise, the nano goes back to waiting to read a tag.
Converting to Neopixel ring and rotary encoder with built-in button.
One feature I wanted to add was making time a factor in the calculation of how strong a strength is. However, I decided against implementing it. Because of the long-running nature of this device, I would need to incorporate timing in the form of an extra component to connect to the arduino. Furthermore, this information would need to be encoded onto the RFID tags. Though this would allow me to differentiate habits I infrequently follow from the ones that I do abide by daily, the amount of technical complications was too much for the time frame of this project.
Instead, after implementing the functions I initially planned on, I decided to focus on designing the appearance of the final product.
Moving components to a protoboard to slim down the form factor.
Initial sketch of the enclosure
Modeled enclosure with built-in supports for the components.
Printed enclosure with placeholder acrylic dial.
Final enclosure with printed lid and dial.
“A time interval could make it even better.”
The most common points raised during the critique was a lack of a time element. The “strength” of a habit should account for the frequency in which an activity is performed, but my device currently only tracks the amount of times it has been scanned. This is definitely a feature I would like to incorporate, including a way to set the target frequency (e.g. X times/ week). One concern for adding this feature is that it might require a more complicated interface. The consensus seems to be that my current press-and-turn interface is “simple and intuitive.” The simplicity may come from the fact that there is only one thing to control right now: the maximum number of times the tag is scanned to fill up the light ring. If there are two degrees of freedom involved, I would need to think of a way for one dial to set two parameters.
Another way I could maintain the simplicity of the interface is to set a constant number/duration for which a tag must be scanned. However, the dial will then only set the desired frequency, using this and the actual frequency of scanning to calculate “strength”.
“I wonder if there could be the opposite, where it punishes you for having bad habits.”
If this time feature is implemented, I could introduce a decay factor into my habit. This way, the strength of the habit could decrease if the scanning becomes too infrequent. Although this is not necessarily a real punishment, it does mean that I will be further from getting to see the rainbow lights spin around.
Another question raised during the critique was how I will remember to scan the tags related to an activity when I am not immediately next to my desk, which is where this device would be. An example I provided is putting a tag on my gym bag, so when I come home from the gym I will naturally scan it. However, not every activity may have a physical object associated with it, which would make remembering more difficult. The hope would then be that seeing this tracker on my desk can also serve as a reminder for me to perform tasks as well as remember to scan tags associated with them. In that sense, remembering to use this device is also a habit I would then need to develop.
Looking back, I think that this project was quite ambitious given that it was an individual project over the course of two weeks. I am happy with the way that my project turned out, but I also think I could have been more ambitious. I decided to focus on the physical design of my project over the software because I wanted to use this assignment as a way to explore that field. Because of this, I settled with a conceptually simple (but still challenging to implement) software side.
One of the potential issues on the software side I decided to overlook was the sequential nature of the LED animations. Because each animation routine was a separate function that used delay() for timing, no RFID tags will be scanned when an animation is being played. This also means that tags must be scanned one at a time, i.e. I would not be able to bulk scan a lot of tags (e.g. at the end of the say when I get home). However, an advantage of this is that I would be able to see the status of each tag I scan, which would can serve as a way for me to check in with myself about a habit and give enough time for all of them.
I wanted to learn how to design and 3D print for this project, and I was able to accomplish that goal. Translating something from my mind to the whiteboard, then to a modeling software, and finally materialized in physical space was a very exciting process for me, and I learned to consider a lot of different factors in my design, especially the space required to fit components. For example, the wiring for the neopixel needed to be routed through the dial to the protoboard, so I needed to make sure that there was enough clearance between the encoder stand and the protoboard stand to route these wires. While designing the internal components, I also had a general idea of how I wanted the external appearance would turn out, and this also helped the final design come together.
Nonetheless, there were also some issues with my 3D printed enclosure, which is understandable for my first time designing. The stands for my protoboard were too small, so I needed to sand down the edges of the board for it to fit. On the other hand, the stand for the encoder was too wide for the component to sit snugly in it. I used tape make sure it stayed in its place during operation. Moreover, I made the dial a bit too flimsy because the sides of it were too thin, and this made turning the dial feel less stable than anticipated. Another detail I overlooked was the gap between the dial and the body of the enclosure. The gap is inevitable, as there needs to be room for the dial to be pressed. However, this exposed the internals originally, and I improvised by wrapping the central LED ring support with white tape.
I learn a lot about my own process for designing, which involved quite a bit of improvising. After all, this is only the first iteration. I wanted to make sure that my internal components worked before I began to think too much about the final “look” of the product. I seem to allow the functionalities and components to dictate the form, instead of the other way around. I also designed the base before I designed the dial and top cover because I needed a tangible product to test fit the internals. Some of the considerations about the base only arose when the dial and lid needed to be designed, but it was too late. To summarize, I think I take on a very impatient but incremental approach to design, which caused some unfortunately silly mistakes.
In the next iteration, I would address some of the issues with the external enclosure (i.e. make the dial sturdier, adjust the supports for components). Since this device would be sitting on my desk, I could afford to make it larger to fit in a real time clock module, which would allow me to implement the time-based features discussed above.
Another potential addition could be a way to report the development of a habit over time, like a line chart. This could be done via IoT method (most likely an AWS or Google Cloud micro service), so I could have a web dashboard that keeps track of my habit development. Another more analog way could be using a dot matrix printer. It could be a slow process where, after each day, a single line is printed, with each dot corresponding to a habit. There are a lot of directions this project could take, and I am excited to see what direction I end up choosing.
/* RFID Habit Tracker This is the code for the Arduino Nano Rev3 board within the habit tracker. Each time an RFID chip is scanned by the MFRC522 unit, a byte stored within the chip (buffer[0]) is incremented in its memory block (buffer) to represent the number of times a habit is reinforced. The maximum limit to increment up until is set by turning the encoder while pressing it down, and "flashing" a new tag with buffer[1] set as the max count. The status of a habit, when incremented, is displayed on the Neopixel ring with an animation. When turning the rotary encoder, the strength is also represented on the ring. When a tag has been scanned its "maximum" times, the ring fills up and rotates. The Adafruit Neopixel API was consulted to implement the LED animations. (https://learn.adafruit.com/adafruit-neopixel-uberguide/arduino-library-use) The Rotary encoder code is based on the example found here: (https://github.com/PaulStoffregen/Encoder) The MFRC522 scanner uses the library in the link below. (https://github.com/miguelbalboa/rfid) MFRC522 Wiring ------------------------------- MFRC522 Arduino Reader/PCD Nano v3 Signal Pin Pin ------------------------------- RST/Reset RST D9 SPI SS SDA(SS) D10 SPI MOSI MOSI D11 SPI MISO MISO D12 SPI SCK SCK D13 Other connections ----------------------------------- Device Dev. Pin Arduino Pin ----------------------------------- Neopixel Digital_in D3 Encoder CLK D6 Encoder DT D5 Encoder SW D4 */ //#define ENCODER_DO_NOT_USE_INTERRUPTS #include <SPI.h> #include <MFRC522.h> #include <Adafruit_NeoPixel.h> #include <Encoder.h> #ifdef __AVR__ #include <avr/power.h> // Required for 16 MHz Adafruit Trinket #endif const int RST_PIN = 9; const int SS_PIN = 10; const int LED_PIN = 3; const int BUTTON_PIN = 4; // milliseconds of no button press until we stop waiting to "flash" const int TIMEOUT = 3000; //Max val the encoder can take on const int MAX_ENCODER = 100; //Max val that buffer[1] could be const int MAX_SCANS = 30 const int LED_COUNT = 16; const int BRIGHTNESS = 100; const int FILLUP_SPEED = 10; //Location of bytes in the RFID tag to write to const byte BLK_ADDR = 4; const byte TRAILER_BLK = 7; float last = millis(); long pos = 0; bool increment; // Declare NeoPixel strip object: Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); // Create MFRC522 instance. MFRC522 mfrc522(SS_PIN, RST_PIN); MFRC522::MIFARE_Key key; Encoder myEnc(6, 5); void dump_byte_array(byte *buffer, byte bufferSize); void setup() { Serial.begin(115200); // Initialize serial communications with the PC pinMode(BUTTON_PIN, INPUT_PULLUP); strip.begin(); // INITIALIZE NeoPixel strip object (REQUIRED) strip.show(); // Turn OFF all pixels ASAP strip.setBrightness(BRIGHTNESS); // Set BRIGHTNESS to about 1/5 (max = 255) SPI.begin(); // Init SPI bus mfrc522.PCD_Init(); // Init MFRC522 card // Prepare the key (used both as key A and as key B) for (byte i = 0; i < 6; i++) { key.keyByte[i] = 0xFF; } } /* Main loop. */ void loop() { bool button = !digitalRead(BUTTON_PIN); bool flashed = false; if (button) { increment = false; last = millis(); pos = min(max(0, myEnc.read()), MAX_ENCODER); myEnc.write(pos); display(pos, MAX_ENCODER); return; } else { // "flash" routine timed out, go back to waiting to // read and increment RFID tag if (millis() - last > TIMEOUT) { fadeout(2); increment = true; myEnc.write(0); } } // Reset the loop if no new card present on the sensor/reader. // We only check rfid tags when the button is not pressed // This prevents encoder lagging if ( ! mfrc522.PICC_IsNewCardPresent()) { return; } // Select one of the cards if ( ! mfrc522.PICC_ReadCardSerial()) return; //reset encoder position myEnc.write(0); //declare buffer for data to be read into MFRC522::StatusCode status; byte buffer[18]; byte size = sizeof(buffer); // Authenticate using key A,B status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate( MFRC522::PICC_CMD_MF_AUTH_KEY_A, TRAILER_BLK, &key, &(mfrc522.uid)); status = (MFRC522::StatusCode) mfrc522.PCD_Authenticate( MFRC522::PICC_CMD_MF_AUTH_KEY_B, TRAILER_BLK, &key, &(mfrc522.uid)); if (status != MFRC522::STATUS_OK) { Serial.print(F("PCD_Authenticate() failed: ")); Serial.println(mfrc522.GetStatusCodeName(status)); return; } // Read to buffer mfrc522.MIFARE_Read(BLK_ADDR, buffer, &size); //Update buffer or reinitialize buffer if (increment) { buffer[0] = min(buffer[0] + 1, buffer[1]); } else { increment = true; flashed = true; buffer[0] = 0; buffer[1] = map(pos, 0, MAX_ENCODER, 0, MAX_SCANS); } // Write data to the block status = (MFRC522::StatusCode) mfrc522.MIFARE_Write(BLK_ADDR, buffer, 16); if (status != MFRC522::STATUS_OK) { Serial.print(F("MIFARE_Write() failed: ")); Serial.println(mfrc522.GetStatusCodeName(status)); } // Halt PICC mfrc522.PICC_HaltA(); // Stop encryption on PCD mfrc522.PCD_StopCrypto1(); // fill up ring and fade out to indicate newly flashed tag if (flashed) { for (int i = pos; i < MAX_ENCODER; i++) { display(i, MAX_ENCODER); delay(3); } fadeout(1); } //always display current strength animation fill_up_to(int(buffer[0]), int(buffer[1])); //Habit full strength, display lights if (buffer[0] == buffer[1]) { rainbow_cycle(); } last = millis(); } /** Helper routine to dump a byte array as hex values to Serial. */ void dump_byte_array(byte *buffer, byte bufferSize) { for (byte i = 0; i < bufferSize; i++) { Serial.print(buffer[i] < 0x10 ? " 0" : " "); Serial.print(buffer[i], HEX); } } //display encoder value on ring with m being full void display(int cur, int m) { int remapped = map(cur, 0, m, 0, BRIGHTNESS * LED_COUNT); int full_bright = remapped / BRIGHTNESS; int rem = (remapped) % BRIGHTNESS / 10 * 10; strip.clear(); if (full_bright) strip.fill(strip.ColorHSV(49152, 255, BRIGHTNESS), LED_COUNT - full_bright, full_bright); strip.setPixelColor(LED_COUNT - 1 - full_bright, strip.ColorHSV(49152, 255, rem)); strip.show(); } //fade out ring light at given speed void fadeout(int speed) { for (int i = BRIGHTNESS; i >= 0; i -= speed) { strip.setBrightness(i); strip.show(); } strip.clear(); strip.setBrightness(BRIGHTNESS); strip.show(); } //animate the ring filling up to an arc based on //value (cur) and what would be a full circle (m) void fill_up_to(int cur, int m) { int remapped = map(cur, 0, m, 0, BRIGHTNESS * LED_COUNT); int full_bright = remapped / BRIGHTNESS; int rem = (remapped) % BRIGHTNESS / 10 * 10; strip.clear(); strip.show(); for (uint16_t i = 0; i < full_bright; i++) { uint16_t hue = 0 - i * (65536 / LED_COUNT); for (int b = 0; b < 256; b += FILLUP_SPEED) { strip.setPixelColor(LED_COUNT - 1 - i, strip.ColorHSV(hue, 255, b)); delay(1); strip.show(); } } uint16_t hue = 0 - full_bright * (65536 / LED_COUNT); strip.setPixelColor(LED_COUNT - 1 - full_bright, strip.ColorHSV(hue, 255, rem)); strip.show(); } //rotate rainbow light around ring and fade out void rainbow_cycle() { strip.clear(); uint16_t prev = 0; for (uint16_t offset = 0; offset >= prev; offset += 65536 / LED_COUNT / 20) { prev = offset; for (uint16_t i = 0; i < LED_COUNT; i++) { uint16_t hue = offset - i * (65536 / LED_COUNT); strip.setPixelColor(LED_COUNT - 1 - i, strip.ColorHSV(hue, 255, BRIGHTNESS)); } delay(2); strip.show(); } fadeout(1); }
]]>
The Physical Pomodoro Clock is a productivity tool disguised as a laptop stand that is designed to assist the user in staying focused over longer work periods by allowing the user to set a goal for how they would like to divide their productive time between working and taking healthy breaks as well as reinforcing achieving that goal through incentivizes for getting closer to the goal and reminders for the user when they stray from their goal.
Here’s a rundown of how the Physical Pomodoro Clock works.
It’s probably best to understand how the Pomodoro system itself works, which is a very simple concept. Break up the entire time you intend to work into 30 minute blocks. Work for the first 25 minutes, then take a break to stretch and move around for the next 5 minutes. Rinse and repeat for every 30 minute block.
The Physical Pomodoro follows a similar concept. While the clock is running, the user will either be in work or break mode. It’s up to the user to let the clock know when they’re doing what, and the clock will keep track of the proportion of time they spend working or on break. When a 30 minute cycle completes, the clock will compare the user’s work-to-break ratio, measured as a percentage, to the target ratio that the user can define as they please. The closer the user’s work-to-break ratio is to the target ratio, the more tokens they are awarded with. These tokens can then be spent for a random chance that a piece of candy is pushed out from the computer stand to award the user for their productivity and encourage them to keep at it.
Viewable on the LCD screen, the user can utilize two menus, the main pomodoro clock menu and a ‘popup’ menu for spending tokens, the virtual currency awarded for completing work cycles.
When interacting with the clock, there are two buttons and a knob.
In the bottom right of the clock screen, there’s an indicator of whether the clock is running or not. If it is running, a “>” symbol will be shown and the clock time and progress indicators will be changing. If it is not running and thus in its paused state, a “||” symbol will appear. The clock as well as any progress on the current cycle will be set to 0 when it is paused.
Another useful indicator is the work vs. break indicator in the third column. If the current set mode is break, then there will be a “B” in the top row of the third column. Otherwise the mode is work, so there is a “W” in the bottom row of the third column.
The clock menu displays a graphical representation of the user’s work-to-break ratio in the left-most column and the target ratio in the column to the right of it. The left side of each column shows work percentage while the right side shows the break percentage. The work time percentages are also shown numerically in the top row, where the first percentage, which has a checkered ‘target’ icon before it is the target ratio, while the user’s ratio is at the far right, which seems to be updating to a mid-20 percent at the moment the photo was taken, thus the furthest left column is around 1/4 of the way full on its left side, but there’s nothing on its right side since no break time has been taken in that cycle. The target ratio at 94% is represented by a completely filled left half in the second column with not enough break time to be shown in the right half.
The ratio system has been scaled such that it has a minimum of 50% and a maximum of 95%.
Note: The cycle and token values are stored on the device and persist even after powering the device off and on.
The remaining two unexplained parts are the numbers followed by “cy” and the coin symbol followed by a “x” and a number. The first set of numbers is an indicator of purely how many cycles have been completed, irregardless of ratios achieved or not achieved. The second number is the number of total tokens the user has available to spend on the token menu.
I’d say this menu is pretty self-explanatory. If you press the red button, it’ll deduct 10 tokens and do some probability math to decide whether it gives you a prize or not.
The clock can notify the user through the use of a vibrating motor which is mounted in the lower right corner of the above photo, as well as flashing an LED in the bottom left corner of this photo.
When the clock is turned on, put into play, or completes a cycle, it’ll do one short buzz.
When the clock wants to remind the user to take a break or get back to work because they’re too far from their target ratio, it repeatedly makes short buzzes until the user complies or the cycle completes.
When the user wins a prize from spending their tokens, a long buzz will sound from the clock to commemorate the moment.
Video of switching menus
Video of spending tokens for reward
This project began as one of several proposed ideas for something that could be useful in my life. The initial idea was a vague design based on a Pomodoro clock with very minimal input, just 2 buttons and a potentiometer.
However, a mess of wires, electronic components, and boards aren’t exactly aesthetically appealing, so as the electronic circuit was nearly finished, I had to look for a suitable structure to mount the simple clock. As the goal of the project was to simply design something useful, I figured I’d mount the Pomodoro clock somewhere it would be most effective and give it a second purpose. A major reason I got a Pomodoro clock on my phone is how long I spend in front of a computer without getting up to move, which can be unhealthy. So I figured a computer stand could be a practical means to elevate my computer screen to a more comfortable level while being right where I need it most.
A picture of the WIP CAD design for the stand from the Fusion 360 software can be seen below.
Works great as a laptop stand, 1/4 in. plywood is pretty sturdy
In the later hours of working on this project, I had wavered on whether to keep or discard a reward system, where a virtual currency could be earned by following the Pomodoro clock closely. In the end, I decided I wanted to implement one and did a somewhat rushed job in adding a servo and some other parts to randomly reward the user with a candy when they spent the currency. Unfortunately, the candy I used did not fit very well through the hole it was designed to leave through, and the servo struggled to push the candy very far against friction. Perhaps if I had a different candy or designed the case to be larger to allow for more options, then it would’ve gone more smoothly.
A rainbow tangle of wires, normally kept hidden underneath the top panel of the laptop stand. The servo, candy, and, and part of a popsicle stick ramp can be seen on the right side, adjacent to the polycarbonate ‘window’.
Although much of the code ran smoothly, there were a few hitches. I had also uploaded a piece of code I had slightly modified but didn’t test thoroughly, thinking that there was no major affect. However, the notification system went off even at times it shouldn’t be during the demo, which made it rather difficult to demo the timer aspect due to the incessant buzzing from the vibration motor. But there was a very curious bug earlier in development when Japanese characters would appear, which was due to incorrect mapping to certain memory addresses of the LCD screen since the LCD screen came with Japanese and English characters by default that could be displayed on the screen. The intended goal was a few custom character slots that I had been writing the custom ratio indicator symbols to.
“It would be awesome if things are drilled onto the board instead of being taped.”
– I admit that the tape did not do as good a job as I hoped to hold things together. If I had more confidence in where things should be located, I may have gone ahead and glued it at least. Drilling wouldn’t be too bad for some components, but the breadboards I was using didn’t seem to be easily mounted in that manner, so I’d probably still avoid drilling.
“I wish we got a little more idea on how the timer itself worked.”
– And I wished I had given you a better idea about how it worked. That was just a flaw in my presentation skills after panicking from the notification system incorrectly going off while the timer was running. There was actually a lot of other functionality I wasn’t able to show, including a graphical representation of the ratios in the left two columns of the LCD, a break/work indicator column, and a play/plause indicator in the bottom right. But the basics are about the same as the classic Pomodoro clock. The ratio potentiometer adjusts a target goal to break out of the hard set 25-5 minute ratio, instead letting the user define the proportion of work they want to get done with each cycle. With the system of letting the user indicate whether they’re on break or working, my system gives the user more freedom in defining when they work and when they take a break, so the break doesn’t have to be at the end of the cycle. When the cycle completes, a productivity ratio closer to the self-chosen target ratio would award more tokens, which seem to be an aspect that received much praise, despite the system of reward delivery being close to non-functional.
“The circular acrylic window for the rectangular LCD display doesn’t make to much sense to me”
– I thought it would look cool to be able to see inside at the electronics underneath. Unfortunately, there’s not much in terms of LEDs or anything else to see, so it probably would’ve been better to stick to a square hole that would’ve made mounting it infinitely easier. So I certainly agree that the acrylic window was fairly unnecessary, though I think it does kind of look cool.
“Does it only hold one piece of candy at a time?” + “Try to use a smaller and lighter candy”
– Sadly yes, it holds only one piece of candy, since I was rather constrained by the space within the computer stand. If I was feeling more creative, I might play it off as a feature to encourage the user to get up and refill the candy each time instead of sit in front of the computer the whole time. If I were to do it again, especially if I wanted to hold more than one candy, I’d probably use a smaller and lighter candy that came in a shape that could easily roll, like a tic-tac, though I would have had to go buy the candy then.
“Nice popsicle stick” – Indeed.
After having completed the project, I can look back and say that there were at least two major problems. The first was creating a design that suffered too much from feature bloat. The design would’ve greatly benefited from retaining the simplicity of a basic Pomodoro clock to go with its simple controls. The whole reward system was an entertaining concept, but it appears to be detrimental to add unnecessary features that take away from the intentionally simple design. It probably would’ve been better to have used the second menu option for more in depth clock configurations.
The other problem is a general lack of planning for a number of issues. These included somewhat minor issues: a lack of space for the electronics to easily fit beneath the top board of the stand, not cutting certain openings a little smaller to account for the width of the laser, not laser cutting a slot for the small ‘tab’ in the potentiometer to fit into, as well as somewhat major ones: lack of a plan for mounting the devices and unclear aesthetic goals for the project. While the initial idea was feature-rich, it was lacking in implementation specifics.
This project has been a very valuable learning experience. The many issues that I didn’t anticipate this time around are something I can learn from and hopefully address when I can anticipate them in future projects and avoid making the same mistakes this time around. I also had to think outside the lasercut box for this project, opting for a laptop stand instead of a generic box. So at least, even if the Pomodoro clock doesn’t alway work, I can rely on it as a fine laptop stand. If I had another shot at it, I’d see if I could make it bigger with clear acrylic and set up a better candy delivery system, probably with a steeper ramp and a servo controlled valve that delivered smaller and rounder candies. The clear acrylic would let you could see the electronics working inside the stand as well as the candies moving around, which I think would be cool, plus the whole stand could pulse with the color of the internal LED when the user was being notified. I’d also see if I could fix the bug where the notification system goes off all the time when the intended behavior is to notify the user when they should end their break. At least it reminds the user to start their breaks and when a cycle ends, just like a classic Pomodoro clock.
/* Physical Pomodoro Clock Description: Code for taking input from two buttons and a potentiometer to control a pomodoro clock with a notification and reward functionality Pin Mapping Input Pin | Input 2 Button 1 (RED) 3 Button 2 (BLACK) A0 Potentiometer A5 Random Noise Pin Output Pin | Output 5 Notification system (Vibration Motor + LED) 7 Servo Pin //Referenced resources LCD Screen code contains snippets and references to code written by Robert Zacharias at Carnegie Mellon University, rzach@cmu.edu released by the author to the public domain, November 2018 //https://forum.arduino.cc/index.php?topic=418257.0 //https://forum.arduino.cc/index.php?topic=215062.0 //https://learn.robotgeek.com/28-robotgeek-getting-started-guides/61-robotgeek-i2c-lcd-library#customChar //https://learn.robotgeek.com/getting-started/59-lcd-special-characters.html */ #include <Wire.h> #include <LiquidCrystal_I2C.h> #include <Servo.h> #include <EEPROM.h> /* Create an LCD display object called "screen" with I2C address 0x27 which is 16 columns wide and 2 rows tall. You can use any name you'd like. */ LiquidCrystal_I2C screen(0x27, 16, 2); Servo gate; //Pins const int BUTTON1_PIN = 2; const int BUTTON2_PIN = 3; const int RATIO_PIN = A0; const int NOTIFIER_PIN = 5; const int REWARD_PIN = 7; const int RANDOM_PIN = A5; //Intentionally unconnected to be a source of random noise //*** //***Utilities*** //Mode bool isPomo = true; //Notifier bool canNotify = false; unsigned long notifyTimer = 0; unsigned long endTimer = 0; bool flop = false; int flopFreq = 0; int duration = 0; //Masks const int B1P_MASK = B0001; const int B1H_MASK = B0010; const int B2P_MASK = B0100; const int B2H_MASK = B1000; //Time constants const int delayRate = 100; //Refresh rate in ms for the whole system const int renderRate = 800; //rate of rendering //Max Pot Value const int MAX_POT = 1023; //Represents the highest potentiometer output //Buttons //tracks how many delays int counter = 0; //The minimum time in ms necessary for a press to count as any press const int minPressTime = 300; //The minimum time in ms necessary for a hold to register const int minHoldTime = 2000; //int[] for button hold times unsigned int button_Times[2]; //Reward Gate parameters const int gateTime = 5000; //2 seconds to grab the candy if won const int openAngle = 5; const int closedAngle = 90; //***Pomodoro unsigned long referenceTime = 0; //Whenever the clock is 'paused', this number is no longer useful and must be reset on resuming pomodoro. Thus a pause resets progress on that pomodoro. //For higher accuracy on progress time unsigned long lastTick = 0; unsigned int elapsed = 0; //Once elapsed in seconds reaches the equivalent of 30 minutes, a cycle will be updated, and 'prizes' awarded. Then it will be reset unsigned long progress = 0; //productive time in milliseconds unsigned int cycles = 0; const unsigned int CYCLE_CONSTANT = 60; //number of seconds in 30 minute pomodoro //const byte pRatios[6] = {1, 2, 4, 5, 9, 14}; //productivity ratio numbers to 1 float ratio = 0.83; //number of productive seconds //Ratio limits. Max break is 15 min, while min break is 2 min per 30 minute pomodoro const float minRatio = 0.5; const float maxRatio = 0.95; //ratio and progress percentages respectively byte rPercent = 0; byte pPercent = 0; bool isPaused = true; bool isBreak = false; //byte columns byte left_c = B11000; byte right_c = B00011; //symbol like > to indicate clock is running #Not Used byte playSign[] = { B10000, B11000, B11100, B11111, B11100, B11000, B10000, B00000 }; // (note the extra row of zeros at the bottom) //symbol like || to indicate clock is paused byte pauseSign[] = { B10001, B11011, B11011, B11011, B11011, B10001, B00000, B00000 }; // (note the extra row of zeros at the bottom) //*** //Not Used byte upSign[] = { B00000, B00100, B01110, B11111, B00000, B00000, B00000, B00000 }; // (note the extra row of zeros at the bottom) //Not Used byte downSign[] = { B00000, B00000, B00000, B11111, B01110, B00100, B00000, B00000 }; // (note the extra row of zeros at the bottom) //Resembles a coin byte rewardSign[] = { B00100, B01010, B11101, B11101, B11101, B01110, B00100, B00000 }; // (note the extra row of zeros at the bottom) //Resembles a target byte targetSign[] = { B00000, B00100, B01010, B10101, B01010, B00100, B00000, B00000 }; // (note the extra row of zeros at the bottom) /* //This is adjustable and determined by code byte targetSign[] = { B00000, B00000, B00000, B00000, B00000, B00000, B00000, B00000 }; // (note the extra row of zeros at the bottom) //This is determined by code and updated as progress changes byte progressSign[] = { B00000, B00000, B00000, B00000, B00000, B00000, B00000, B00000 }; // (note the extra row of zeros at the bottom) */ //***Reward System unsigned int tokens = 100; //Total 'currency' unsigned int spinPrice = 10; //Price to attempt to draw a prize bool trySpin = false; //Indicates whether an attempt to draw a prize is made const int randomThreshold = 40; //Percent chance that prize is won //*** struct saveData { unsigned int savedCycles; unsigned int savedTokens; }; void setup() { Serial.begin(9600); saveData lastSave; //Assumed that EEPROM will read and write from first address EEPROM.get(0,lastSave); Serial.println(lastSave.savedTokens); cycles = (lastSave.savedCycles!=65535)?lastSave.savedCycles:0; tokens = (lastSave.savedTokens!=65535)?lastSave.savedTokens:0; //Random Seed setup randomSeed(analogRead(RANDOM_PIN)); //Setup Pins //Buttons pinMode(BUTTON1_PIN, INPUT); pinMode(BUTTON2_PIN, INPUT); //Potentiometer pinMode(RATIO_PIN, INPUT); //Audio-visual Notification pinMode(NOTIFIER_PIN, OUTPUT); gate.attach(REWARD_PIN); //turn gate to closed position initially gate.write(closedAngle); // initialize the screen (only need to do this once) screen.init(); // turn on the backlight to start screen.backlight(); //Loads symbols into memory screen.createChar(0, playSign); screen.createChar(1, pauseSign); //2 is reserved for both up and down signs and dynamically set later screen.createChar(3, rewardSign); screen.createChar(4, targetSign); // set cursor to home position, i.e. the upper left corner //screen.home(); screen.clear(); screen.home(); //screen.print("b1 = "); //screen.setCursor(0, 1); //screen.print("b2 = "); digitalWrite(5, HIGH); delay(1000); digitalWrite(5, LOW); //tokens = 100; } void loop() { //screen.clear(); delay(delayRate); //screen.setCursor(5, 0); //screen.print(digitalRead(2)); //screen.setCursor(5, 1); //screen.print(digitalRead(3)); //Serial.println(analogRead(RATIO_PIN)); byte buttonData = buttonInput(); //Button Data: #### //Last # (2^0): B1 Press //Left1# (2^1): B1 Hold //Left3# (2^2): B3 Press //Left4# (2^3): B4 Hold //A short hold has short hold logic //A long hold has long hold logic //if it isn't paused, process data from last loop if (!isPaused)pomodoro_tick(); //Divides logic based on whether the mode is Pomodoro or not if (isPomo) { //updates various internal variables for this display mode pomodoro_logic(); //Render to screen step if (counter > renderRate)renderPomo(); //Button press logic //If the 1st button has been pressed and released, ***PLAY*** if ((buttonData & B1P_MASK) != 0) { //start/resume functionality //Start from paused if (isPaused) { isPaused = false; //Re-syncs the reference time referenceTime = millis(); //syncs up the lasttick initially lastTick = millis(); //enables notifies enableNotify(); } //Resumes from break if (isBreak) isBreak = false; } //If the 1st button has been held, ***SHOP*** if ((buttonData & B1H_MASK) != 0) { //Switch menus isPomo = !isPomo; //Limits clear commands to when screen changes screen.clear(); } //If the 2nd button has been pressed, Mark Break ***BREAK*** if ((buttonData & B2P_MASK) != 0) { isBreak = true; } //If the 2nd button has been held, Mark Pause/End ***PAUSE*** if ((buttonData & B2H_MASK) != 0) { isPaused = true; //reset all current progress and time, this way there won't be any surprises when resyncing resets progress progress = 0; elapsed = 0; //Disable notifies disableNotify(); } } else { //shop Logic, only relevant when in shop, so does not need to be in main loop body shopLogic(); //Render Entertainment if (counter > renderRate)renderShop(); //press button 1 if ((buttonData & B1P_MASK) != 0) { //the spend your token sort of fun stuff trySpin = true; } //press button 2 if ((buttonData & B2P_MASK) != 0) { //returns to main screen isPomo = !isPomo; //Limits clear commands to when screen changes screen.clear(); } } //Ensures that counter always increments, so refresh doesn't get stuck at menu changes counter += delayRate; } //Relevant Logic for the Shop void shopLogic() { //Spin button pressed if (trySpin) { //Tokens are charged if (tokens >= spinPrice) { tokens -= spinPrice; generateReward(); updateSave(); } else { //show inssuficient funds //Serial.println("fail"); screen.clear(); screen.home(); screen.print("Insufficient "); screen.write(3); } trySpin = false; } } void renderShop() { counter = 0; //screen.clear(); screen.home(); //Default Screen //Top Row screen.print("Token Spin | Own"); //Bottom Row screen.setCursor(0, 1); screen.print("Use "); screen.write(3); screen.print("x" + (String)spinPrice); screen.setCursor(11,1); screen.print("|"); screen.write(3); screen.print("x" + (String)tokens); } //to be implemented void generateReward() { screen.clear(); screen.home(); int randNumb = random(100); //Generates a random number from 0 to 99 (100 possible numbers) if(randNumb<randomThreshold){ //If the number is less than the threshold, or percent chance to win, then the user wins a prize gate.write(openAngle); //Opens the prize gate screen.print("You Win a Prize!"); digitalWrite(NOTIFIER_PIN, HIGH); //Some other indications of winning delay(gateTime); //Time to claim prize gate.write(closedAngle); //Closes the prize gate digitalWrite(NOTIFIER_PIN, LOW); //Silences the notification } else{ screen.print("...Try again?"); //A somewhat encouraging message in case of loss delay(1000); } } //byte Cells[2][8] = {}; //fill rows 0-6 of 1D arrays //byte Cell1[8] = {}; //byte cell2[8] = {}; //print double column (each cell has 7 rows) void RatioPercent(int column, float r1, float r2, byte slot) { //These individual byte arrays may be converted to a 2D array //byte topCell[8]; //fill rows 0-6 //byte lowCell[8]; //fill rows 0-6 int divisions = 7; int cells = 2; //starts at the bottom of the cell, row 1 for (int j = cells - 1; j >= 0; j--) { //j = cell index, therefore //j is inverse for left //j is direct for right byte temp[8] = {}; for (int i = divisions - 1; i >= 0; i--) { //i begins at the bottom rows, at index 6 //left: the relative number of rows that should be lit : Right (cells begin from the bottom, as do the row writing, thus thye have to be subtracted) if (r1 * divisions * cells > (cells - j - 1)*divisions + (divisions - i)) { //Cells[j][i] = Cells[j][i]|left_c; temp[i] = B11000; } // //At the bottom ,it would be all previous cells plus all rows of last cell (j*divisions) + i rows if (r2 * divisions * cells > (j)*divisions + i + 1) { //Cells[j][i] = Cells[j][i]|right_c; temp[i] = temp[i] | B00011; } } screen.createChar(slot + j, temp); //Print current cell screen.setCursor(column, j); screen.write(slot + j); } } //Renders the screen for the pomo mode void renderPomo() { //Resets the counter for screen refresh counter = 0; //clear the screen //screen.clear(); //Personal Progress/ratio (column 0, both rows) RatioPercent(0, pPercent / 100.0, (float)elapsed / CYCLE_CONSTANT - pPercent / 100.0, 5); //Target Progress/ratio (column 1, both rows) RatioPercent(1, rPercent / 100.0, 1 - rPercent / 100.0, 7); //Break or work (column 2, both rows) screen.setCursor(2, 0); //isBreak is true (top) screen.print((isBreak) ? ("B") : (" ")); //Prints B for break on top or empty for not break screen.setCursor(2, 1); //isBreak is false (bottom) screen.print((isBreak) ? (" ") : ("W")); //Prints B W for work on bottom or empty for on break //target percent (column 3-6, row 0) char char_buffer [6]; // a few bytes larger than your intended line sprintf (char_buffer, "%02d", rPercent); screen.setCursor(3, 0); screen.write (4); //target sign, index 3 screen.print (char_buffer); // index 4,5 screen.write (37); //percent symbol, index 6 //# of cycles (column 4-7, row 1) screen.setCursor(4, 1); //screen.print(cycles); sprintf (char_buffer, "%02d", cycles); //number of cycles. May go into the 10s, but assumed that using this for 100 cycles or 3000 continuous minutes is unlikely screen.print(char_buffer); screen.setCursor(6, 1); screen.print("cy"); //for cycles //Prints time string (column 7-11, row 0) //char char_buffer2[6]; sprintf (char_buffer, "%02d%s%02d", elapsed / 60, ":", elapsed % 60); // send data to the buffer screen.setCursor(7, 0); screen.print(char_buffer); //print reward string (column 9-13or14, row 1) screen.setCursor(9, 1); screen.write(3); //reward sign, index 8 sprintf (char_buffer, "%03d", tokens); screen.print("x"); screen.print(char_buffer); //indicator of current percent (c12-15, r0) //Writes percentage screen.setCursor(12, 0); if(pPercent<100){ //to avoid three digits pushing the line off the screen sprintf (char_buffer, "%3d", pPercent); screen.print (char_buffer); // index 4,5 } screen.write (37); //percent symbol, index 6 /* screen.setCursor(12, 0); //print indicator of more work or more rest //threshold is hard set as 5 currently, can be serialized as a const variable if necessary if (pPercent - rPercent > 5) { screen.createChar(2, downSign); screen.write(2); } else if (rPercent - pPercent > 5) { screen.createChar(2, upSign); screen.write(2); } else screen.print(" "); */ //indicator of pause or play status screen.setCursor(15, 1); screen.write((isPaused) ? (1) : (62)); //if paused, print pause symbol, if playing, use > for play symbol (62). } //Pomodoro Logic to keep variables updated while clock functionality is in play void pomodoro_tick() { //temp long //moves progress if not on break elapsed = (unsigned int)((unsigned long)(millis() - referenceTime) / (unsigned long)1000); if (!isBreak)progress += millis() - lastTick; lastTick = millis(); //Loops pomodoro once time reaches limit if (elapsed > CYCLE_CONSTANT) { elapsed = 0; progress = 0; cycles += 1; calcReward(); referenceTime = millis(); updateSave(); } //Reminder Logic //Operate on the NOTIFIER_PIN //Long LED and vibration for when cycle restarts if(elapsed == 0){ //an indicator that a cycle restart occurred startNotify(1000, 0); } //Short pulses when break should begin if((!isBreak)&&(pPercent-rPercent>5)){ if(duration==0)startNotify(1000,1); } //Short pulses when work should resume if((isBreak)&&((int)((float)(elapsed*100)/CYCLE_CONSTANT-pPercent)-(100-rPercent)>5)){ if(duration==0)startNotify(1000,1); } if(canNotify)writeNotify(); } //Relevant logic that occurs in pomodoro screen mode but not related to its ticking (clock) functionality void pomodoro_logic() { ratio = analogRead(RATIO_PIN)*(maxRatio - minRatio)/MAX_POT+minRatio; //Sets the ratio proportional to input potentiometer signal within the range between min and max ratio rPercent = (int)(ratio * 100); pPercent = ((int)(progress / (unsigned long)10) / (CYCLE_CONSTANT)); } //Determines Reward for completing a pomodoro void calcReward() { //Uses rPercent and pPercent to determine accuracy thresholds if(abs(pPercent-rPercent)<10)tokens += 10; else if(abs(pPercent-rPercent)<20)tokens+=8; else if(abs(pPercent-rPercent)<30)tokens+=6; else tokens+=4; } void startNotify(int totalTime, int flopfrq){ notifyTimer = millis(); endTimer = notifyTimer + totalTime; flop = false; flopFreq = flopfrq; duration = totalTime; digitalWrite(NOTIFIER_PIN, HIGH); } //Determines when and how to end the notify void writeNotify(){ Serial.println("Write notify"); //Checks if there's a timer in progress, i.e. duration has been set. if(duration>0){ //if the timer time has reached the flop time (the entire duration if 0 flopFreq) if(notifyTimer+(duration/(flopFreq+1))<=millis()){ Serial.println("writing"); digitalWrite(NOTIFIER_PIN, flop?HIGH:LOW); flop = !flop; notifyTimer = millis(); } //Deactivates timer if the time has been reached if(endTimer<=notifyTimer){ duration = 0; digitalWrite(NOTIFIER_PIN, LOW); } } } //Allows notify system to go void enableNotify(){ canNotify = true; } //Prevents notify system from going off void disableNotify(){ digitalWrite(NOTIFIER_PIN, LOW); canNotify = false; duration = 0; } //Commits the persistent data to storage void updateSave(){ saveData newSave = { cycles, tokens }; //Assumed same address EEPROM.put(0,newSave); } byte buttonInput() { //Register Button Hold Times //For that button, detect if it is held or released. //If the array elements are greater than 0, then that element has been held earlier. //Minimum time registered as any hold to avoid any flickering byte input = 0; //button 1 logic if (digitalRead(BUTTON1_PIN)) { //Determines if the signal duration has entered the hold range if (button_Times[0] >= minHoldTime)input = input | B1H_MASK; //While held, the button time will continue to increment button_Times[0] += delayRate; } else { //if the button hold duration is long enough to count as an intentional signal but not a hold, then it must be a press signal if (button_Times[0] >= minPressTime && button_Times[0] < minHoldTime)input = input | B1P_MASK; //Since the button has been 'released' or is currently not pressed, time is reset to 0. button_Times[0] = 0; } //Same logic as above but for button 2 if (digitalRead(BUTTON2_PIN)) { //Determines if the signal duration has entered the hold range if (button_Times[1] >= minHoldTime)input = input | B2H_MASK; //While held, the button time will continue to increment button_Times[1] += delayRate; } else { //if the button hold duration is long enough to count as an intentional signal but not a hold, then it must be a press signal if (button_Times[1] >= minPressTime && button_Times[1] < minHoldTime)input = input | B2P_MASK; //Since the button has been 'released' or is currently not pressed, time is reset to 0. button_Times[1] = 0; } return input; }
]]>
This is a smart 4-plant watering machine that caters to the different need of each plant.
Front view
Back view
I own a couple unfortunate plants, two small ones and two big ones. They suffer from dehydration whenever I’m away from home. I decided to design a plant watering machine because I want them to stay alive. Moreover, I want to design a watering machine that caters to the different need of every plant. The idea is that I am able to set a watering cycle such that the machine repetitively waters the plants with different amount of water every set amount of time. For example, I can configure the machine to water the 2 small plants with 1 portion of water and 2 big plants with 3 portions of water everyday.
The final product allows me to set the wait time between each watering cycle with 3 preset buttons (0~3 units of time). Also, for each individual plant, there are 3 preset buttons to set the watering amount (0~3 portions).
Labelled LED Interface:
plant 1 watering amount = 3 (15 seconds of watering)
plant 2 watering amount = 0
plant 3 watering amount = 2 (10 seconds of watering)
plant 4 watering amount = 1 (5 seconds of watering)
Time between cycle = 2 (20 seconds between each cycle)
In action (disclaimer: the LED doesn’t jitter in reality. The frame rate aliasing makes it to look jittery in the video)
Valve close-up
Valves
Tubing
One of the hardest problems of this project was designing the valves. It was a mechanical problem and I had zero experience in designing mechanics. I tried to design the valve three times and the last time worked.
In the first iteration, I built a valve with two crossing screws such that when the servo rotates, it “pinches” the tube to stop the water from flowing. However, this design was not good, because first of all, the tube was too hard to pinch, and second, the cross shape of the plastic base always trip on the tube and move the tube away.
Soft tube (ordered)
Hard tube (original)
For the second iteration, I ordered softer tubes that made it possible for the servo to stop water from flowing. In the above pictures, you can see the soft tube is much easier to bend.
Final valve design
In the third iteration, I remade the valves with round plastic base and wide apart screws. The crossing screws might cut the tube open. So I designed another version of the valve where the screws won’t pinch the tube, but rather stretch the tube. The soft tube is flexible enough to be stretched. The round plastic base also allows the tube to stay in place when the valve rotates.
Closed vs open valves
This is how the valves look like when they are all open.
Prototype
This is the prototype version of this watering machine. In the above image, you can see the first iteration of the valve design.
I also considered using another pump, but this pump is WAY too powerful for watering purpose.
Tested many servos to find the ones that don’t jitter when the pump is on
Capacitor
One problem I encountered was that the servos jitter really hard when the pump was on. I tested all servos in physical computing lab and found 4 servos that don’t jitter. I also added a capacitor to smooth out the power supply.
Labelled LED Interface: plant 1 watering amount = 3 (15 seconds of watering) plant 2 watering amount = 0 plant 3 watering amount = 2 (10 seconds of watering) plant 4 watering amount = 1 (5 seconds of watering) Time between cycle = 2 (20 seconds between each cycle)
I also found it challenging to design the interface of the watering machine. I chose the Adafruit Trellis squishy 4×4 LED and button matrix simply because I love the feeling of pressing on them. However, with 16 buttons and LEDs it’s hard to tell the user how to use it and what the setting is. If I want to teach someone else to use the watering machine, I’d love it to be easy to explain and remember. The interface I came up with is the following. The top left button is a red button that execute the setting when it’s pressed. It blinks to confirm the setting. Column 2-4 allow the user to toggle the watering level from 0-3. The first column sets the wait time between each watering cycle from 0-3.
However, without the labels, it is extremely difficult to explain. Therefore, I labeled the buttons and asked someone who has never seen this project to try to interpret it. The only confusion with the labels is the time between cycle setting. The person wondered if it sets the watering frequency during a day or the amount of wait time between each cycle. Currently the time between cycle sets the amount of wait time between each cycle. One unit of time is 10 seconds. So in the above setting, there’s a 20-second wait between each cycle.
“The interface might be confusing to some—no labels so it’s easy to get lost!”
Yeah I agree. It’d be great to use different colors to indicate the different functionality of each column. I could have also used a LED screen to show words and numbers. It was a personal decision to use the 4×4 LED and button matrix and I didn’t have enough variety of LED colors to color-code the functionality.
“I found the mechanism interesting, difficult to construct and very useful! Bravo Jeena!!”
Wahhoooo! It was pretty difficult to construct the valves. It took 3 iterations to finally get the valves to tighten properly. In the first iteration, I had two crossing screws close the valve by pinching the tube, but the tube was too hard to actually close up. The second iteration I bought softer tubes, which made it much easier. However, crossing screws were too weak to form a close seal. In the third iteration, I designed a slightly different valve mechanism, where the soft tubes are fixed on both end with zip ties and hot glue, and the valve simply stretch and bend the tube to form a seal.
I mostly agree with the comments I received at the critique. I’m happy with how it turned out to be — a functioning watering machine that can water 4 different plants with a sufficient interface (for me). But I’d love it to be smaller, something that can be tucked away in my living room. Right now, it’s a giant open shelf of wires. It’s not pretty enough to occupy that much space. It’d be great to use a smaller clear acrylic box that encloses everything and allows me to see through.
Mechanical problems are real.
I didn’t think the valves are hard to design at all. The idea is to water one plant at a time, so I close all valves but one to water only one plant. To switch between valves, I open another valve and close all other valves. I spent most time trying different ways to stop water from flowing while another valve is opened. In the prototype week, I didn’t figure out the mechanical problem. Next time, I should try to figure out the hardest part first.
Also, I gained a ton of soldering skills by soldering 32 LEDs in one go for the button interface, plus many wires later on. I learned how to be very careful and fast at the same time.
I drilled many holes to make the valves work. I drilled holes into the plastic pieces that come with the hobby servos first, then screw the screws in. It was not easy at all, given that the holes are all so tiny. Also, I drilled holes for the zip ties to secure the tubes onto the wooden shelf.
I learned how to pronounce “valve” correctly. It’s “vaaaalv” not “volve”. Ah.
I will make a box with a clear acrylic door that can enclose the box.
Schematic
Schematic
Shelf design
/* * Project 2 * Jeena Yin (qyin) * It took two weeks * * Collaboration: * Referenced code in * https://learn.adafruit.com/adafruit-trellis-diy-open-source-led-keypad/connecting * https://courses.ideate.cmu.edu/60-223/f2019/tutorials/code-bites#blink-without-blocking * * Summary: The code below waters 4 plant with different watering * amount according to the setting * * Inputs: * Adafruit Trellis LED buttons | PIN A2 * Valve 0 button | PIN 4 * Valve 1 button | PIN 5 * Valve 2 button | PIN 7 * Valve 3 button | PIN 8 * * Outputs: * Valve 0 servo | PIN 6 (PWM) * Valve 1 servo | PIN 9 (PWM) * Valve 2 servo | PIN 10 (PWM) * Valve 3 servo | PIN 11 (PWM) * Pump motor | PIN 3 */ #include <Wire.h> #include <Servo.h> #include "Adafruit_Trellis.h" #define NUMTRELLIS 1 #define numKeys (NUMTRELLIS * 16) #define INTPIN A2 // Valve 0 const int VALVE0_PIN = 6; // PWM const int VALVE0_SWITCH_PIN = 4; // for testing // Valve 1 const int VALVE1_PIN = 9; // PWM const int VALVE1_SWITCH_PIN = 5; // for testing // Valve 2 const int VALVE2_PIN = 10; // PWM const int VALVE2_SWITCH_PIN = 7; // for // Valve 3 const int VALVE3_PIN = 11; // PWM const int VALVE3_SWITCH_PIN = 8; // for testing // Pump const int PUMP_PIN = 3; const int VALVE_CLOSE_POS = 0; const int VALVE_OPEN_POS = 140; // 10 seconds as cycle length: if time set to 2 then water every 20 seconds const int CYCLELENGTH = 10; // 5 seconds as unit for watering amount const int WATERAMOUNTUNIT = 5; // Blink the red LED as feedback confirmation const int blinkLEDdelay = 70; // Array that stores the water amount setting int waterAmount[] = {0, 0, 0, 0}; int waterFrequency = 0; unsigned long microTimer = 0; unsigned long macroTimer = 0; unsigned long quarterMacroTimer = 0; // wait time between watering cycles in second unsigned long quarterMicroTimer = 0; // wait time between each plant in second int wateringPlantId = -1; // id of plant being watered // only water if frequency > 0 and wateramount is > 0 for any plant bool shouldWater = false; bool waitingForNextCycle = false; Servo valve0; Servo valve1; Servo valve2; Servo valve3; bool valveStates[] = {false, false, false, false}; Adafruit_Trellis matrix = Adafruit_Trellis(); Adafruit_TrellisSet trellis = Adafruit_TrellisSet(&matrix); void setup() { // put your setup code here, to run once: pinMode(PUMP_PIN, OUTPUT); pinMode(VALVE0_SWITCH_PIN, INPUT_PULLUP); pinMode(VALVE1_SWITCH_PIN, INPUT_PULLUP); pinMode(VALVE2_SWITCH_PIN, INPUT_PULLUP); pinMode(VALVE3_SWITCH_PIN, INPUT_PULLUP); pinMode(INTPIN, INPUT); Serial.begin(9600); valve0.attach(VALVE0_PIN); valve1.attach(VALVE1_PIN); valve2.attach(VALVE2_PIN); valve3.attach(VALVE3_PIN); for(int i = 0; i < 4; i++){ OpenValveForPlant(i); } OpenAllValves(); digitalWrite(INTPIN, HIGH); trellis.begin(0x70); // turn on all LEDs for (uint8_t i=0; i<numKeys; i++) { trellis.setLED(i); trellis.writeDisplay(); delay(50); } // then turn them off for (uint8_t i=0; i<numKeys; i++) { trellis.clrLED(i); trellis.writeDisplay(); delay(50); } trellis.setLED(0); // light up the red button only trellis.writeDisplay(); } void loop() { if (trellis.readSwitches()) { for (uint8_t i=0; i<numKeys; i++) { if (i == 0 && trellis.justReleased(i)) { BlinkLED(i); } if (trellis.justPressed(i)) { ButtonPressed(i); } } // tell the trellis to set the LEDs we requested trellis.writeDisplay(); } // Watering plants if(shouldWater) { if(millis()/1000 - microTimer >= quarterMicroTimer) { // switch to next plant if(wateringPlantId == 3) WaitForNextCycle(); // Cycle ends else{ GetReadyForPlant(wateringPlantId + 1); } } } // Waiting for next watering cycle else if(waitingForNextCycle) { if(millis()/1000 - macroTimer >= quarterMacroTimer) { Execute(); macroTimer = millis()/1000; } } // Only check test switches when it's not watering. else{ CheckSwitches(); } delay(30); } void ButtonPressed(int i) { // The red button is pressed if(i == 0) { Execute(); // if it was pressed, turn it on if (trellis.justPressed(i)) { trellis.setLED(i); } return; } int col = i % 4; int row = (int) (i / 4); // Set Time if(col == 0) { // Interface design silimar to a slider if (trellis.isLED(i)) { if(row == 3 || !trellis.isLED(i+4)) { // Frequency = 0 SetFrequency(0); for(int k = 1; k <= 3; k++) { trellis.clrLED(k*4); } } else { // Adjust Frequency SetFrequency(row); for(int k = 1; k <= row; k++) { trellis.setLED(k*4); } for(int k = row+1; k <= 3; k++) { trellis.clrLED(k*4); } } } else { // Adjust Frequency SetFrequency(row); for(int k = 1; k <= row; k++) { trellis.setLED(k*4); } for(int k = row+1; k <= 3; k++) { trellis.clrLED(k*4); } } } // Set water level else { // Interface design silimar to a slider if (trellis.isLED(i)) { if(col == 3 || !trellis.isLED(i+1)) { // No water SetWater(row, 0); for(int k = 1; k <= 3; k++) { trellis.clrLED(k+row*4); } } else { // Adjust water level SetWater(row, col); for(int k = 1; k <= col; k++) { trellis.setLED(k+row*4); } for(int k = col+1; k <= 3; k++) { trellis.clrLED(k+row*4); } } } else { // Adjust water level SetWater(row, col); for(int k = 1; k <= col; k++) { trellis.setLED(k+row*4); } for(int k = col+1; k <= 3; k++) { trellis.clrLED(k+row*4); } } } } // Helper function that starts watering the plant right now void Execute() { // Close all valves for(int i = 0; i < 4; i++) { CloseValve(i); } // only water if frequency > 0 and total wateramount is > 0 shouldWater = (waterFrequency > 0 && WaterAmountNonZero()); waitingForNextCycle = false; if(shouldWater) { Serial.println("Start watering"); TurnOnPump(); // Initialize global variables GetReadyForPlant(0); } else { TurnOffPump(); OpenAllValves(); Serial.println("Stop watering"); } } void GetReadyForPlant(int id) { Serial.print("Get ready for plant "); Serial.println(id); microTimer = millis()/1000; quarterMicroTimer = GetWaterTimeForPlant(id); Serial.print("Water amount(seconds): "); Serial.println(quarterMicroTimer); wateringPlantId = id; OpenValveForPlant(id); } void WaitForNextCycle() { Serial.println("Wait for next cycle... "); TurnOffPump(); OpenAllValves(); shouldWater = false; waitingForNextCycle = true; macroTimer = millis()/1000; quarterMacroTimer = waterFrequency * CYCLELENGTH; Serial.print("Wait time(seconds): "); Serial.println(quarterMacroTimer); } int GetWaterTimeForPlant(int id) { return waterAmount[id] * WATERAMOUNTUNIT; } // Change the global watering frequency per minute void SetFrequency(int frequency) { Serial.print("Set time: "); Serial.println(frequency); waterFrequency = frequency; } // Set the array of watering amount void SetWater(int plantId, int water) { Serial.print("Set water: "); Serial.print(plantId); Serial.print(" "); Serial.println(water); waterAmount[plantId] = water; } // Only close one valve void CloseValve(int valve) { // Don't close if is already closed if(!valveStates[valve]) return; else valveStates[valve] = false; Serial.print("Close "); Serial.println(valve); switch(valve) { case 0: valve0.write(VALVE_CLOSE_POS); break; case 1: valve1.write(VALVE_CLOSE_POS); break; case 2: valve2.write(VALVE_CLOSE_POS); break; case 3: valve3.write(VALVE_CLOSE_POS); break; default: break; } delay(50); } void OpenValve(int valve) { // Don't open if is already open if(valveStates[valve]) return; else valveStates[valve] = true; Serial.print("Open "); Serial.println(valve); switch(valve) { case 0: valve0.write(VALVE_OPEN_POS); break; case 1: valve1.write(VALVE_OPEN_POS); break; case 2: valve2.write(VALVE_OPEN_POS); break; case 3: valve3.write(VALVE_OPEN_POS); break; default: break; } delay(50); } // Helper function returns true if any plant water amount is > 0 bool WaterAmountNonZero() { for(int i = 0; i < 4; i++) { if(waterAmount[i] > 0) return true; } return false; } void CheckSwitches() { // return; if(!digitalRead(VALVE0_SWITCH_PIN)) { OpenValveForPlant(0); } if(!digitalRead(VALVE1_SWITCH_PIN)) { OpenValveForPlant(1); } if(!digitalRead(VALVE2_SWITCH_PIN)) { OpenValveForPlant(2); } if(!digitalRead(VALVE3_SWITCH_PIN)) { OpenValveForPlant(3); } } // Open valve for only plant id, close all other valves void OpenValveForPlant(int id) { for(int i = 0; i < 4; i++) { if(i == id) OpenValve(i); else CloseValve(i); } Serial.print("Opening valve for only plant "); Serial.println(id); } void TurnOnPump() { digitalWrite(PUMP_PIN, HIGH); Serial.println("Pump on"); } void TurnOffPump() { digitalWrite(PUMP_PIN, LOW); Serial.println("Pump off"); } // Blink the LED void BlinkLED(int i) { trellis.clrLED(i); trellis.writeDisplay(); delay(blinkLEDdelay); trellis.setLED(i); trellis.writeDisplay(); delay(blinkLEDdelay); trellis.clrLED(i); trellis.writeDisplay(); delay(blinkLEDdelay); trellis.setLED(i); trellis.writeDisplay(); delay(blinkLEDdelay); trellis.clrLED(i); trellis.writeDisplay(); delay(blinkLEDdelay); trellis.setLED(i); trellis.writeDisplay(); delay(blinkLEDdelay); } void OpenAllValves() { Serial.println("Open all"); for(int i = 0; i < 4; i++){ OpenValve(i); } }
]]>
Overview
Turning on the device.
Sitting to standing position wearing the device.
An overview of the device.
Close up shot of the device; a white LED in a triangular form.
Close up shot of the battery pack; designed to hold a 9 volt battery.
The device being worn above the knee.
Process
Decision Points:
One decision point that occurred fairly early on was the decision to use a rechargeable 9 volt battery not contained within the device itself. In the initial ideation stages, I had planned to use two tiny button batteries in series. I wanted everything to fit inside the main body of the device, which I later realized was quite difficult to achieve. I then made the decision to switch to a rechargeable 9 volt battery with an external battery pack to allot more space, and to avoid burning through too many batteries over extended wear.
An early battery pack prototype. The 3D print was uneven due to printing multiple parts together once.
Another decision point (also related to the size of the device) was the decision to remake the top half of the device, due to it not being able to fit the electronic components. The original design had the acrylic circle inset into the triangular form. However, the height was too short to fit the electronics, so it was remade so that the circle sat on top. Even though the extruded circle gives the impression of a circle, which is not the intended interaction, it was more important to fit the electronic components.
Original top half of the device with the cutout as an inset.
Process Highlights:
Initial sketchbook drawing and planning.
Soldering the tiny circuit.
Applying Bondo and sanding the 3D prints.
Piecing together the case and the electronics.
Discussion
Responses:
“It’s a shame that there aren’t smaller batteries to go with it, or it could be even smaller.”
I addressed this for the most part in the decision points of the process section. I definitely agree that it would be more elegant if the design did not include a battery pack and if the device itself was smaller. I think the main benefit of having a battery pack is that the battery can easily be changed when it dies. Since the main device is sealed shut, I would have had to integrate some sort of access point to change the batteries had they been within the device itself; which would have been difficult due to the lack of space and the cluttered electronic components inside.
“Clearly a lot of thought was put into figuring out how the components all fit together functionally and aesthetically.”
I think the plan since the beginning was to make the circuit as small as possible, make the case as spacious as possible (without being obtrusive), and smashing the two together. Soldering all the components to a tiny piece of protoboard was definitely straining. I did have to make an adjustment to the top half of the case when the acrylic indent got in the way. I also ran into some problems when the connections broke because I shoved the circuit too hard. Overall, I think I got lucky that everything fit how I imagined for the most part.
———-
Overall, I am satisfied with the way the project turned out. In the ideation stages, I felt like I underestimated the difficulty of the coding and electronics aspect. I ended up having to spend a lot of time on the two, especially in getting them to synchronize. I feel like I accomplished a lot in terms of the technical aspects of the project because I had very little prior experience with electronics and physical computing in general. I think the aesthetics of the project are below my usual standards, but I am happy with the end result given the time restrictions. With purely design projects, I am able to dedicate 100% of the time to aesthetics and UI, while I had to allocate time to technical aspects in realizing this project. Although I wish I had the time to polish up its outer appearance, I am happy about the opportunity to combine technical and design skills.
Through all this, I delved into some things that I never thought I would. First, I learned not to underestimate the size of electronic components. On the bright side, I was able to learn how to program an ATTINY85. Another thing I learned was how to problem solve. Uniquely in terms of technical problems, I was never aware of how many useful resources can be found online through a simple Google search. I was able to find solutions to many of my hardware and coding problems online. If I were to go through this experience again, I would definitely lower my expectations for how fast things progress. With the type of work I normally do in design, I never really get stuck past the ideation phase; so I found myself getting really frustrated when the code was not working the way I intended, or the circuit was not wired up correctly. I expected everything to go smoothly so this caused me a lot of anxiety. I wish I would have anticipated this and mentally prepared myself for all the mishaps. On the topic of future work, I would be interested in building another iteration of this device. I would like to explore higher quality materials, such as silicone or leather straps. Even though I have already dedicated a lot of effort to the aesthetic qualities of the project, I would be interested in reworking it to the point where it can be useful to me as a showpiece. I would also consider adding a programmable element for the sitting time.
Schematic
Code
/* Get-Up Reminder for Thrombosis Patients * * Description: * The code below takes the input from an accelerometer * to detect whether or not the wearer is sitting or * standing, and vibrates a motor and blinks an LED in * a predetermined pattern to remind them to stand up * if they have been sitting for too long. * * Inputs: * ATTINY85 pin | input * 1 switch * A2 accelerometer (z) * * Outputs: * ATTINY85 pin | output * 0 vibration motor * 3 LED * * Credits: * Programming the ATTINY85: sparkfun tutorial by JIMBLOM * https://learn.sparkfun.com/tutorials/tiny-avr-programmer-hookup-guide/programming-in-arduino * Programming outputs for pancake vibration motor: youtube video by Electronic Clinic * https://www.youtube.com/watch?v=y-Fgm4yYsqg * Debouncing switch inputs: course page tutorial * https://courses.ideate.cmu.edu/60-223/f2019/tutorials/debouncing */ const int BUTTON_PIN = 1; const int MOTOR_PIN = 0; const int Z_PIN = A2; const int LED_PIN = 3; int buttonState; // the current reading from the button input pin int lastButtonState = LOW; // the previous reading from the button input pin bool powerState = false; // is the device on or off unsigned long lastDebounceTime = 0; // the last time the output pin was toggled unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers unsigned long startTime = millis(); // the time at which the user began sitting unsigned long timeElapsed = 0; // the amount of time the user has been sitting void setup() { pinMode(BUTTON_PIN, INPUT); pinMode(Z_PIN, INPUT); pinMode(MOTOR_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT); } void loop() { int reading = digitalRead(BUTTON_PIN); // the state of the button switch int zRead = analogRead(Z_PIN); // the reading from the z input pin of the accelerometer if (reading != lastButtonState) { // if the switch changed during single press instance lastDebounceTime = millis(); // reset debouncing timer } // whatever the reading is at, it's been there for longer than the debounce delay, // so take it as the actual current state: if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { // if the button state has changed buttonState = reading; if (buttonState == HIGH) { // only toggle the on/off state if the new button state is HIGH powerState = !powerState; startTime = millis(); // reset the time the user began sitting to the current time } } } if (powerState) { // if the device is "on": digitalWrite(LED_PIN, HIGH); // turn on the LED if (zRead > 350) { // if the accelerometer indicates that the user is sitting timeElapsed = millis() - startTime; // start counting the amount of time the user has been sitting } else { // if the accelerometer indicates that the user is standing startTime = millis(); // reset the time the user began sitting to the current time } if (timeElapsed > 3600000) { // if the user has been sitting for more than 1 hour // if (timeElapsed > 10000) { // demo case: the user has been sitting for 10 seconds // vibrate and blink LED 3 times: analogWrite(MOTOR_PIN, 255); delay(500); analogWrite(MOTOR_PIN, 0); delay(500); analogWrite(MOTOR_PIN, 255); delay(500); analogWrite(MOTOR_PIN, 0); delay(500); analogWrite(MOTOR_PIN, 255); delay(500); analogWrite(MOTOR_PIN, 0); delay(500); startTime = millis(); // reset the time the user began sitting to the current time } } else { // if the device is "off" digitalWrite(LED_PIN, LOW); // turn the LED off } lastButtonState = reading; // save the reading as lastButtonState for the next loop }
]]>
It constitutes a mechanical flower, opening and closing in response to the degree of humidity in the atmosphere, notifying the patients when they have to take antihistamine and how much.
The project applied for me a creative platform to experiment with new things such as organic design, construction and engineering concepts for organic design, 3d printing, improvising with new electronics and libraries, as also combining them with components we had already learned in class such as ‘maintained’ switches, potentiometers as threshold, and LCD screens.
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
petals_construction details_digital model
petals_construction details_physical model
stepper motor_axis rotation_back and forth
axis_bidirectional rotation_strings wrapping_ pull & push petals
1) humidity sensor__humidity & temperature index, 2) ‘maintained switch’__on/off according to being/not being sick__ pill increase/decrease, 3) analog potentiometer__patient’s tolerance to humidity__increases/decreases according to sickness mode, 4) LED on/off__open/close, 5) A4988 chip__stepper motor
humidity>tolerance_petals open_1 pill
humidity>tolerance_sickness mode is on_petals open_2 pill (double dose)
fast speed_emergent distortion_petals overlap
total view
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
Soon I discovered that the most valuable lessons for me were the followings:
I really liked the idea to perceive the electronics as an integral part of the flower’s physical design. That was a good motivation for me to start soldering the components on thin boards and attach them onto the parts of the final construction. I separated the components into two categories, those that should be directly exposed because they cause physical interaction (humidity sensor, switch) and those which needed more protection (Arduino, LED, LCD etc.). On these grounds, I placed them either on the upper levels of the construction or deeper in that.
I totally agree that the mechanism needs some slight modifications in order to become more reliable. I should reconsider the starting position (close or open) of the flower, the degree and the velocity of the rotation, as well as the length of the strings and their even wrapping. However, I disagree with the part about the device’s size. For the petals to be 3d printed properly, without breaking and with preserving harmonic proportions between length and thickness, I firmly believe that this is a sufficient size.
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
Self critic:
During this project, I realized 1) my affinity for exploring new components, figuring out how to make them work and interact with each other, exchanging values. 2) I also experienced a confusion whenever a ‘bug’ appeared. That made me consider to create a better strategy for ‘debugging’. I should create a list of checks for inspecting bugs and excluding potential reasons for errors.
In general lines, I am quite satisfied with the project’s outcome. However, next time I will spend more time in organizing a detailed engineering strategy, to decrease the amount of possible ‘motion errors’ arising in kinetic devices like this. Furthermore, I will experiment more with the mechanical motion in situ in order to calibrate it better and adjust it to the given circumstances. Finally, in my next projects I will try to integrate the electronics into the physical model from the early beginning, either fully designating them or hiding them.
Next steps:
In the future, I would like to iterate the same concept, but instead of creating mechanical motion for opening and closing the flower’s petals, I will produce shape transformation using morphing materials. On the grounds, humidity will be turned into energy (heat) and energy into physical transformation (motion). Finally, this time the design would constitute an indoor ornament, that wirelessly receives the humidity and temperature information from the outdoor environment.
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
schematic diagram_circuits
/* Project title: Water pilly Intro to Physical Computing @ Carnegie Mellon University Made by Maria Vlachostergiou Inputs: Arduino pin | Input 2 Humidity sensor (DHT22) A0 Potentiometer 13 Switch Outputs: Arduino pin | Output 7 LED 4 Motor step 3 Motor direction Step1: We measure humidity degree, the state of potentiometer and switch. Step 2: According to four different conditions, the stepper motor rotates in two different directions, back and forth. Step 3: The LCD screen shows how many pills the patient has to take. */ // humidity sensor #include "DHT.h" #define DHTPIN 2 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); float humidity; float temperature; float fahrenheit; // Stepper motor #include <AccelStepper.h> #define STEP_PIN 4 #define DIRECTION_PIN 3 AccelStepper flowerActivity(1, STEP_PIN, DIRECTION_PIN); // it conducts 200 steps per revolution float turns = 1.1; int initialPos = 200 * turns; // the motor starts at 0 // maintained button for illness #define SWITCH_PIN 13 int switchMode; int isSick = false; int amountOfPills = 0; // potentiometer that evaluates the tolerance toward humidity /* when being sick, a lower degree of humidity might cause // more regular and intenser asthma episodes*/ #define POT_PIN A0 int tolerance; // LED, it turns on when himidity is higher than patient's tolerance #define LED_PIN 7 // I²C LCD, prints out the amount of pills the patient has to take #include <Wire.h> #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C myLCD(0x27, 20, 4); /*********************************************************************************/ void setup() { // pinModes pinMode(SWITCH_PIN, INPUT); pinMode(POT_PIN, INPUT); pinMode(LED_PIN, OUTPUT); // initialize humidity sensor dht.begin(); // Serial.begin(9600); // initialize stepper motor flowerActivity.setMaxSpeed(400); flowerActivity.setAcceleration(50); flowerActivity.moveTo(initialPos); //initialize LCD myLCD.init(); } // void setup /********************************************************************************/ void loop() { // delay the reading of sensors //delay(500); // motor starts flowerActivity.run(); // Read potentiometer and fix a tolerance int t = analogRead(POT_PIN); tolerance = map(t, 0, 1023, 0, 100); // Read humidity sensor, compute heatindex and serially print the necessary data humidity = dht.readHumidity(); temperature = dht.readTemperature(); fahrenheit = dht.readTemperature(true); // in case that the humidity sensor fails to read if (isnan(humidity) || isnan(temperature) || isnan(fahrenheit)) { Serial.println("Sorry, something is wrong! Failed to read from sensor!"); return; } float hif = dht.computeHeatIndex(fahrenheit, humidity); float hic = dht.computeHeatIndex(temperature, humidity, false); Serial.print("Humidity is: "); Serial.print(humidity); Serial.print(" %,\t"); Serial.print("Temperature is: "); Serial.print(temperature); Serial.print(" *C\t"); //Serial.print(fahrenheit); //Serial.print(" *F\t"); //Serial.print("Heat index: "); //Serial.print(hic); //Serial.print(" *C or "); //Serial.print(hif); //Serial.print(" *F,\t"); // Read the switch switchMode = digitalRead(SWITCH_PIN); if (switchMode == 1) { isSick = true; } else { isSick = false; } // Compute and print the amount of Pills, open and close the flower, adjust the LCD Serial.print("Tolerance to humidity: "); Serial.print(tolerance); myLCD.home(); myLCD.print(humidity); myLCD.setCursor(4, 0); myLCD.print("% Humidity!"); myLCD.setCursor(0, 1); myLCD.print(tolerance); myLCD.setCursor(2, 1); myLCD.print("%: Your tolerance"); if ((humidity < tolerance) && (isSick == false)) // condition 1 { flowerActivity.moveTo(initialPos); amountOfPills = 0; digitalWrite(LED_PIN, LOW); myLCD.noBacklight(); myLCD.setCursor(0, 3); myLCD.print("Not being sick"); } // condition 1 else if ((humidity < tolerance) && (isSick == true)) // condition 2 { flowerActivity.moveTo(0); amountOfPills = 1; Serial.println("\tYou are sick. Take 1 pill!"); digitalWrite(LED_PIN, LOW); myLCD.backlight(); myLCD.setCursor(0, 2); myLCD.print("You are sick"); myLCD.setCursor(0, 3); myLCD.print("Take 1 pill!"); } // condition 2 else if ((humidity >= tolerance) && (isSick == false)) // condition 3 { flowerActivity.moveTo(0); amountOfPills = 1; Serial.println("\tHumidity is over your tolerance. Take 1 pill!"); digitalWrite(LED_PIN, HIGH); myLCD.backlight(); myLCD.setCursor(0, 2); myLCD.print("High humidity"); myLCD.setCursor(0, 3); myLCD.print("Take 1 pill!"); } // condition 3 else if ((humidity >= tolerance) && (isSick == true)) // condition 4 { flowerActivity.moveTo(0); amountOfPills = 2; Serial.println("\tBoth sick & Humidity over tolerance. Take 2 pills!"); digitalWrite(LED_PIN, HIGH); myLCD.backlight(); myLCD.setCursor(0, 2); myLCD.print("High humidity & sick"); myLCD.setCursor(0, 3); myLCD.print("Take 2 pill!"); } // condition 4 if (flowerActivity.currentPosition() == initialPos) { Serial.println("\tFlower closed"); } else { Serial.println("\tFlower opened"); } } // void loop /********************************************************************************/
]]>