Final documentation – Intro to Physical Computing: Student Work fall 2019 https://courses.ideate.cmu.edu/60-223/f2019/work Intro to Physical Computing: Student Work Mon, 16 Dec 2019 16:02:49 +0000 en-US hourly 1 https://wordpress.org/?v=5.2.20 The fountain of poof: Team Jim Final Documentation https://courses.ideate.cmu.edu/60-223/f2019/work/team-jim-final-documentation/ Mon, 16 Dec 2019 07:08:24 +0000 https://courses.ideate.cmu.edu/60-223/f2019/work/?p=9078 Introduction

Team Jim consisted of Nicholas, Jeena, Leland, and Jim. We were tasked with helping Jim in his daily life with an electronic device that we could build for him. At first it was hard to find a real problem that we could help fix, but with a little more exploration of his daily life and house, we found that Jim has a piano in his living room that needs to be at certain humidity in order to be maintained.  We combined this with the fact that Jim has a lot of fountains in his house, so we made a fountain that could read the humidity in the living room and humidify the room if needed.

You can see our initial meeting post here and our prototype post here

What we built

We built a fountain that will humidify Jim’s living room based on the humidity level that he wants maintained. The humidity is measured by a remote part that can be placed wherever in the room, and the fountain can then take the data from the remote part and activate the humidifier when needed. The fountain itself has a knob for which Jim can turn to set the humidity level that he wants the room to be at. The purpose of this fountain is to automatically maintain the humidity in the room if he wants to while also serving an indoor fountain that supplies the calming sound of flowing water.

 

Narrative Sketch:

Its the time of the year again where Jim knows he needs to bring in an external humidifier to maintain the humidity for his piano. However, he remembers that a couple of CMU students built him a fountain to monitor the humidity and humidify the room. Jim then plugs in the fountain if it isn’t already, then he sits back in a chair in his living room watching the fountain spewing a satisfying mist of vaporized water.

A photo of a working running second version of the final project. The LEDs are not on because the remote part was not turned on during the photo shoot session, but everything's working, including the pump, LED strip, humidifier, low-voltage LED indicator and the humidifier set point potentiometer.

A photo of a working running second version of the final project. The LEDs are not on because the remote part was not turned on during the photo shoot session, but everything’s working, including the pump, LED strip, humidifier, low-voltage LED indicator and the humidifier and set point potentiometer.

First iteration
The main fountain part of the first iteration.

The main fountain part of the first iteration.

The first iteration of the main fountain part has a round base that traps the mist underneath it. In the gif, you can see the mist only as you lift up the top part.

The first iteration of the main fountain part has a round base that traps the mist underneath it. In the gif, you can see the mist only as you lift up the top part.

Second iteration
A second iteration of our final project. Here you can see a new 'stage case' design of the top part where the water can create an ambient water sound while flowing down. The humidifier is exposed in the air such that it can directly humidify the room (as opposed to being underneath the top part in our first iteration and not able to humidify the environment).

A second iteration of our final project. Here you can see a new ‘stage case’ design of the top part where the water can create an ambient water sound while flowing down. The humidifier is exposed in the air such that it can directly humidify the room (as opposed to being underneath the top part in our first iteration and not able to humidify the environment).

Turning the set point potentiometer allows the user to adjust the threshold that determines when the humidifier gets turned on. When the humidity is slightly above the threshold (within 10%), the LED strip shows a rainbow color. When the humidity is above the threshold + 10%, the LED becomes blue. The LED shows full red color when the humidity is below the threshold and the humidifier gets turned on.

Turning the set point potentiometer allows the user to adjust the threshold that determines when the humidifier gets turned on. When the humidity is slightly above the threshold (within 10%), the LED strip shows a rainbow color. When the humidity is above the threshold + 10%, the LED strip becomes blue. The LED strip shows full red color when the humidity is below the threshold and the humidifier gets turned on.

Detail of the nozzle of the second iteration of the final project. This is where the water comes out directly from the pump. This nozzle creates a height such that the water creates an ambient water sound.

Detail of the nozzle of the second iteration of the final project. This is where the water comes out directly from the pump. This nozzle creates a height such that the water creates an ambient water sound.

A top-down view of the fountain main part. The humidifier lays among the rocks.

A top-down view of the fountain main part. The humidifier lays among the rocks.

The main control panel of the second iteration of the main fountain part.

The main control panel for the second iteration of the main fountain circuit box. The potentiometer has a nice numerical label that shows the set point for the humidity. The switch has on/off indication.

The second iteration of the main circuit box needs 'only' 2 power supplies. The power supply holes are labeled with permanent marker.

The second iteration of the main circuit box needs ‘only’ 2 power supplies. The power supply holes are labeled with permanent marker.

The look of the second remote box. It has an embedded radio sender (inside the box) and a humidity detector (where the white part is).

The look of the second remote box. It has an embedded radio sender (inside the box) and a humidity detector (where the white part is).

How we got here

One thing our group seemed to be very good at was making things difficult for ourselves. From the beginning, the journey from conception to delivery of this fountain was plagued by a variety of setbacks. We had left our initial meeting with Jim, quite convinced that we would be solving one of the two largely mechanical problems he had lain out for us as clear difficulties in his life. Upon further inspection (and prompting from Zach), we reached the conclusion that perhaps we hadn’t though broadly enough about the problem statement and possibilities for this project.
As our team has done time and again, we decided to revise our approach and try again, returning to gym with fewer expectations and only a vague goal of getting to know his and his wife’s lifestyles better rather than find a specific problem to solve right off of the bat. What we learned from this approach turned out be far more stimulating for the ideation process than our prior meeting. We left knowing that Jim and his wife both enjoy music (their Boston grand-piano being a centerpiece of their living room) as well as the ambient sounds of running water. And so we resolved to create a fountain that would provide a pleasant trickle as well as automatically regulate the humidity in the room to maintain the health of their piano.

The shelf were we intended to place the fountain in Jim’s house..

First iteration
Getting the materials for the first version of the main fountain part from Home Depot.

Getting the materials for the first version of the main fountain part from Home Depot.

The first version of the main fountain part.

The materials for the first version of the main fountain part.

The design of the first iteration top part.

The design of the first iteration top part.

The laser-cut side panel for power supplies for the first iteration final project. The holes were cut manually without drill bits. side

The laser-cut side panel for power supplies for the first iteration final project. The holes were cut manually without drill bits.

The main fountain part circuit box of the first iteration. It has 4 power supplies.

The main fountain part circuit box of the first iteration. It has 4 power supplies.

From this point onward, we were sure we knew what our biggest challenges were. Though there would have to be some technical/electrical work behind our design, the most daunting task would surely be engineering and integrating the various physical/mechanical elements of our design. How would we print or manufacture the curvature of a custom bowl? How could we keep things waterproof? And what kind of pump fit our use case?

A successful prototype critique brought with it a renewed confidence that we were on schedule.

Despite a slightly shortened timeline, the prototype critique came and went about as successfully as we could have asked. Our core hardware seemed to work as intended (at least when lain out on a breadboard) we had planned a tight schedule that emphasized starting our manufacturing and building as soon as possible. With the awkward timing of thanksgiving some delays did start to pile out. We didn’t order new pump or humidifier as early as we had intended and the vacuum forming needed to create the base and top basin for our fountain couldn’t be completed until after the break or the Sunday before the project was due.

These delays were result of a variety of factors. Not least of which was the fact that in many ways were in a bit in over our heads and unprepared for the amount of work still left to do to produce a finished product. So, when the eve of the final critique came around, it was all hands on deck. We first had to redesign and adapt to the fact that our original base design could not work. The vacuum former simple hadn’t provided the necessary minimum of four inches of depth to allow it to work, so we improvised by purchasing a base and using our original base as our new basin.
By the time we finished our “final” product, it was nearly seven in the morning, but we felt confident that we had been successful in our effort—event if we had required attaching four separate power sources to our base due to voltage supply and current draw issues we had been unable to solve earlier in the week.
When we arrived for our final critique, we confidently plugged our fountain in. To our surprise, it seemed to be a definitive success with a better stream of water than we had seen the night before and at least some humidity streaming out from the bottom. Of course, this was promptly followed by a smoking power-supply, a quick pop, and all of our electronics dropping immediately dead.

Second iteration
Remote part final box

Remote part final box

Nick was cutting the foam for vacuum forming the second iteration of the main fountain part.

Nick was cutting the foam for vacuum forming the second iteration of the main fountain part.

The main control panel for the second iteration of the main fountain circuit box. The potentiometer has a nice numerical label that shows the set point for the humidity. The switch has on/off indication.

The main control panel for the second iteration of the main fountain circuit box. The potentiometer has a nice numerical label that shows the set point for the humidity. The switch has on/off indication. The middle hole is for the low-voltage LED indicator that lights up when the remote box’s batteries are low.

For the second version, we made a mistake of soldering an Arduino nano directly onto our main circuit board. The nano stopped working and then we had to brutally destroy the entire Arduino nano with a heat gun and remove it from the board. In the photo, you can see the cut off pins that were connected to the nano.

For the second version, we made a mistake of soldering an Arduino nano directly onto our main circuit board. The nano stopped working and then we had to brutally destroy the entire Arduino nano with a heat gun and remove it from the board. In the photo, you can see the cut off pins that were connected to the nano.

Assembling the circuit box for the second version of the final project that needs only two power supplies. All internal connections are labelled.

Assembling the circuit box for the second version of the final project that needs only two power supplies. All internal connections are labelled.

This is the head of a beheaded Arduino Nano. We soldered an arduino nano onto the second iteration of the main circuit box and ended up having to destroy it because it stopped working. Then, for the second iteration.

This is the head of a beheaded Arduino Nano. We soldered an arduino nano onto the second iteration of the main circuit box and ended up having to destroy it because it stopped working. Then, for the second iteration we switched to an Arduino Uno as we did for the first version of the final project.

The second iteration of the main circuit box needs 'only' 2 power supplies. The power supply holes are labeled with permanent marker.

The second iteration of the main circuit box needs ‘only’ 2 power supplies. The power supply holes are

In hindsight, this sort of outcome could have been at least expected if not anticipated. The continuous last-minute scrambles we seemed to create for ourselves surely bred minor mistakes or oversights that culminated at the worse possible moment.
In the end, with the combined efforts of our team, the understanding of Jim, and the grace of a certain instructor, we were given further time to rethink elements of our fountain and come together to produce a final product. Major steps included reforming our basin, replacing the pump, and re-soldering our circuit inside a new box with only two power outlets (thanks to a new 12v 5A power supply). With all of this, the new fountain is actually able to humidify a room properly, regulate its own water level passively, and actively change its LEDs in response to the measured humidity of the room.

Conclusions and lessons learned:

There was a lot to be learned from this project. As a group, we spent a lot of time learning from our mistakes and shortcomings and most importantly trying again and again when certain things didn’t go our way. At these times, what was often most helpful was taking a second to reconsider our design and consult the right people before we decided a specific course of action. For example, when we decided to redo the top basin of our fountain using a vacuum former once again, it was Zach who not only suggested we try using the better quality architecture vacuum former, but who also put Nick in contact with someone who could help with the actual production when he couldn’t find a contact of his own.

Despite the clear failure of our final critique, we received a lot of helpful information to guide us as we worked to revise our fountain towards its “real” final iteration. Some of the most useful were to actually order and external power supply with the specs we needed, as well as position our humidifier on the top layer of the fountain rather than the bottom. In some ways these should have been common sense, but after all of the rushing we had done to finish our work, we first needed to hear the ideas suggested by others to actually implement them ourselves.

In this end, this was not an ideal project. We should have been finished with certain things earlier and should have planned for the delays we had later. Through our trial and error, we learned about the process of asking the right questions and not settling for the obvious solution or project idea. We learned pumps are temperamental and that integrating hardware requires planning ahead. Moreover, we found that simple solutions can exist if we are open to them. If we had considered sooner our power requirements, we would have realized that we could accomplish what we needed with no more than two cables, if not one. Most importantly, we attempted something we weren’t initially sure of or comfortable with and put our best effort into learning and creating our vision as we progressed—and there’s got to be some value in that.

Of course, even with hectic nature of our design process, working with Jim was always a pleasure. Besides being a clearly brilliant and impressive human being, father, grandfather, and husband, he was always more than willing to be involved in the project with us. As we struggled during our initial ideation, he and his wife helped a lot by continually pressing the conversation even when it seemed to reach a lull—suggesting new ideas and probing our thoughts as we worked together on a solution. After our prototype, we spent nearly an hour considering the different features that could be considered for a fountain of this time. Everything from the visual design and form (we had initially considered modeling something after Falling Water) to the stats we could provide in terms of estimated humidification time (something we would have had to derive using the diffusion equation).

Though we didn’t spend much time with him, working with Jim was certainly the of this experience. He was never short or impatient—always friendly and intensely curious about not only our ideas and our lives. Although we never expected anything different, we know it couldn’t have been easy dealing with some of our shortcomings and for that we thank and learn from him. He gave us a chance and we were given the opportunity to deliver on something that at times I’m not sure any of us believed that we actually could.

Technical details

Code

Main fountain part code

#include <AutoPID.h>
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <PololuLedStrip.h>

RF24 radio(9, 10); // CE, CSN

// Create an ledStrip object and specify the pin it will use.
PololuLedStrip<4> ledStrip;

// Create a buffer for holding the colors (3 bytes per color).
#define LED_COUNT 30
rgb_color colors[LED_COUNT];
rgb_color reds[LED_COUNT];
rgb_color blues[LED_COUNT];

// LED DEFINITIONS
const int HumOutputPin = 3; // humidifier control pin

int HUMIDITY_SETPOINT = 0;
int RangeLow = HUMIDITY_SETPOINT-10;   // humidity low range
int RangeHigh = HUMIDITY_SETPOINT+10;  // humidity high range

//PIN DEFINITIONS
const int SETPOINT_POT = A0;
const int LOW_VOLTAGE_PIN = 5;

//Radio Defintions:
const byte address[6] = "00001";
const unsigned int RADIO_UPDATE = 1000; //Update period in ms

//Controller Definitions
const double KP = .12;
const double KI = .0003;
const double KD = 0.0; //No Derivative Control Needed

const unsigned long HUMIDITY_READ_DELAY = 30000; //Every ~30s humidity is read

const double OUTPUT_MIN = 60; //Minimium output speed is every 60 min
const double OUTPUT_MAX = 1; //Maximum output speed is every 1 min

//Controller Global Variables
int motor_val = 0;
double setPoint, outputTiming; 
unsigned long lastUpdate; //Time of last PID updated

double humidity = 0.0;

int transmit_state = 0;

int setpoint = 0;

int humidity_timing = 60; //Time in seconds between puffs
const char Lchar = 'L';
const char Vchar = 'V';
const char Rchar = 'R';

AutoPID controller(&humidity, &setPoint, &outputTiming, OUTPUT_MIN, OUTPUT_MAX, KP, KI, KD);

void setup() {
  //Pin Setup
  pinMode(SETPOINT_POT, INPUT);
  pinMode(LOW_VOLTAGE_PIN, OUTPUT);
  digitalWrite(LOW_VOLTAGE_PIN, HIGH);

  //LED SETUP
  pinMode(HumOutputPin, OUTPUT);
  
  Serial.begin(9600);

  //Radio Setup
  radio.begin();
  radio.openReadingPipe(0, address);   //Setting the address at which we will receive the data
  radio.setPALevel(RF24_PA_MIN);       //You can set this as minimum or maximum depending on the distance between the transmitter and receiver.
  radio.startListening();              //This sets the module as receiver

  //Humidifier Setup
  setpoint = digitalRead(SETPOINT_POT);
  char text[32] = "";
  
  //Controller Setup
  controller.setBangBang(10); //Possible range is +- 10%
  controller.setTimeStep(4000); //Update PID every 4 secs
  delay(1000);
  digitalWrite(LOW_VOLTAGE_PIN, LOW);
}

void loop() {

  if(humidity < RangeLow) {
    humidifierWrite(true);
  } else {
    humidifierWrite(false);
  }

  // set led colors based on humidity
  updateLED(humidity);
  
  HUMIDITY_SETPOINT = analogRead(SETPOINT_POT);
  HUMIDITY_SETPOINT = map(HUMIDITY_SETPOINT,0,1023,0,100);
  RangeLow = HUMIDITY_SETPOINT - 10;
  RangeHigh = HUMIDITY_SETPOINT + 10;

  //Radio Code
  char text[32] = "";                 //Saving the incoming data
  double tempVal;

  if (radio.available()) {
    radio.read(&text, sizeof(text));    //Reading the data
    radio.read(&tempVal, sizeof(humidity));    //Reading the data
 
    if(text[0] == Rchar) {
      // Read failure
      Serial.println("Read Failure");
    } else if(text[0] == Lchar){
      // Low voltage
      //Don't update the humidity;
      digitalWrite(LOW_VOLTAGE_PIN, HIGH);
      Serial.print("Turn on LED on PIN ");
      Serial.println(LOW_VOLTAGE_PIN);
    } else if (text[0] == Vchar) {
      // Voltage fine
      digitalWrite(LOW_VOLTAGE_PIN, LOW);
    }else{
      humidity = tempVal;
    }
    Serial.println(text);
    Serial.println(humidity);
  }
  //Updating PID
  setPoint = analogRead(SETPOINT_POT);
  controller.run();

  humidify(outputTiming);

  delay(500);
} 

bool checkRadio(char *text, double *humidity) {
  static unsigned long lastUpdate = millis(); //Only happens onces
  if(lastUpdate > millis()) lastUpdate = millis(); //for overflow condition
  if(millis() - lastUpdate > RADIO_UPDATE) {
    if(radio.available()) {
      radio.read(text, sizeof(*text));
      radio.read(humidity, sizeof(*humidity));
      if(text[0] == Rchar) {
        Serial.println("Read Failure");  
      } else {
        Serial.println(*text);
        Serial.println(*humidity);
      }
    }
    return true;
  } else {
    return false;
  }
}

// Converts a color from HSV to RGB.
// h is hue, as a number between 0 and 360.
// s is the saturation, as a number between 0 and 255.
// v is the value, as a number between 0 and 255.
rgb_color hsvToRgb(uint16_t h, uint8_t s, uint8_t v)
{
  uint8_t f = (h % 60) * 255 / 60;
  uint8_t p = (255 - s) * (uint16_t)v / 255;
  uint8_t q = (255 - f * (uint16_t)s / 255) * (uint16_t)v / 255;
  uint8_t t = (255 - (255 - f) * (uint16_t)s / 255) * (uint16_t)v / 255;
  uint8_t r = 0, g = 0, b = 0;
  switch((h / 60) % 6){
    case 0: r = v; g = t; b = p; break;
    case 1: r = q; g = v; b = p; break;
    case 2: r = p; g = v; b = t; break;
    case 3: r = p; g = q; b = v; break;
    case 4: r = t; g = p; b = v; break;
    case 5: r = v; g = p; b = q; break;
  }
  return rgb_color(r, g, b);
}

void humidifierWrite(bool state) {
  if(state) {
    digitalWrite(HumOutputPin, HIGH);
  } else {
    digitalWrite(HumOutputPin, LOW);
  }
}

void updateLED(int humidity) {
  uint16_t time = millis() >> 2;
  if(!humidity) return;
  if(humidity < RangeLow) {
    // update all colors to red
    for(uint16_t i = 0; i < LED_COUNT; i++) {
      colors[i] = rgb_color(255, 0, 0);
    }
  } else if(humidity > RangeHigh) {
    // update all colors to blue
    for(uint16_t i = 0; i < LED_COUNT; i++) {
      colors[i] = rgb_color(0, 0, 255);
    }
  } else {
    for(uint16_t i = 0; i < LED_COUNT; i++) {
      byte x = (time >> 2) - (i << 3);
      colors[i] = hsvToRgb((uint32_t)x * 359 / 256, 255, 255);
    }
  }
 
  // Write the colors to the LED strip.
  ledStrip.write(colors, LED_COUNT);

  delay(10);
}

Remote part code

#include <SPI.h>
#include <nRF24L01.h>
#include "cactus_io_AM2302.h"
#include <RF24.h>
#include "LowPower.h"

const int VOLTAGE_CHECK_THRESHOLD = 30000;
const int RADIO_THRESHOLD = 50;
const int LOW_VOLTAGE_THRESHOLD = 650; //Corresponds to just around 5.5V
const int HUMIDITY_PIN = 4;
const int VOLTAGE_PIN = A1; //Measuring low voltage state
AM2302 dht(HUMIDITY_PIN);

RF24 radio(9, 10); // CE, CSN         

const byte address[6] = "00001";     //Byte of array representing the address. This is the address where we will send the data. This should be same on the receiving side.
int transmit_state;

long unsigned int last_voltage_check = 0;
long unsigned int last_transmission = 0;

void setup() {
  pinMode(HUMIDITY_PIN, INPUT);
  pinMode(VOLTAGE_PIN, INPUT);
  radio.begin();                  //Starting the Wireless communication
  radio.openWritingPipe(address); //Setting the address where we will send the data
  radio.setPALevel(RF24_PA_MIN);  //You can set it as minimum or maximum depending on the distance between the transmitter and receiver.
  radio.stopListening();          //This sets the module as transmitter
  Serial.begin(9600);
  dht.begin();
}

void loop() {
  if(millis() < last_voltage_check) last_voltage_check = millis(); //Overflow condition
  if(millis() < last_transmission) last_transmission = millis();


  if(millis()-last_voltage_check > VOLTAGE_CHECK_THRESHOLD) { //Low voltage handling condition
    last_voltage_check = millis();
    if(analogRead(VOLTAGE_PIN) < LOW_VOLTAGE_THRESHOLD) { //Voltage is less than 5v 
      
      const char low_voltage_text[32] = "Low Voltage"; 
      double dummy = analogRead(VOLTAGE_PIN);
      radio.write(&low_voltage_text, sizeof(low_voltage_text));
      radio.write(&dummy, sizeof(dummy));
      Serial.println("Voltage LOW: ");
      Serial.println(analogRead(VOLTAGE_PIN));
      
    } else {
        const char ok_voltage_text[32] = "Voltage Fine";
        double dummy = (double)analogRead(VOLTAGE_PIN);
        radio.write(&ok_voltage_text, sizeof(ok_voltage_text));
        radio.write(&dummy, sizeof(dummy));
        Serial.println("Voltage ok");
        Serial.println(analogRead(VOLTAGE_PIN));
    }
    delay(4000);
  } 

  if(millis()-last_transmission > RADIO_THRESHOLD) {
    last_transmission = millis();
    dht.readHumidity();
    dht.readTemperature();
    const char success_text[32] = "Transmitting: ";
    const char failure_text[32] = "Read Failure";
    bool failure = false;
    if(isnan(dht.humidity) || isnan(dht.temperature_C)) {
      failure = true;
    }
  
    double humidity = dht.humidity;

    Serial.println(humidity);
  
    transmit_state = millis();
    if(failure) {
      radio.write(&failure_text, sizeof(failure_text));
    } else  {
      radio.write(&success_text, sizeof(success_text));  
    }
    radio.write(&humidity, sizeof(humidity));  //Sending the message to receiver 
    delay(4000); 
  }
}
Schematic and design files

Main fountain part schematic

Main fountain part schematic

Main fountain module schematic

Remote fountain module schematic

The Schematic of the Remote Transmission Module. Uses a voltage divider to map the input voltage (normally > 5v) to a range that is readable by the Arduino’s ADC through pin A1 that allows it to detect low input voltages.

 

]]>
Trivia Machine by Team Enid: Final Documentation https://courses.ideate.cmu.edu/60-223/f2019/work/trivia-machine-final-documentation/ Sat, 14 Dec 2019 00:28:23 +0000 https://courses.ideate.cmu.edu/60-223/f2019/work/?p=9014
Introduction

Team Members: Neeharika, Mike, Maria, and Enid

Tasked to create a device that could help an older individual, we worked closely with Enid to see what she needed assistance within her daily life. She noted that she often finds the cleaning-up process after a meal to be boring and tedious, and would like a way to make it less so. Keeping in mind that she enjoys books, movies, and puzzles, we decided to combine her interests in creating a Trivia Machine that she can keep in her kitchen and keep her entertained. Since cleaning up can get her hands messy, she can play the game in a hands-free way by waving a utensil in a designated answer slot to lock in her answer. The retro-futuristic design both stands out yet integrates well within the appliances in a common kitchen. The device can be hung in the kitchen off of a cabinet for easy viewing, or can be removed and taken anywhere around the house in a portable fashion. 

You can see our Initial Meeting documentation here and our Prototype Documentation here

 

What We Built

This device displays a question on the top screen and up to four answer choices on the bottom screen, waiting for the user to select an answer. In order to do so, hold a fork, spoon, small plate or even your hand in the answer area you want to select (Red for 1st option, Orange for 2nd option, Yellow for 3rd option and White for 4th option). Once selected, the bottom screen will tell you if you are right or wrong, and what the correct answer is. It then displays a new question/answer set and repeats this process.

Front View, removable panel on the side reveals battery pack

Back View

Side View

Other side view

Shot of the answering portion, you can see the bulbs within each section and the ultra sonic sensor at the top

Easily accessible battery pack, just pop open the panel!

Example of how to answer – LED indicates which section you are in

Placed in the kitchen

Trying it out for the first time, device can also sit on any surface

Video 1: How to use the device

Video 2: How to change the batteries

Narrative Sketch:

Enid and her husband have just finished dinner. By the end of the day, they always end up extremely tired and the boring activity of cleaning the dishes makes their fatigue even worse.  Both enter the kitchen to place the plates into the sink. And there it hangs, the Trivia Machine. Enid turns the switch on and the game starts. Forks,  spoons, plates all are turned into game parts to answer the trivia questions. After half an hour all the plates are clean. After two hours, Enid and her husband go eventually to sleep!

How We Got Here

We started off with many ideas about what to build for Enid but we quickly converged on two contenders – a plant watering/monitoring system and a trivia machine. We decided against the first option because many commercial solutions are already available. A trivia machine would be more personal and allow us to explore and leverage physical interactions between the user and the device in a more interesting way.

Because we wanted the device to be usable while Enid is doing dishes, we began by exploring different hands-free methods of interaction. In terms of delivering questions, we wanted to explore the option of using text-to-speech. However, this would require connecting to the internet for an online API, which we decided was too ambitious for the timeframe. Additionally, Enid did not want yet another internet device in her home. We decided on using two liquid crystal displays for the question and answer options, respectively.

We wanted to integrate the answering process with the dishwashing process. The basic principle was to allow the player to “lock in” on an answer choice via a utensil or plate that is being cleaned. We decided against any method of detecting a “lock” with contact between the potentially dirty dishes and electronic components. We decided to use the distance between an object as input for the device, and tested different ways of measuring proximity. The laser time-of-flight sensor proved to be inaccurate with different ambient light conditions and material, and the ultrasonic ranger had adequate accuracy for our design.

Based on the prototype crit, we scaled down the device and improved the way in which questions are displayed on the LCDs for better viewing.

Scaling down the device involved rethinking the way in which distance sensing was done. Enid was concerned that large objects would accidentally come into sensing range, so we restricted the sensing area to within the dimensions of the device. This meant the the answer choice regions can only accept small utensils or fingers as input. 

The new device would be suspended in front of Enid at a distance that is reachable but not obtrusive

An early design for the scaled-down version. We decided to flatten out the bottom to make the device able to stand freely.

We also wanted to make the device portable, so that Enid can remove the device when she wanted to. This also allowed for other use cases, where the game can be played anywhere with a flat surface. This also means making the device battery powered, and we decided on placing the battery pack on the panel that would be leaning against the wall.

The first printed design contained many dimension errors that were corrected in the final version.

After the first print, we also removed the hanging ledge in the question selection area, added a removable panel for batteries, and added a power switch on the top side. Out of concern that the second print will not be printed in time, we also explored other enclosures that could be laser cut.

A more box-like design that could be created more quickly, but less aesthetically pleasing.

However, we managed to reduce the print time by subdividing the entire body into sections that are later assembled together. We also added LEDs behind the answer selection region to provide better feedback to the user who is selecting the answers.

One of the three segments of the enclosure. Here we are test fitting the LEDs and ultrasonic ranger.

Protoboards inside the enclosure for the final product. The screens are also secured, but can be removed if necessary.

The second major issue raised from the prototype crit was the method in which questions were displayed. Because some questions were too long to be displayed on the LCD, we used a “scrolling” method to display the text, using only one of the 4 lines on the LCD.

The original side scrolling display that was too difficult to read.

Per Enid’s suggestion, the questions that are too long to be displayed in a screen are “scrolled” from top to bottom. Because Enid would not be constantly viewing the screen while cleaning, we looped the scrolling and added a long pause at the beginning of each loop to indicate the beginning of a question. Unfortunately, the answer options that are too long still needed to be scrolled from side to side. We reduced the speed of the scrolling and added a pause at the beginning to reduce the cognitive overhead of figuring out how to read the screen.

The new display method, with the questions filling the full screen and the answers scrolling (if necessary).

After addressing these major issues, we also added over 1700 new questions to the SD card and a strap for hanging the machine on the cabinet walls. Finally, we sealed the segments together for delivery.

The finished assembly before glueing.

Generally, we followed our anticipated/loose schedule. We were briefly concerned about the  3D print time, but we managed to finish assembly on time.

Final Crit

During the final feedback session, we were able to show the final product to Enid as well as getting feedback from our peers and other members of the community. Some good points and suggestions were made:

  • “The sensing action is demanding as it requires a floating hand in space with precision at arm’s length” – This was a common point brought up by many people. While it was easy for us to hold a utensil in the air for a couple seconds, we can see why it may be difficult for an elderly user, especially one that may have Arthritis or Parkinson’s, to do the same motion. This is a critical component of the mechanism of answering, and in our eyes was key to keeping the device clean from anything that may be on the hands or utensil. However, what we could do to remedy this accessibility issue and keep a somewhat clean device is to create small ledges or nubs in the back of the answer space that the user can rest the utensil on, but not let go of, to alleviate some pain. This way, one can still use the utensil to keep answering questions, but if it is getting tiring, they can use the small ledge to relax.
  • “Sensor position below would make it more playable and have stronger ties to dishwashing” – This was another good point brought up. This means that instead of having the answer space to the right of the device and in a vertical orientation, it would be below the device in a horizontal orientation. In this design, it may be easier to bring dishes and forks under to the answer space right out of the sink that in our current design. Actually, this was one of the initial designs we had in mind, but Enid indicated to us that she liked the version we have now better. It’s all up to preferences about the user.

Major takeaways 

This was the first time the members of our team built something for an elderly individual. Before starting this project, we learned a little in class about potential hardships the elderly community faces. It turns out that there are a lot of things that we take for granted as young, abled people that may be difficult for someone else. Thinking about potential projects to pursue required us to put ourselves in another person’s shoes and try to see the world from their perspective, and what they could potentially want in design and utility. Our meetings with Enid proved to be very useful to collect this kind of information. 

On a similar note, we learned how truly meaningful feedback can be. Every meeting with Enid, we were able to present our current stage in development and receive ideas about certain things that should be changed to make the device more effective for her. Usually, during development, we tend to get very wrapped up in the technical aspects. However, the various feedback sessions allowed us to step back and look at the bigger picture, especially from the user’s perspective.

Being part of a team is a lot of work but ultimately a rewarding experience. Clear communication between members and equal participation is critical for a well functioning team. However, no matter how enthusiastic we were to complete this project, we learned that things definitely take a lot more time than what is expected. Certain small tweaks we thought would take a day ended up taking a week. Keeping this in mind while pursuing any project is good preparation for the many hours of work ahead.

Concluding Thoughts

If we were to continue building on this project, we would probably want to integrate some sort of voice capabilities that would rid the need to have to look at a tiny LCD. We could use basic text-to-speech to read both questions and answers to make it easier to still perform tasks in the kitchen. We could also add a score-keeping system too, where a user could set the number of players and the device could keep track of each player’s points to declare a definite winner. Right now, the device has a fixed ~1700 questions in its bank, but if we had internet capabilities, we would be able to dynamically request many, many more – perhaps it would never run out! With this, Enid would be able to select her favorite categories as well, allowing for even more personalization. 

It was an amazing experience to work with Enid to create the Trivia Machine. We were able to dive into many different branches of product development – electronics, software, design, and fabrication – all while receiving valuable feedback along the way. We hope that Enid and her husband enjoy using the Trivia Machine, and hopefully now cleaning up is a little less boring!

Technical Details

Code

/*
 * Trivia Machine Arduino Code
 * Code for the Arduino in the Trivia Machine.
 * The code reads a random question from the SD card,
 * Display the question and answer options on two LCDs,
 * and awaits input from the ultrasonic ranger. After a 
 * question is answered, the screen displays feedback and 
 * resets the Arduino.
 * 
 * Pins:
 *  Ultrasonic Ranger
 *    Echo: D3
 *    Trig: D2
 *    
 *  LEDs
 *    Option A: D7
 *    Option B: D6
 *    Option C: D5
 *    Option D: D4
 *    
 *  LCDs - I2C SDA and SCL
 *  SD Card: 
 *    MOSI -  11
 *    MISO -  12
 *    CLK -   13
 *    CS -    10
 *    
 *  Connect Pin A0 to the Reset pin
 */

#include <SPI.h>
#include <SD.h>
#include <LiquidCrystal_I2C.h>

const int WIDTH = 20;
const int HEIGHT = 4;

LiquidCrystal_I2C lcd0(0x27, WIDTH, HEIGHT);
LiquidCrystal_I2C lcd1(0x23, WIDTH, HEIGHT);

const int TRIGPIN = 2;
const int ECHOPIN = 3;
const int RESET_PIN = A0;


// New Stuff
const int LEDAPIN = 7;
const int LEDBPIN = 6;
const int LEDCPIN = 5;
const int LEDDPIN = 4;

//LCD Display Params
const int ANS_CHANGE_THRESHOLD = 2000;
const int SCREEN_CHANGE_THRESHOLD = 1500;
const int INITIAL_SCREEN_FACTOR = 2;
const int EXTRA_SCROLLS = 2;
const int OPTION_CHANGE_THRESHOLD = 500;
const int NUM_QS = 1703;


File file;

const int arrayLength = 5;
int lines = 0;
String qArray[arrayLength];
int perm[arrayLength - 1];

void setup()
{
  digitalWrite(RESET_PIN, HIGH);
  pinMode(TRIGPIN, OUTPUT);
  pinMode(ECHOPIN, INPUT);
  pinMode(RESET_PIN, OUTPUT);
  // New Stuff
  pinMode(LEDAPIN, OUTPUT);
  pinMode(LEDBPIN, OUTPUT);
  pinMode(LEDCPIN, OUTPUT);
  pinMode(LEDDPIN, OUTPUT);

  digitalWrite(RESET_PIN, HIGH);
  Serial.begin(9600);
  delay(analogRead(A2));
  randomSeed(analogRead(A1)*analogRead(A2)+ analogRead(A3));
  Serial.println();

  lcd0.init();
  lcd0.backlight();
  lcd0.home();
  lcd0.noAutoscroll();

  lcd1.init();
  lcd1.backlight();
  lcd1.home();
  lcd1.noAutoscroll();

  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.print("Initializing SD card...");
  if (!SD.begin(10)) {
    while (1);
  }

  //open the file here
  lines = loadQuestions(String(random(NUM_QS)) + ".TXT", qArray);
  permute(lines - 1, perm);
  lcd0.setCursor(0, 0);
  //scrollQ(qArray[0]);
  displayAns(qArray, perm, lines, 0);
}

void loop() {
  //scroll answers
  static int screenInd = 0;
  static long lastS = 0;
  int numofScreens = INITIAL_SCREEN_FACTOR + qArray[0].length() / (WIDTH) - HEIGHT + EXTRA_SCROLLS;
  if (millis() - lastS > SCREEN_CHANGE_THRESHOLD) {
    lastS = millis();
    if (screenInd == 0 || screenInd > INITIAL_SCREEN_FACTOR) {
      printQScreen(qArray[0], max(screenInd - INITIAL_SCREEN_FACTOR, 0));
    }
    screenInd = (screenInd + 1) % (numofScreens);
  }

  //scroll questions
  static int optOffset = 0;
  static long lastScroll = 0;
  if (millis() - lastScroll > OPTION_CHANGE_THRESHOLD) {
    lastScroll = millis();
    displayAns(qArray, perm, lines, optOffset);
    optOffset++;
  }

  //lock in on choices
  static long lastChange = millis();
  static int lastChoice = 0;
  int dist = getDist();
  int choice = map(dist, 0, 120, 0, 6);
  if (lastChoice != choice) {
    lastChange = millis();
  }

  if (1 <= choice && choice < lines) {
    lcd1.cursor();
    lcd1.blink();
    lcd1.setCursor(19, choice - 1);
    if (millis() - lastChange > ANS_CHANGE_THRESHOLD) {
      feedbackAndReset(perm[choice - 1] == 0, qArray[1]);
    }
  } else {
    lcd1.noCursor();
    lcd1.noBlink();
  }

  lightLEDs(choice);

  lastChoice = choice;
  delay(100);
}


int loadQuestions(String path, String arr[]) {
  file = SD.open(path, FILE_READ);
  int index = 0;
  while (file.available()) {
    //A inconsistent line length may lead to heap memory fragmentation
    arr[index] = file.readStringUntil('\n');
    arr[index].trim();

    index++;
  }
  file.close();
  return index;
}

//Generate random permutation of n numbers
void permute(int n, int perm[]) {
  int ord[n];
  for (int i = 0; i < n; i++) {
    ord[i] = i;
  }
  for (int i = 0; i < n; i++) {
    int ind = random(n - i);
    perm[i] = ord[ind];
    for (int j = ind; j < n - 1; j++) {
      ord[j] = ord[j + 1];
    }
  }
}

//get Distance from ultrasonic
int getDist() {
  digitalWrite(TRIGPIN, LOW);
  delayMicroseconds(5);
  digitalWrite(TRIGPIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIGPIN, LOW);
  long duration = pulseIn(ECHOPIN, HIGH);
  int distance = duration * 0.34 / 2;
  Serial.println(distance);
  return distance;
}

//Display answer choices and scroll
void displayAns(String qArray[], int perm[], int lines, int offset) {
  for (int i = 0; i < lines - 1; i++) {
    lcd1.setCursor(0, i);
    int scrollsNeeded =
      qArray[perm[i] + 1].length() - (WIDTH - 2) + INITIAL_SCREEN_FACTOR + EXTRA_SCROLLS;
    if (scrollsNeeded <= INITIAL_SCREEN_FACTOR + EXTRA_SCROLLS) {
      lcd1.print(qArray[perm[i] + 1]);
    } else {
      int cur_off = offset % (scrollsNeeded) - INITIAL_SCREEN_FACTOR;

      if (-INITIAL_SCREEN_FACTOR < cur_off && cur_off < 1) {
        continue;
      }

      cur_off = max(cur_off, 0);
      lcd1.print("                   ");
      lcd1.setCursor(0, i);
      lcd1.print(qArray[perm[i] + 1].substring(cur_off, cur_off + WIDTH - 2));
    }
  }
}

//Send reset signal
void feedbackAndReset(bool correct, String ans) {
  lcd1.noCursor();
  lcd1.noBlink();
  lcd1.clear();
  lcd1.home();

  if (correct) {
    lcd1.print("Correct!");
  } else {
    lcd1.print("Incorrect.");
    lcd1.setCursor(0, 1);
    lcd1.print("Correct Answer is:");
    lcd1.setCursor(0, 2);
    lcd1.print(ans);
  }

  delay(1000);
  digitalWrite(RESET_PIN, LOW);
}

//Print question based on screen index
void printQScreen(String q, int s) {
  int qLen = q.length();
  int area = WIDTH * HEIGHT;

  if (qLen <= area) {
    for (int i = 0; i < qLen; i++) {
      int x = i % WIDTH;
      int y = i / WIDTH;
      lcd0.setCursor(x, y);
      lcd0.print(q[i]);
    }
    return;
  }

  lcd0.clear();
  for (int i = 0; i < area && ((s * WIDTH) + i) < qLen; i++) {
    int x = i % WIDTH;
    int y = i / WIDTH;
    lcd0.setCursor(x, y);
    lcd0.print(q[(s * WIDTH) + i]);
  }
}

void lightLEDs(int choice) {
  digitalWrite(LEDAPIN, LOW);
  digitalWrite(LEDBPIN, LOW);
  digitalWrite(LEDCPIN, LOW);
  digitalWrite(LEDDPIN, LOW);
  switch (choice)
  {
    case 1:
      digitalWrite(LEDAPIN, HIGH);
      break;
    case 2:
      digitalWrite(LEDBPIN, HIGH);
      break;
    case 3:
      digitalWrite(LEDCPIN, HIGH);
      break;
    case 4:
      digitalWrite(LEDDPIN, HIGH);
      break;
    default:
      break;
  }
}

Schematic diagram of wiring

 

]]>
Exercise Reminder Clock (by Team J.A.N.): Final Documentation https://courses.ideate.cmu.edu/60-223/f2019/work/exercise-reminder-clock-by-team-j-a-n-final-documentation/ Fri, 13 Dec 2019 21:42:27 +0000 https://courses.ideate.cmu.edu/60-223/f2019/work/?p=8951 The Intro:

This Exercise Clock was created as a final project submission for CMU Ideate’s Physical Computing class. Our prompt was to meet with an older person in our community (selected for us from CMU’s OSHER classes) and create an assistive device personalized to them.

Our group was paired with Jan, and through our previous discussions with her, we learned that she enjoyed going on walks, but felt that she wasn’t as active as she should be. Since she didn’t care for the exercise apps she had tried, we created a prototype clock for her to use to track her physical activity.

We then finalized the project with this version, according to Jan’s feedback to the prototype.

The Product:

Our final product is a clock that reminds Jan to exercise and keeps track of how long she exercises. The clock cycles between three screens,  the first displays the time and date, the second displays bar graph of how long Jan exercised each day for the past two weeks, and the third compares the time Jan spent exercising the current week and last week. There is also a row of LEDs that display how many days from the past week that Jan has exercised. If Jan hasn’t met her exercise goal for the day, the clock will beep every hour if it senses someone in the room. When the button is pressed, the screen displays a timer.  When the button is pressed again, the timer is turned off and the time is added to the graph and if the exercise goal is met, one of the LEDs turns on. There is also a mute switch that puts makes the screen dim and makes the clock stop beeping.

Overall Photo:

An overall photo of our final product.

Basic Operation:

Pressing the button to start and stop the timer.

Twisting the button to set the time.

Flipping the mute switch on and off.

Detail Photos:

The display showing the time and date.

The display showing the graph of time spent exercising for the past two weeks.

The display comparing the amount of exercise this week and last week.

The display when the clock is in muted mode.

The LED row with one LED on, and the motion sensor.

The speaker and the off switch.

Usage Photos:

Pressing the button to stop the timer.

Turning the button to set the time and date.

Flipping the switch and putting the clock in muted mode.

The Process:

Following the formative critique, we created a schedule to guide us through the remainder of the project.

After completing our prototype we created this plan to finish the project.

Our first tasks were to complete the functionality of the device, and we were able to stay on schedule while doing so.

We wanted Jan to be able to view her recent workout history from a distance.   Our group decided to use a row of 7 Red-Green LED’s on our reminder clock to display the information in an manner that was easily noticeable but not out of place in her kitchen.  Each Red-Green LED would represent a day in the past week, appearing green if she worked out but red otherwise.  The easiest way to attach these lights would be with one pin per color for every light, which would use 14 pins.   However, due to the limited number of outputs on the Arduino we needed to condense this feature into just 7 pins.  To achieve this goal, we took advantage of each light only needing to be red or green at a certain time and used transistors to select between the two options with a single pin.   We also ran into early problems powering all 7 Red-Green LEDs at once, but using the 5v pin as the main power source for the lights fixed this problem for the time being.

Initial ideas for reducing the number of pins needed for the LEDs.

Our final circuit for the Red-Green LEDs.

Furthermore, Jan mentioned multiple features that she believed would improve her experience with the device.  She worried that the clock would continue to provide alerts while she was out of town with her neighbor looking after the house.  Our group decided to include a mute switch which dimmed the screen and stopped alerts in this situation.  She also expressed multiple concerns about the longevity of the device, so we decided on several changes to improve this area.  We added a more precise time keeping element, so the clock would skew less overtime.  In software, we fixed bugs that would occur when the date had an overflow error.  Also, for any unexpected errors ,as well as daylight savings time and leap years, we added the ability to change the time and date manually.  This feature required a new input to choose between times, so we changed the button on the device into a rotary encoder to increase the functionality while keeping the interface minimal.

The rotary encoder we replaced our button with to enable the time changing functionality.

A test of changes we made to our code to prevent overflow errors.

After completing the functionality of the reminder clock we began to assemble the final product, but the construction took longer then we anticipated.  We originally planned to finish before Thanksgiving break, but our group had to do considerable work after we returned.  This delay was largely due to us underestimating the work needed to finish the case.  When initially designing the outside, we combined math and intuition to create an overall shape.  We wanted the device to be large enough to sit between two wall trims without leaving too much space, and the top to be at an angle that she could easily view while standing.

A sketch used in deciding the angle of the clock’s top.

Our first setback in manufacturing occurred when choosing how to assemble the case.  We originally designed a file to 3d print, but afterwards decided to change to laser engraving due to size constraints on the 3d printer.  When we tried to assemble our final box our group faced multiple other issues.  We decided to use a “5 minute” epoxy to connect the pieces.  However, we did not mix the components correctly, which created a messy layer of residue on our product.  This initial failure made later attempts to glue box less effective, as the original epoxy blocked the new adhesive.  Later, when putting components into the box, we accidentally shattered one of the sides.  These two errors led to us re-engraving the case.  The second time was much neater, as we were more careful and used acrylic glue.

A cardboard prototype of our box used to optimize hole sizes before our final print.

A shattered side of our box after a panel mounting accident.

Unfortunately, when assembling the final circuitry using an Arduino nano we ran into a problem powering all 7 LEDs similar to our earlier issue.  This time, however, our solution of using the 5v pin as the main power source for the lights was not working.  Since we had fallen behind schedule, we did not have time to solve this problem, so we decided to replace the Red-Green LEDs with normal LEDs for our final clock.

The Red-Green LEDs were replaced with simple blue LEDs due to problems when making the final circuitry.

The Feedback:

Our project received mostly positive feedback, though, some valid criticisms and suggestions were raised. One commenter noted, for example, that

“LED lights + day of week on display should go in same direction”

in reference to the fact that our LED display on the front of the device represented the most recent day on the left of the device, while our bargraph on the LCD had  the most recent day on the right. This is an aspect of the device we hadn’t noticed, however changing it did make the different displays of the device more intuitive to comprehend at a glance.

Another comment we received regarded the physical presence of the device:

“Consider making the knob a bit bigger – it might make it easier to use.”

As a group, we also agreed with this criticism, to the extent that we were originally planning to make the knob much larger than it is currently in our final project. While unfortunately we weren’t able to get the custom part created in time, Jan at least seemed pleased with the replacement knob we did end up using.

One of the most important things we learned while creating something for another person, is the importance of listening. Many of the design choices we made to customize this clock for Jan were the result of minor comments she made during the interview process. Things like the size of the device, and our method of interaction we as effective as they could be because we noted specifically how Jan expected to use the device.  If we were to do this project another time, we would most likely pay more attention to the physical for and aesthetics of the device earlier in the design process, to ensure we wouldn’t have another plastic box panic the night before the project was due.

If we were to do this project again, one of the most useful modifications to the design process would be to include some time for “beta testing.” While we were able to demonstrate a prototype of our project to Jan to get some basic feedback, the feedback we got was somewhat limited in that we collected it in a classroom setting. Being able to let Jan take the device home with her for a few days of testing could potentially bring light to some more minor issues which we wouldn’t be able to discover within a classroom environment.

The Details:

The electrical schematic of our project.

The Code:

/*
 * Workout_Clock.ino
 * 
 * Exercise Reminder Clock
 * Karen Abruzzo, Justin Kiefel, George Ralph
 * 
 * This code runs a clock, which can be used to track the duration of
 * exercise intervals with stopwatch-like functionality whenever TIMER_BUT_PIN
 * is pressed. The clock also tracks exercise history over a 2 week period, 
 * and reminds the user through audio tones to exercise whenever they are 
 * detected by the motion sensor. (Though this feature can be disabled)
 * 
 * Inputs
 *    0   -   Motion Sensor
 *    2   -   Rotary Encoder Clock Pin
 *    3   -   Rotary Encoder Data Pin
 *    4   -   Rotary Encoder Button
 *    5   -   Power/Mute Switch
 * 
 * Outputs
 *    6     - Speaker Pin
 *    7..13 - Bargraph LEDs
 *    
 * Outputs
 * 
 * NOTE: This project must be compiled with compiler warnings disabled due to 
 *       aspects of the libraries we use, which are beyond our control.
 */

#include <Encoder.h>
#include <Wire.h>
#include <DS3231M.h> //library for the rtc
#include <LiquidCrystal_I2C.h>
#include "pitches.h"

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

//Make buffer size the size of the screen area, just to be safe
const uint8_t  SPRINTF_BUFFER_SIZE = 80;
char inputBuffer[SPRINTF_BUFFER_SIZE];

const uint32_t SERIAL_SPEED = 115200;
const int MOTION_PIN         = 0;
const int ENC_CLK_PIN        = 2;
const int ENC_DT_PIN         = 3;
const int TIMER_BUT_PIN      = 4;
const int POWER_SWITCH       = 5;
const int SPEAKER_PIN        = 6;
const int LED_BASE_PIN       = 7;

const int LED_CNT = 7;

//Goal time is 40 minutes
const int GOAL_TIME = 60*40;

Encoder enc(ENC_CLK_PIN, ENC_DT_PIN);

const char weekLetters[] = {'S', 'M', 'T', 'W', 'H', 'F', 'S'};
const String weekDays[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};


// 2 = half note, 4 = quarter note, ect.

int openSound[] {2, 8, 8, NOTE_B3, NOTE_B4};
int workoutSound[] {2, 8, 16, NOTE_B3, NOTE_B5};
int clockSound[] {1, 8, NOTE_A5};
int sadSound[] {2, 4, 4, NOTE_F3, NOTE_C3};
int happySound[] {4, 8, 8, 8, 8, NOTE_C4, NOTE_E4, NOTE_G4, NOTE_C5};


unsigned long start;
bool timerOn = 0;
int pastTimes[28];
int timeSeconds;
boolean isMidnight = false;


long lastPing = 0;
long pingTimeout = 3600;

bool advancePast = false;
bool timerPast = false;
long updateTime, buttonDownTime;
int screenState = 0;

bool pastPowerState = true;
bool powerState = true;

void setup() {
  // Initialize pins
  pinMode(TIMER_BUT_PIN, INPUT_PULLUP);
  pinMode(POWER_SWITCH, INPUT_PULLUP);
  pinMode(SPEAKER_PIN, OUTPUT);
  pinMode(MOTION_PIN, INPUT);

  digitalWrite(SPEAKER_PIN, LOW);
  
  //Initialize the LED pins as a consecutive block of pins
  for(int i = 0; i < LED_CNT; i++) {
    pinMode(LED_BASE_PIN + i, OUTPUT);
  }
  
  //setup pins
  //Serial.begin(SERIAL_SPEED);

  while (!DS3231M.begin())
  {
    //Serial.println(F("Unable to find DS3231MM. Checking again in 3s."));
    delay(3000);
  }

  // initialize the screen (only need to do this once)
  screen.init();
  screen.backlight();
  
  powerState = !digitalRead(POWER_SWITCH);
  pastPowerState = powerState;
  
  DateTime now = DS3231M.now();
  lastPing = now.unixtime();

  updateTime = millis();

  //setTime();
}

void loop() {
  bool timerState = !digitalRead(TIMER_BUT_PIN);
  bool powerState = !digitalRead(POWER_SWITCH);

  //On button down
  if (timerState && !timerPast) {
    buttonDownTime = millis();
  }

  //On button up
  if (!timerState && timerPast) {
    //If the button has been down for more than a second
    //Enter settings dialog
    if(buttonDownTime + 1000 <= millis()) {
      setTime();
    }
    else
    //Otherwise, start the timer
    {
      timer();
    }
    
    updateTime = millis() - 3000;
  }

  //In timer mode, draw the elapsed time so far
  if (timerOn) {
    //But only, once a second
    if ((updateTime + 1000) <= millis()) {
      drawTimer();
      updateTime = millis();
    }
  }
  else
  {
    if(powerState) {
      //Cycle the screen every 3 seconds when power is on
      if ((updateTime + 3000) <= millis()) {
        switch (screenState) {
          case 0:
            drawHome();
            break;
            
          case 1:
            drawWeekCompare(getTotalTime(14, 21), getTotalTime(21, 28));
            break;
            
          case 2:
            drawTwoWeekGraph(pastTimes);
            break;
        }
        
        updateTime = millis();
        screenState = (screenState + 1) % 3;  
      }
    }
    else
    {
      //Update the clock every few seconds when power is off
      if ((updateTime + 5000) <= millis()) {
        drawHome();
        updateTime = millis();
      }
    }

    //If no time is logged for today (and device is on)
    if (pastTimes[27] == 0 && powerState) {
      //If the motion sensor detects motion
      if (digitalRead(MOTION_PIN)) {
        //If it's been a while since we last pinged
        DateTime now = DS3231M.now();
        if (now.unixtime() >= (lastPing + pingTimeout)) {
          //Ping the user
          playSound(sadSound);
          lastPing = now.unixtime();
        }
      }
    }
  }

  if (isNewDay() && !isMidnight)
  {
    newDaySetup();
    isMidnight = true;
  }

  updateLEDs();
  
  //Turn the screen on/off if the power switch is flipped
  if (powerState != pastPowerState) {
    if (powerState) {
      screen.backlight();
      updateTime = millis();
    }
    else {
      screen.noBacklight();
      screenState = 0;
      drawHome();
    }

  }

  pastPowerState = powerState;
  timerPast = timerState;
}

/* The time-setting dialog menu */
void setTime() {
  screen.clear();

  //Grab the current time
  DateTime now = DS3231M.now();
  int hour = now.hour();
  int minute = now.minute();
  int day = now.day();
  int month = now.month();
  int year = now.year();
  
  //Initialize buffers to print before and after the number we choose
  char beforeBuffer[SPRINTF_BUFFER_SIZE];
  char afterBuffer[SPRINTF_BUFFER_SIZE];

  char* header = "      Set Time      ";

  //For each time, format the string around it with the previously set time
  sprintf(beforeBuffer, "%s (", header);
  sprintf(afterBuffer, "):%02d %d/%02d/%04d", minute, month, day, year);
  hour   = getNumInput(24,    beforeBuffer, afterBuffer,   hour, 0);
  
  sprintf(beforeBuffer, "%s %2d:(", header, hour);
  sprintf(afterBuffer, ") %d/%02d/%04d", month, day, year);
  minute = getNumInput(60,    beforeBuffer, afterBuffer, minute, 0);

  sprintf(beforeBuffer, "%s %2d:%02d (", header, hour, minute);
  sprintf(afterBuffer, ")/%02d/%04d", day, year);
  month  = getNumInput(12,    beforeBuffer, afterBuffer,  month, 1);

  sprintf(beforeBuffer, "%s %2d:%02d %2d/(", header, hour, minute, month);
  sprintf(afterBuffer, ")/%04d", year);
  day    = getNumInput(31,    beforeBuffer, afterBuffer,    day, 1);
  
  sprintf(beforeBuffer, "%s %2d:%02d %2d/%02d/(", header, hour, minute, month, day);
  sprintf(afterBuffer, ")");
  year   = getNumInput(10000, beforeBuffer, afterBuffer,   year, 0);

  DateTime newTime = DateTime(year, month, day, hour, minute, 0);
  DS3231M.adjust(newTime);

  //Make sure we show the clock screen after this
  screenState = 0;
}

/* Creates a number input dialog, with strings before and after the number 
   An initial value can be specified, 
   Input is modular about bound (ie 60 for values 0-59)
   The bound range can be offset via minOffset (ie 1 for 1-60 in the above example) */
int getNumInput(int bound, String before, String after, int initial, int minOffset) {
  int value = 0;
  int offset = enc.read() / -4;
  //Make the screen update at least once when we enter this dialog
  bool forceDraw = true;

  while (digitalRead(TIMER_BUT_PIN)) {
    int pos, oldPos;
    pos = (enc.read() / -4) - offset + (initial - minOffset);
    if (pos != oldPos || forceDraw) {
      value = ((pos + bound) % bound) + minOffset;
      screen.clear();
      screen.print(before);

      sprintf(inputBuffer, "%2d", value);
      
      screen.print(inputBuffer);
      screen.print(after);
      oldPos = pos;
      forceDraw = false;
    }
  }

  //Wait for the user to release the button and debounce
  while (!digitalRead(TIMER_BUT_PIN));
  delay(100);

  return value;
}

/* Draws the default clock face screen */
void drawHome() {
  screen.clear();

  screen.setCursor(7, 1);
  DateTime now = DS3231M.now();

  int hr12 = (now.hour() % 12);
  hr12 = (hr12 == 0) ? 12 : hr12;

  sprintf(inputBuffer, "%d:%02d%c", hr12, now.minute(), now.hour() >= 12 ? 'p' : 'a');
  screen.print(inputBuffer);
  screen.setCursor(1, 2);

  //Serial.println(weekDays[0]);

  screen.print(weekDays[DS3231M.weekdayRead() % 7]);
  sprintf(inputBuffer, " %d/%d/%02d", now.month(), now.day(), now.year() % 100);
  screen.print(inputBuffer);
}

/* Draws the timer screen for when an exercise interval is in progress */
void drawTimer() {
  screen.clear();
  screen.setCursor(4, 1);
  screen.print("Elapsed Time");
  
  screen.setCursor(6, 2);

  long unixtime = (DS3231M.now()).unixtime();
  long elapsed = unixtime - start;

  int seconds = elapsed % 60;
  elapsed -= seconds;
  int minutes = (elapsed / 60) % 60;
  elapsed = (elapsed / 60) - minutes;
  int hours = elapsed / 60;

  sprintf(inputBuffer, " %d:%02d:%02d", hours, minutes, seconds);
  screen.print(inputBuffer);

}

/* Draws the the 14 day bargraph screen (for during idle mode) */
void drawTwoWeekGraph(int values[]) {
  int weekDay = DS3231M.weekdayRead();

  screen.clear();
  initGraphChars();
  for (int i = 0; i < 14; i++) {
    drawGraphBar(values[14 + i] / 225, 1, i + 3, 2);
  }

  screen.setCursor(3, 3);
  for (int i = 1; i < 15; i++) {
    screen.write(weekLetters[(i + weekDay) % 7]);
  }
}

/* Draws two horizantal graph bars, representing the total exercise time across the past two weeks */
void drawWeekCompare(int w1, int w2) {
  screen.clear();
  initBarChars();

  //See how much we've improved since last week
  int percentChange = ((w2 - w1) * 100) / w1;

  screen.setCursor(0, 0);
  screen.print("Last week");
  drawHBar(w1 / 300, 1);

  screen.setCursor(0, 2);
  screen.print("This week (");

  //Add a plus if we've gone up (negative added automatically)
  if (percentChange >= 0) {
    screen.print("+");
  }

  screen.print(percentChange);
  screen.print("%)");

  drawHBar(w2 / 300, 3);
}

//Writes each of the horizantal progress bar chars into the LCD
void initBarChars() {
  screen.clear();
  //Start with an empty glyph for the bargraph char
  byte hBar[] = {0, 0, 0, 0, 0, 0, 0, 0};

  //Loop through each bargraph level
  for (int i = 0; i < 5; i++) {
    //Loop through each row of the character
    for (int row = 0; row < 8; row++) {
      //Add in a vertical bar (via bitwise OR)
      hBar[row] = hBar[i] | (0B10000 >> i);
    }

    screen.createChar(i, hBar);
  }
}


/* Draws a horizantal graphbar x pixels wide on the given LCD row */
void drawHBar(int x, int row) {
  screen.setCursor(0, row);

  int offset = 0;
  while (x - offset > 0) {
    int blockChar = x - offset;
    blockChar = constrain(blockChar, 0, 5);

    screen.write((blockChar == 0) ? 32 : blockChar - 1);

    offset += 5;
  }
}

//Writes each of the bargraph level chars into the LCD
void initGraphChars() {
  //Start with an empty glyph for the bargraph char
  byte bar[] = {0, 0, 0, 0, 0, 0, 0, 0};

  //Loop through each bargraph level
  for (int i = 0; i < 8; i++) {
    //Set the lowest line to be all ones
    bar[7 - i] = 0B11111;
    screen.createChar(i, bar);
  }
}

/* Draws a vertical bar 'level' pixels high capped at a given height in char blocks
   at the given position (measured from the top of the bar space) */
void drawGraphBar(int level, int row, int col, int height) {

  for (int y = 0; y < height; y++) {
    int offset = y * 8;
    screen.setCursor(col, row + (height - 1) - y);

    int blockHeight = level - offset;

    //Limit the block height to 0-8
    blockHeight = constrain(blockHeight, 0, 8);

    screen.write((blockHeight == 0) ? 32 : blockHeight - 1);
  }
}

/* Take the sum of exercise time across an interval in our records */
int getTotalTime(int start, int finish) {
  int out = 0;

  for (int i = start; i < finish; i++) {
    out += pastTimes[i];
  }

  return out;
}

/* Starts and stops the exercise timer */
void timer()
{
  long unixTime = DS3231M.now().unixtime();

  if (!timerOn) {
    start = unixTime;
    playSound(happySound);
  }
  else {
    timeSeconds = unixTime - start;

    pastTimes[27] += timeSeconds;
    
    //Set the screen up to show the bargraph
    screenState = 2;
    updateTime = millis() - 3000;
  }

  timerOn = !timerOn;
}

/* Moves all recorded times over one day in memory (so day 27 is always the most recent) */
void shiftTimes()
{
  for (int j = 0; j < 27; j++)
  {
    pastTimes[j] = pastTimes[j + 1];
  }

  pastTimes[27] = 0;
}

/* Updates that need to be performed once each day */
void newDaySetup()
{
  shiftTimes();
  timeSeconds = 0;
}

/* Decide if a day has passed and we need to update things */
boolean isNewDay()
{
  DateTime now = DS3231M.now();
  if (now.hour() == 0 && now.minute() == 0 and now.second() == 0)
  {

    return true;

  }
  else
  {
    isMidnight = false;
  }
  return false;
}

/* Plays the given sound effect */
void playSound(int noteArray[])
{
  int noteLength = noteArray[0];
  for (int thisNote = 0; thisNote < noteLength; thisNote++) {
    int noteDuration = noteArray[1 + thisNote];
    int notePitch = noteArray[2 * noteLength - noteLength + 1 + thisNote];

    int noteTime = 300 / noteDuration;
    tone(SPEAKER_PIN, notePitch, noteTime);

    int pauseBetweenNotes = noteTime * 1.30;

    delay(pauseBetweenNotes);
    noTone(SPEAKER_PIN);
  }
}

/* Update the LEDs to reflect which days we have and haven't logged times */
void updateLEDs() {
  for(int i = 0; i < LED_CNT; i++) {
    digitalWrite(LED_BASE_PIN, pastTimes[27 - i] > GOAL_TIME);
  }
}

 

/*************************************************
 * pitches.h
 * Public Constants
 *************************************************/

#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35
#define NOTE_D1  37
#define NOTE_DS1 39
#define NOTE_E1  41
#define NOTE_F1  44
#define NOTE_FS1 46
#define NOTE_G1  49
#define NOTE_GS1 52
#define NOTE_A1  55
#define NOTE_AS1 58
#define NOTE_B1  62
#define NOTE_C2  65
#define NOTE_CS2 69
#define NOTE_D2  73
#define NOTE_DS2 78
#define NOTE_E2  82
#define NOTE_F2  87
#define NOTE_FS2 93
#define NOTE_G2  98
#define NOTE_GS2 104
#define NOTE_A2  110
#define NOTE_AS2 117
#define NOTE_B2  123
#define NOTE_C3  131
#define NOTE_CS3 139
#define NOTE_D3  147
#define NOTE_DS3 156
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_FS3 185
#define NOTE_G3  196
#define NOTE_GS3 208
#define NOTE_A3  220
#define NOTE_AS3 233
#define NOTE_B3  247
#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988
#define NOTE_C6  1047
#define NOTE_CS6 1109
#define NOTE_D6  1175
#define NOTE_DS6 1245
#define NOTE_E6  1319
#define NOTE_F6  1397
#define NOTE_FS6 1480
#define NOTE_G6  1568
#define NOTE_GS6 1661
#define NOTE_A6  1760
#define NOTE_AS6 1865
#define NOTE_B6  1976
#define NOTE_C7  2093
#define NOTE_CS7 2217
#define NOTE_D7  2349
#define NOTE_DS7 2489
#define NOTE_E7  2637
#define NOTE_F7  2794
#define NOTE_FS7 2960
#define NOTE_G7  3136
#define NOTE_GS7 3322
#define NOTE_A7  3520
#define NOTE_AS7 3729
#define NOTE_B7  3951
#define NOTE_C8  4186
#define NOTE_CS8 4435
#define NOTE_D8  4699
#define NOTE_DS8 4978

 

]]>
Jeffrey’s Light: Final Documentation https://courses.ideate.cmu.edu/60-223/f2019/work/jeffreys-light-final-documentation/ Tue, 10 Dec 2019 22:52:51 +0000 https://courses.ideate.cmu.edu/60-223/f2019/work/?p=8897 With the goal of creating a useful implement for an older person in mind, we looked into the life of our team member Jeff. As a blinds installation professional, Jeff often has to demonstrate to his clients how his products affect the quality of light passing through them. We designed a portable and adjustable light source to help him simulate different lighting conditions in relation to his window blind samples.

Our initial meeting documentation can be found here.

Our prototype documentation can be found here.

What we built

Our final product is a portable light source that replicates a range of weather and lighting conditions through a control panel on its top surface. Two knobs control color temperature and intensity, while three buttons provide sunny, cloudy, and sunrise/sunset lighting presets. These presets can also be customized by holding the button down for more than two seconds, adjusting the values, and pressing any button to save. Additionally, the light source comes with a carrier box that doubles as a stand. It can be used to transport the light source and adapter, and flipped over to act as a stand for the light source to be placed atop.

Overall Photo:

An overall image of the final product.

Basic Operation:

Demonstrating the knob and button interaction.

Details / Highlights:

Light source and adapter in carrier.

Light source (off) on stand.

Light source (on) on stand.

Close-up of diffused LED panel.

Knobs and their corresponding icons (left: color temperature, right: intensity).

Buttons and their corresponding icons (left: sunny, middle: cloudy, right: sunrise/sunset).

Usage:

Turning the knobs.

Pressing the buttons.

Reading the LCD display.

Editing the cloudy preset values.

Narrative:

Jeff has a sales pitch today at a client’s house. He places the lighting simulator and its adaptor into its carrier and packs it into his car. He commutes to his client’s house and brings the carrier inside along with his other equipment. As he is going through the window blind choices with his client, his client gets confused about some of the options. To give his client a better visual, Jeff unpacks the light and places it on top of its carrier. He plugs in the adaptor and the device turns on. He pressed the sunny button and holds the opaque blind sample in front to show that no light passes through. He then hold up the semi-opaque blind sample to show the difference in light quality. After Jeff fiddles with the light settings to give his client a realistic image of how light would pass through each of the blinds, Jeff’s client is able to make a confident decision on which blinds to purchase.

How We Got Here

In our initial planning, we split the tasks into three main stages: programming/hardware, fabrication, and assembly. Our proposed project timeline can be seen below:

Proposed project timeline.

Based on the feedback we received on our prototype, we began by researching options for a brighter LED module in order to create a light source closer to natural sunlight.

After some research, we identified several important criteria we wanted our light source to achieve.

Since we intended to simulate lighting conditions through a window, the light source would have to achieve color qualities within a large portion of those ranges, which can extend from 1000 K to 10000K, which is basically a range from a deep red sunset to a deep blue sky. We determined that such a lighting simulator would be well-balanced if it encompassed the middle range, 3000K to 6000K, which would give options from orange-ish for dawn and dusk times to a white that’s tinted slightly blue for a general daylight color.

Secondly, we aimed for a product that could reproduce the intensities of sunlight and artificial lights to some degree. Sunlight intensity at Earth’s surface has a value of about 100,000 lux, and while a window is not likely to reach such intensities often, these can be important factors in replicating the performance of the blinds had they been demoed in those lighting conditions. Also, the high intensity also offers a way to simulate bright artificial lights, such as security lights, as well. Jeff mentioned that blinds may be installed with blocking such lights in mind, so a light capable of bright white light like that of bright sunlight could also approximate an artificial light to some extent.

A third factor that we thought was worth considering was the light source’s ability to simulate the quality of sunlight, specifically its full spectrum of light. A measure of a light’s quality, or closeness to the full spectrum, is its CRI value, so if we wanted a light that simulated lighting conditions, then it would make sense to have a light to approximate the natural light source as much as possible.

Thus, we settled on a light with the following specifications:

Color Range: 2700K – 6500K

Intensity: Max intensity is 3500 lumens at a color temperature of 4100K (close to white). A quick calculation gives around 110,000 to 120,000 lux. This is of course at its peak intensity, which occurs at a color temperature of 4100 K, but this intensity is mostly just necessary for direct sunlight, which is a close temperature of around 5000K, so the light should still be able to get close to the necessary intensity.

CRI value: rated to at least 95 CRI on a scale of 100.

Overall, the LED strip we got generally satisfied all our criteria. The only catch was that they were rather expensive. A link to the store page is provided below.

BC Series High CRI LED Multirow Hybrid Color Temperature LED Flexible Strip – Pack: 1 pcs

Fabrication Details

In terms of fabrication, the first step was to make adjustments to the design of the outer casing. Jeff had requested that the form be elongated horizontally to cover more area. In addition, he asked for a stand to elevate the light in order to create extra space at the bottom for when he stretches the blind sample. The outer casing and stand were designed in tandem to fit together. With the element of the light, adaptor, and stand in mind, we decided to make the stand double as a carrying case that can transport the other two items. We thought this could be particularly useful for Jeff since he commutes to his clients’ homes by car.

Redesigning aspects through sketching. Jeff had also mentioned that the light should be stable atop the stand, so we decided to add bumpers to the bottom face of the light that perfectly fit into cutouts on the top face of the stand.

After the designs were finalized, we constructed the CAD model for the outer casing in Fusion360 and sent it for 3D printing through the Stratasys printer.

Screenshot of the outer casing CAD model in Fusion 360.

We based the dimensions of the stand on the size of both the light and its adaptor. We input these dimensions into makeabox.io which gave us the dxf file to laser cut the stand. Slots on either side were added in Adobe Illustrator for when Jeff chooses to use the stand as a carrying case. We also added the circle cutouts that would fit the bumpers. Beyond this, fabricating the stand was a fast process.

The finished stand / carrier. The laser cut pieces were then joined together using bondene.

As for the outer casing, the 3D print was placed in the parts wash and finished using bondo and spray paint. Bondo was applied and sanded down to create a smooth surface for spray painting. We chose to use matte grey spray paint because Jeff wanted a sleeker look.

The finished stand / carrier and outer casing. Icons can be seen beneath the hole for each knob / button.

We also laser cut and engraved small icons that indicate the function of each knob / button. From left to right, the icons represent the color temperature knob, the brightness / intensity knob, the sunny preset button, cloudy preset button, and sunrise / sunset preset button. The holes for the icons were designed into the CAD file.

Finally, we added the bumpers on the bottom face of the outer casing.

Bottom face of outer casing with bumpers attached.

Technical details

While the structure of the prototype code was largely retained by the final code, several revisions had to be made account for several factors.

The initial big change was switching from using a LED library to manage the LEDs to a more ‘manual’ direct control of the LED intensities via PWM on transistors that controlled the actual current flow to the LED panels, although this is greatly simplified by the analogWrite() function.

This also meant that the color temperature of the board would have to be guided through a completely different method. The research on the LEDs here offered some helpful information on how the colors blended between the two LED circuits on the board, one for warm LEDs and one for cold LEDs, to produce the color temperatures we desired. One of the graphs on the sight indicated a close to linear relationship between intensity and color for both LED circuits given the other one was fully on, so I reasoned that the intensity of either circuit would cause an additive change in the temperature given the other was fully on. Without a way to test, we couldn’t be entirely sure, but the visual results suggested that the observed LED color temperatures were fairly close to the color temperatures we mapped them to.

Another change that was included fairly late in the game was the inclusion of a save system for the button presets, which resulted in some less than optimal code since it hadn’t been initially planned for.

On the electronics side of things, there were several notable changes to account for the high voltage LED panels and their 2 circuit design. We switched to using transistors and power regulators in the design to account for the 24V supply we switched to per the voltage requirements of the LEDs while allowing us to power the 5V Arduino Pro Mini. As pushing current through a resistor wastes a lot of the power that the LED will use and creates heating problems, a transistor allowed us to control the LED intensity as well in conjunction with PWM.

Before fully committing to the new design, we tested a few subsystems like transistors and power regulators on a solderless breadboard with the LED panel before upgrading to a soldered one for a more permanent design. This soldering requirement forced us to switch to the Arduino Pro Mini as we had been using the Arduino Uno for the prototype and testing many of the subsystems.

Soldering ended up taking a fair deal of time, but once it was done, all that was left was to install the wired up parts into the frame. We did not solder the power supply barrel jack immediately to the board however as we needed to mount it first, so we soldered the rest of the board before mounting the barrel jack, then soldered the barrel jack’s wires to the board.

Testing the code and electrical components on a mini LED panel before our actual module arrived in the mail.

Integrating the LED module.

The new LED module is much brighter; closer to natural sunlight than the LED strip used in our prototype.

Wiring up the pro mini.

Moving from breadboard to protoboard.

Making progress in soldering the electrical components

Once all the connections were soldered, we began assembling everything together.

All components ready to be assembled.

We started by securing the LCD screen by hot glueing it in place, then mounting the knobs and buttons. We then glued the protoboard to the back face of the casing to prevent shifting during transport.

LCD screen, knobs and buttons mounted. Protoboard in place.

Finally, we created a cardboard backing for the LED module and glued that into place. The last step was glueing the frosted acrylic panel in front.

The LED module glued in place on cardboard backing.

In the end, we weren’t able to quite follow our timeline due to the time taken to find and confirm the purchase of the LED panel as well as planning certain milestones for classes that were during Thanksgiving break, but we were able to catch up with some work done on the Sunday (lots of soldering) and Monday (final assembly) before the final critique.

Conclusions and Lessons Learned
Findings from the final critique

A common point of critique was the stand for the device, which some mentioned could easily look dirty or show signs of wear due to its very smooth acrylic material. One of the reviewers remarked that the stand could be made of “wood or another opaque surface” and that both the device stand and our acrylic ‘diffuser’ panel that goes in front of the LED panel could be made of “a material that can take more of a beating.” While the stand had admittedly been designed with the device in mind, it certainly didn’t receive close to the attention from us that the device received, and its the thin, smooth acrylic material turned out to be a poor choice.

One reviewer remarked that we should’ve taken a “closer study of Jeff’s pitch” while another posed the question of “how will Jeff carry this into [the] customer’s home”, probably concerning the handles on the stand not being ideal. Since we hadn’t seen Jeff do a sales pitch, we recognized that this was an unfortunate oversight in our development process, as a number of the other points of criticisms can be drawn from a lack of awareness of its day to day use.

Another point of contention was our choice of placement of the controls, which could affect the sales pitch. While the controls are on the top in our design for ease of accessibility according to Jeff’s desire to put it up against a window, a reviewer said “controls might be better on back”, which would hide them from the client. Both options certainly have advantages, although Jeff seemed pleased enough with the interface as it was.

The 2 adjustable variables of temperature and intensity give the user a lot of direct control and a simpler UI to deal with, but one reviewer did point out that “lighting could be more simulation-oriented”, which is perhaps more user friendly for those less inclined to the control scheme using somewhat technical variables and prefer something they can think of, like a cloudy morning in April (weather, time, season settings vs. color temperature, light intensity settings). If we had looked for and found simple mathematical models that combine all these inputs into something like color temperature and intensity, we recognize that this may have been feasible, although this would ultimately have been a design decision we’d have had to make with Jeff after agreeing initially upon a design fairly similar to our prototype.

Despite a number criticisms, many of the reviewers also had praise for the visual aspect of the design, especially that of the main device (a bit less enthusiasm for the stand). Adjectives like “sleek”, “beautiful”, “well-designed”, “professional”, “fit together”, and “product-like” were used to describe the visual design, and what’s more, Jeff wrote that “I will use this light box daily”. What more could we ask for?

Major takeaways

One of the biggest takeaways we got from working with an older person is that everyone is unique in ways that are often overlooked by stereotypes. We went into the initial meeting with generalizations about the hobbies that an older person might enjoy, or the way that they live their life. However, we were surprised to find that many of these assumptions did not line up. This seemed to be the case for many other groups as well. In our case, Jeff is very active and had no troubles with any physical aspects of his life. He is still highly engaged in his professional life and is very technologically adept. Him and his wife are very social and spend little time in their home. This went against many of our initial assumptions of what we were designing as we expected to create something more closely related to physical needs caused by aging as well as something that would remain at his home. Just as not all young people are addicted to social media and pop culture, older people are also unique in their habits and lifestyles, making it all the more important to research the individual’s particular wants and needs.

We also got a chance to learn from Jeff how our device could play a role in his business and even the entire industry. While there are large studio setups that could do a better job than our device, they’re hardly portable. Our device is both light and compact, making it easy to transport from car to home, but the pragmatic aspect didn’t seem all that novel to us considering similar photography lights did exist, even if they generally didn’t aim for such high light intensities. So we had figured it would be simply a minor addition to the sales pitch to reinforce what Jeff was already telling the customer. But according to Jeff, the device’s role in demonstrations is part of the “sizzle” in a sales pitch that can give a salesman an edge, and, as per Jeff’s own words, “sizzle sells”.

Concluding thoughts

There were several lessons that we took away from this experience.

A variety of technical issues came up throughout the course of developing this device that will serve as helpful experience for the future to prevent mistakes.

Soldering was a challenge at times, especially on the rare occasion that one of the many pins we had to solder in for the Arduino Pro Mini we were using wasn’t connected well by the solder, which made debugging a real challenge, as determining what was a code bug vs. an electrical issue can be hard to see. Bulk soldering can also lead to forgetting about adding heat shrink until both ends of a wire is soldered on, and the potential risk isn’t worth the quicker soldering, since wrapping electrical tape around small wires isn’t fast by any means.

An unfortunate change in the final product from the prototype was switching of the encoder pins from pins 2,4 for one encoder to pins 2,3 and pins 3,5 for the other encoder to pins 4,5. While it appeared to be a seemingly innocuous and more organized change,it turns out that pins 2 and 3 are special interrupt pins on the Arduino Uno (for the prototype) and the Arduino Pro Mini that are especially good for use with encoders. As a result, an encoder output is read best if it has two or at least 1 of these interrupt pins attached, but in this case, 1 encoder got both of the interrupt pins when it would’ve been better for both to get 1. Nevertheless, the ‘faulty’ encoder still works very well if it’s turned slowly enough, so the problem was largely overlooked, as it was an uncommon issue during testing when we hardly turned it very fast.

A few design choices could’ve also potentially enhanced our device.

We hadn’t designed the housing to have a ledges for the LED panel to rest on, which lead to the use of the cardboard backing and hot glue. Additionally, the housing also had no access from the back, so it was effectively impossible to access the internal electronics once it was sealed in by the acrylic panel.

Perhaps embedding the controls in the back could’ve given it an even sleeker look, although the top access is arguably more accessible, but it would’ve been worth considering.

We really should’ve inquired more into how Jeff might use the device on a sales pitch to better customize the stand/carrier. Then we probably would’ve ended up with a sturdier and hopefully easier to carry device.

Overall, creating this device, the result of our extensive efforts, was an engaging and wonderful opportunity for improving skills, gaining experience and  enhancing Jeff’s life.

Technical details:

Code:

/*
  Final Project: Jeffrey's Light

  Description: This code is intended to serve as an interface
  between User Input (consisting of 2 encoders, 3 buttons and an LCD display)
  and configurations for approximate color and relative intensities
  for a high power, bicolor LED panel.

  NOTE: The pins chosen for the encoders are not optimal,
  as one encoder has no interrupt pins (pins 2 and 3), which makes it less effective
  While 2 is optimal, the limited number of interrupt pins makes giving
  each encoder 1 interrupt pin a better balance, therefore pins 2,4 -> Encoder 1 and
  3,5 -> Encoder 2 or something similar would work best.

  Input:
  Pins|Connection (relevant properties)

  2   |Encoder 1 (interrupt pin)
  3   |Encoder 1 (interrupt pin)
  4   |Encoder 2
  5   |Encoder 2
  7   |Button 1
  8   |Button 2
  9   |Button 3

  Output:
  Pins|Connections
  10  |LED Circuit 1 (PWM)
  11  |(PWM) LED Circuit 2 (PWM)
  A4/A5|LCD Screen (SDA/SCL)


  Resources Used:

  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

  Some Button-related code snippits adapted from my Project 2 code

  Adapted code from public domain examples in Encoder Library by Paul Stoffregen
  https://www.pjrc.com/teensy/td_libs_Encoder.html

  Data on bicolor LED panel used in device from product website
  BC Series High CRI LED Multirow Hybrid Color Temperature LED Flexible Strip - Pack: 1 pcs

  Some data on the LEDs, most importantly regarding the balance of LED temperatures
  
Complexities of Bicolor LED Lights: An Extensive Color Analysis
*/ #include <Encoder.h> #include <LiquidCrystal_I2C.h> #include <EEPROM.h> //PINS assignments Encoder IntenKnob(2, 3); Encoder ColorKnob(4, 5); const int B1_PIN = 7; const int B2_PIN = 8; const int B3_PIN = 9; const int WARM_LEDS = 10; const int COLD_LEDS = 11; //Uses SDA/SCL pins, not shown LiquidCrystal_I2C screen(0x27, 16, 2); //Presets and State Constants// //State constants based on states set by buttons (effectively an enumeration) const byte CUSTOM = 0; const byte DAYLIGHT = 1; const byte CLOUDY = 2; const byte SUNSET = 3; //Temperature constants of LEDs in Kelvin const int MAX_TEMP = 6500; //Highest color temperature rating by manufacturer const int MIN_TEMP = 2700; //Lowest color temperature rating by manufacturer const int MID_TEMP = 4100; //LEDs reach peak brightness at this color temperature //Max - Min = 3800 K //LED Presets int DAY_TEMP = 6000; float DAY_INTEN = 0.86; int CLOUDY_TEMP = 6500; float CLOUD_INTEN = 0.02; int SUNSET_TEMP = 3000; float SUNSET_INTEN = 0.2; //Commented out struct below is present in device code but is not used /* struct saveData { int DAY_TEMPs; float DAY_INTENs; int CLOUDY_TEMPs; float CLOUD_INTENs; int SUNSET_TEMPs; float SUNSET_INTENs; }; */ //Variables// //I/O Constants// //Presets chosen to define the range of encoder values //50 intervals for intensity ->0.02 per encoder 'tick' //38 intervals for temperature -> 100 K per encoder 'tick' const int EncIntenRange = 50; const int EncColorRange = 38; //Constants that serve as delimeters for milliseconds it takes to recognize either //a press (50ms) for setting light to config preset or a hold (2000ms) to edit that preset const int bPressTime = 50; const int bEditTime = 2000; //Used for storing previous Intensity and Color values between loop calls int lastInten, lastColor = -1; //Keeps track of how long any of the three buttons have been held int buttonTimes[3]; //These store the output values to the PWM pins (index 0 is warm LEDs; index 1 is cold LEDs) int PWMs[2]; //Time Variables// //Used to keep track of elapsed time between loop calls unsigned long lastMillis; //A constant delay value (should be a constant variable) at the end of loop calls as it should reduce LCD flickering. Possibly unnecessary. int delayTime = 10; //State Variables// //The state of the device in non-editing mode (not relevant while editing) byte state = CUSTOM; //The state of the device during editing (not relevant when non-editing) byte EditState = CUSTOM; //Initialized to not an editable state //Determines if the device is in an editing state or not. bool isEditing = false; //A flag to determine whether to redraw the LCD screen or not bool dirtyLCD = true; //Initialization Constants// //Initialization Temperature and color (a soft white glow to indicate activity without being blinding) const int InitTemp = 5000; const float InitInt = 0.02; void setup() { //Initialize button pins pinMode(B1_PIN, INPUT_PULLUP); pinMode(B2_PIN, INPUT_PULLUP); pinMode(B3_PIN, INPUT_PULLUP); //Initialize LED (PWM) pins pinMode(WARM_LEDS, OUTPUT); pinMode(COLD_LEDS, OUTPUT); lastMillis = millis(); //Loads the 3 presets from EEPROM loadPreset(); //Initialize the screen with relevant commands screen.init(); screen.backlight(); screen.clear(); screen.home(); //Initial color and intensity is set in the relevant 'encoder objects'. IntenKnob.write(rotFromInten(InitInt)); ColorKnob.write(rotFromTemp(InitTemp)); } void loop() { //Reads the encoder values int intenvalue = IntenKnob.read(); int colorvalue = ColorKnob.read(); //Prevents the encoders from leaving the allowed range if (intenvalue > 0) { IntenKnob.write(0); intenvalue = 0; } if (colorvalue > 0) { ColorKnob.write(0); colorvalue = 0; } if (intenvalue < -EncIntenRange) { IntenKnob.write(-EncIntenRange); intenvalue = -EncIntenRange; } if (colorvalue < -EncColorRange) { ColorKnob.write(-EncColorRange); colorvalue = -EncColorRange; } //Record Encoder input (negative due to clockwise motion decreasing the encoder values) int cin = -colorvalue; int inn = -intenvalue; //This variable is largely irrelevant unless in edit mode, in which case inputs that make it //true will lead to the leaving of edit mode. bool stopEdit = false; //Response to changes between current and previous encoder positions if (cin != lastColor || inn != lastInten) { dirtyLCD = true; if (!isEditing)state = CUSTOM; //Only while editing does state changing to custom matter } //Set state or editing based on button input if (!digitalRead(B1_PIN)) { buttonTimes[0] += millis() - lastMillis; if (buttonTimes[0] > bPressTime) { if (state != DAYLIGHT)dirtyLCD = true; state = DAYLIGHT; if (buttonTimes[0] < bEditTime)stopEdit = true; } } else buttonTimes[0] = 0; if (!digitalRead(B2_PIN)) { buttonTimes[1] += millis() - lastMillis; if (buttonTimes[1] > bPressTime) { if (state != CLOUDY)dirtyLCD = true; state = CLOUDY; if (buttonTimes[1] < bEditTime)stopEdit = true; } } else buttonTimes[1] = 0; if (!digitalRead(B3_PIN)) { buttonTimes[2] += millis() - lastMillis; if (buttonTimes[2] > bPressTime) { if (state != SUNSET)dirtyLCD = true; state = SUNSET; if (buttonTimes[2] < bEditTime)stopEdit = true; } } else buttonTimes[2] = 0; //LastMillis is only used above, so we can set it again afterwards lastMillis = millis(); //If any buttons been held for long enough, then device enters Edit Mode if (buttonTimes[0] > bEditTime || buttonTimes[1] > bEditTime || buttonTimes[2] > bEditTime) { if (!isEditing)dirtyLCD = true; stopEdit = false; isEditing = true; EditState = state; } //Initializes the string so it can be used later to print to the LCD String stateStr; if (!isEditing) { switch (state) { case CUSTOM: stateStr = "CUSTOM"; break; //Daylight presets are converted to Encoder presets case DAYLIGHT: cin = (DAY_TEMP - MIN_TEMP) / ((MAX_TEMP - MIN_TEMP) / EncColorRange); inn = (int)(EncIntenRange * DAY_INTEN); stateStr = "SUNNY"; break; //Cloudy presets are converted to Encoder presets case CLOUDY: cin = (CLOUDY_TEMP - MIN_TEMP) / ((MAX_TEMP - MIN_TEMP) / EncColorRange); inn = (int)(EncIntenRange * CLOUD_INTEN); stateStr = "CLOUDY"; break; //Sunset presets are converted to Encoder presets case SUNSET: cin = (SUNSET_TEMP - MIN_TEMP) / ((MAX_TEMP - MIN_TEMP) / EncColorRange); inn = (int)(EncIntenRange * SUNSET_INTEN); stateStr = "SUNRISE/SET"; break; } //If state has been set to non-custom, then overwrite current encoder settings if (state != CUSTOM) { IntenKnob.write(-inn); ColorKnob.write(-cin); } } else { //From above, this is the case where the device is in Editing mode switch (EditState) { case DAYLIGHT: stateStr = "EDIT:SUNNY"; break; case CLOUDY: stateStr = "EDIT:CLOUDY"; break; case SUNSET: stateStr = "EDIT:SUNSET"; break; } } //Convert ending Encoder values to temperature(equivalent to color) and intensity int temperature = MIN_TEMP + (int)((long)(MAX_TEMP - MIN_TEMP) * cin / EncColorRange); float intensity = (float)inn / EncIntenRange; //Writes to the LED panel control transistors using a function of temperature and intensity setPWM(temperature, intensity); //Only redraw the LCD if dirtyLCD flag is true if (dirtyLCD) { screen.setCursor(0, 0); screen.print(" "); screen.setCursor(0, 0); screen.print(stateStr); screen.setCursor(0, 1); screen.print((String)temperature + " K"); screen.setCursor(7, 1); screen.print(" "); //Easier to clear part of screen this way than clear the entirety. screen.setCursor(7, 1); screen.print((String)((int)(intensity * 100))); screen.print("%"); dirtyLCD = false; } //In this case, a button has been pressed during editing, so editing will cancel and the preset will be saved if (isEditing && stopEdit) { savePreset(EditState, temperature, intensity); dirtyLCD = true; //Redraw LCD when canceling out of edit } //Store current values for next loop lastInten = inn; lastColor = cin; //Analog write PWM pins with values set by SetPWM() method analogWrite(WARM_LEDS, PWMs[0]); analogWrite(COLD_LEDS, PWMs[1]); //May be unnecessary. Although it may make the LCD less flickery, it may also make changes to LED values more choppy delay(delayTime); } //conversion from temperature to encoder value int rotFromTemp(int temp) { return -((temp - MIN_TEMP) / 100); } //conversion from intensity to encoder value int rotFromInten(float inten) { return -inten / 0.02; } //sets PWM values when called based on input temperature and intensity void setPWM(int temp, float inten) { //Branching cases depending on whether the temperature is on the cold or the warm side if (temp - MID_TEMP <= 0) { //Warm temperature is at maximum for temperatures above the midTemp PWMs[0] = (int)(255 * inten); //The cold temperature varies depending on how far it ranges from MAX to midTemp PWMs[1] = (int)(inten * 255 * (temp - MIN_TEMP) / (MID_TEMP - MIN_TEMP)); } else { //Cold temperature is at maximum for temperatures above the midTemp PWMs[1] = (int)(255 * inten); //The warm temperature varies depending on how far it ranges from MAX to midTemp PWMs[0] = (int)(inten * 255 * (MAX_TEMP - temp) / (MAX_TEMP - MID_TEMP)); } } //reads the EEPROM for corresponding values for the button presets void loadPreset() { int checkLoad = 0; int i = 0; EEPROM.get(i, checkLoad); if (checkLoad >= MIN_TEMP) { EEPROM.get(i, DAY_TEMP); i += sizeof(int); EEPROM.get(i, DAY_INTEN); i += sizeof(float); } i = sizeof(int) + sizeof(float); EEPROM.get(i, checkLoad); if (checkLoad >= MIN_TEMP) { EEPROM.get(i, CLOUDY_TEMP); i += sizeof(int); EEPROM.get(i, CLOUD_INTEN); i += sizeof(float); } i = 2 * (sizeof(int) + sizeof(float)); EEPROM.get(i, checkLoad); if (checkLoad >= MIN_TEMP) { EEPROM.get(i, SUNSET_TEMP); i += sizeof(int); EEPROM.get(i, SUNSET_INTEN); i += sizeof(float); } } //Method to save the presets for the buttons to EEPROM void savePreset(int preset, int temp, float inten) { int i = 0; switch (preset) { case DAYLIGHT: DAY_TEMP = temp; DAY_INTEN = inten; break; case CLOUDY: CLOUDY_TEMP = temp; CLOUD_INTEN = inten; i = sizeof(float) + sizeof(int); break; case SUNSET: SUNSET_TEMP = temp; SUNSET_INTEN = inten; i = 2 * (sizeof(float) + sizeof(int)); break; } EEPROM.put(i, temp); EEPROM.put(i + sizeof(int), inten); isEditing = false; }

 

Schematic:

Note that this schematic is the one present in the device with the aforementioned encoder pin issue: pin 2,3 ->Encoder 1 and pin 4,5 ->Encoder 2, where 1 encoder gets both interrupt pins while the other gets none. This is also noted in the device code and concluding thoughts, so anyone aiming to reproduce at least the encoder part of the design should take note of it in both their electrical and software components.

Design Files:

design-files.zip

]]>