Description

The Universe Comparator is an interactive project where people try to understand the scale of objects all around the universe relative to each other. If you ever wondered how many humans will fit into the Milky Way or how many ants can span across the Grand Canyon, this is the project you want.

There is a big balloon whose size represents 1 of an object shown on the screen. There is also a number you control on screen. Together, you can find out what these number of objects combine to be equivalent to. For example, if you set the number to 10 and object to Jupiter, you will find that 10 Jupiters are roughly equal to 1 Sun in diameter. You can control this number and object by tilting your hand forward, backward, left and right. There is a sensor (accelerometer) which detects these movements and connects it to certain actions. Tilting it forward/backward means the balloon will inflate/deflate and the object will change. Tilting it left and right will increase/decrease the number.

The balloon and sensors were controlled by an Arduino, pneumatics and solenoid valves connected to atmosphere and air compressor tank. The calculations and display was done through Processing.

In-Progress Media and Code

Planning and buying the pneumatic components for airflow

Initial circuitry to test the sensors and relays

Final Test Run before the Due Date

// Laser Time of Flight sensor (VL52L0X)

#include "Adafruit_VL53L0X.h"

Adafruit_VL53L0X lox = Adafruit_VL53L0X();

void setup() {
  Serial.begin(115200);

  // wait until serial port opens for native USB devices
  while (! Serial) {
    delay(1);
  }
  
  Serial.println("Adafruit VL53L0X test");
  if (!lox.begin()) {
    Serial.println(F("Failed to boot VL53L0X"));
    while(1);
  }
  // power 
  Serial.println(F("VL53L0X API Simple Ranging example\n\n")); 
}


void loop() {
  VL53L0X_RangingMeasurementData_t measure;
    
  Serial.print("Reading a measurement... ");
  lox.rangingTest(&measure, false); // pass in 'true' to get debug data printout!

  if (measure.RangeStatus != 4) {  // phase failures have incorrect data
    Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter);
  } else {
    Serial.println(" out of range ");
  }
    
  delay(100);
}
// This program was used to help control the accelerometer
/*
 * This program reads an accelerometer connected to ACCEL_X_PIN,
 * ACCEL_Y_PIN, and ACCEL_Z_PIN and sends the data back to the
 * computer via serial.
 *
 * Created 2021-02-19 by Perry Naseck
 */

const int ACCEL_X_PIN = A0; // Analog input pin for X movement
const int ACCEL_Y_PIN = A1; // Analog input pin for Y movement
const int ACCEL_Z_PIN = A2; // Analog input pin for Z movement

// Variables to keep track of the current positions
int accelXPos = 0;
int accelYPos = 0;
int accelZPos = 0;

void setup() {
  // Setup serial port to send the accelerometer data back to the computer
  Serial.begin(115200); 
  
  // Setup the accelerometer inputs
  pinMode(ACCEL_X_PIN, INPUT);
  pinMode(ACCEL_Y_PIN, INPUT);
  pinMode(ACCEL_Z_PIN, INPUT);
}

void loop() {
  // Get the current states
  accelXPos = analogRead(ACCEL_X_PIN);
  accelYPos = analogRead(ACCEL_Y_PIN);
  accelZPos = digitalRead(ACCEL_Z_PIN);
  
  // Send the data over serial
  Serial.print("X: ");
  Serial.print(accelXPos);
  Serial.print(" Y: ");
  Serial.print(accelYPos);
  Serial.print(" Z: ");
  Serial.println(accelZPos);

  // Delay to not send messages too fast
  delay(100);
}

Process Reflection

What worked well?

Thanks to the continued effort from Professor Zach and I, the system with the balloon functioned well. There were no leaks in air. The pressure was good. The sensor inside the balloon was responsive. The relays worked well with the solenoid valves and responded to the accelerometer instantly. Fastening the setup to a board helped make it look cleaner and easier to carry.

While the display was not the most effective, the programming behind it was still something that worked out well. I worked on sub-modules with smaller goals like recognizing an object, parsing data, etc. and combined them in the end. There was a lot of complexity behind the scenes (which I will talk about in the next section), which is why I was a bit worried that entire module won’t click together at the end. However, it turned out to respond and communicate with the Arduino well and without errors by the final demo.

Lastly, I enjoyed mounting the sensor on the Velcro through knitting. I’d like to thank Professor Mallea who helped me with doing so. The sensor looked clean and was relatively easy to wear.

Challenges faced

Since it was my first time working with Processing and making it communicate to Arduino, I ran into many issues. Fortunately, having some coding knowledge from my ECE background helped pinpoint and debug errors more easily than I would otherwise. Some of the common issues I ran into that I spent a while figuring out were:-

  • Parsing serial data from Arduino and handling cases where it would sometimes send ‘null’ to crash the program
  • Realizing that strings can’t be compared with == sign
  • Data types having maximum and minimum ceilings and working around that
  • Indexing into weird lists/dictionaries when data wasn’t communicated properly from Arduino

I also spent a good amount of time coding the calculation and finding the equivalent object it corresponded to. Dictionaries and lists were my friends in this case.

Finally, a lot of time was also spent to figure out the connections between the air compressor tank and balloons. Through the help of Professor Zach, who had a little bit of experience with this, we were able to find the right components to make this system work.

What I could have done better?

The balloon and the screen felt very disconnected in the end product. A larger display closer to the balloon would have strengthened their connection better. Furthermore, while the written information conveyed the difference in scale, this effect is nearly not as effective as showing a visual comparison on the screen as well. Dealing with the many issues in programming, I did not have enough time to implement a visual scaling comparison on the display.

Similar to the last project, the wires were out on display again. Some sort of a covering or container to hide everything but the balloons and the screen would have definitely made it more aesthetically pleasing. Last but not least, my initial plan was to make people feel more insignificant in the huge scale of things once they interacted with this project. I don’t believe I achieved that. To do so, I feel that I need to build more of an atmosphere or a story and a constant reference of their own size to the everything else.

Lastly, the demo and suggestions showed me that the project could have gone in an entirely different direction based on how the balloon turned out. The clicking noises of the relays and valves with the loud compressor tank created an atmosphere of being alert. The balloon growing larger and larger induced a sense of danger. Working around these aspects, I might have been able to make a more ‘Halloween’ themed project to create a scary atmosphere.

Learning

This has definitely been one of the few projects throughout my life that taught me so many different things. Processing is a new tool I learnt, which seems to have so many more cool things to explore. Working with pneumatics, compressor tanks and air is another big thing I learnt. Basic knitting is also something I got to experience and learn. My arsenal of new tools expanded by a lot, and I am grateful for this experience.

Universe Comparator in Action

Content on the Display Screen

Participant inflating the balloon and observing the display

Participant wearing the 3-axis accelerometer

Inflation of the balloon to a decently large size

Participant changing the number on the display

Video of Universe Comparator (File too large/poor quality after compression to fit here):

https://drive.google.com/file/d/1YU0wM-NZFxYr8IlpXU9Y9c5c38fPb6R9/view?usp=sharing

Same video on Youtube:

Code

/*
   Universe Comparator: Arduino Code, Tushhar Saha
   This sketch uses the accelerometer to control relays attached to
   the balloon inflation and deflation mechanism. Furthermore, it
   translates data from VL53L0X and accelerometer to send to 
   Processing sketch in Serial.
*/
#include "Adafruit_VL53L0X.h"
const int ACCEL_X_PIN = A0; // Analog input pin for X movement
const int ACCEL_Y_PIN = A1; // Analog input pin for Y movement
const int ACCEL_Z_PIN = A2; // Analog input pin for Z movement
const int Balloon_inf = 12;
const int Balloon_def = 11;
const int offset = -100;

// Variables to keep track of the current positions
int accelXPos = 0;
int accelYPos = 0;
int accelZPos = 0;
int num = 1;

Adafruit_VL53L0X lox = Adafruit_VL53L0X();

void setup() {
  Serial.begin(115200);

  // wait until serial port opens for native USB devices
  while (! Serial) {
    delay(1);
  }

  if (!lox.begin()) {
    Serial.println(F("Failed to boot VL53L0X"));
    while (1);
  }

  pinMode(ACCEL_X_PIN, INPUT);
  pinMode(ACCEL_Y_PIN, INPUT);
  pinMode(ACCEL_Z_PIN, INPUT);
  pinMode(Balloon_inf, OUTPUT);
  pinMode(Balloon_def, OUTPUT);
}

void loop() {
  VL53L0X_RangingMeasurementData_t measure;

  lox.rangingTest(&measure, false); // pass in 'true' to get debug data printout!

  if (measure.RangeStatus != 4) {  // phase failures have incorrect data
    //Serial.print("Distance (mm): ");
    int distance = measure.RangeMilliMeter;
    String object = whichObject(distance);
    // Send to Processing
    Serial.print(object); //object
  } else {
    Serial.print("Observable Universe"); //out of range
  }

  Serial.print(",");
  accelXPos = analogRead(ACCEL_X_PIN);
  accelYPos = analogRead(ACCEL_Y_PIN);
  num = calculateNumber(num, accelYPos);
  Serial.println(num);

  // Inflation and Delfation
  if (accelXPos < 310) {
    digitalWrite(Balloon_inf, LOW);
    digitalWrite(Balloon_def, HIGH);
  } else if (accelXPos < 360) {
    digitalWrite(Balloon_inf, LOW);
    digitalWrite(Balloon_def, LOW);
  } else { //set max
    digitalWrite(Balloon_inf, HIGH);
    digitalWrite(Balloon_def, LOW);
  }

  delay(500);
}

// Corresponding celestial objects sent to Processing
String whichObject(int dist) {
  // x = 250,290,330,375,400,450,500,550,600,650 (add a max)
  // "Ants", "Humans", "Grand Canyon", "Earths", "Suns", "Diameter of Solar System", "1 Light Year", "The Milky Way", "The Local Group of Galaxies", "Observable Universe"
  if (dist < (offset + 250)) {
    return "Ant";
  } else if (dist < (offset + 290)) {
    return "Human";
  } else if (dist < (offset + 340)) {
    return "Grand Canyon";
  } else if (dist < (offset + 375)) {
    return "Earth";
  } else if (dist < (offset + 425)) {
    return "Sun";
  } else if (dist < (offset + 500)) {
    return "Diameter of Solar System";
  } else if (dist < (offset + 550)) {
    return "1 Light Year";
  } else if (dist < (offset + 600)) {
    return "The Milky Way";
  } else if (dist < (offset + 650)) {
    return "The Local Group of Galaxies";
  } else {
    return "Observable Universe";
  }
}

//Thresholds for number sent to processing
int calculateNumber(int number, int x) {
  if (x < 265) {
    return number + 2;
  } else if (x < 290) {
    return number + 1;
  } else if (x < 350) {
    return number;
  } else if (x < 365) {
    return number - 1;
  } else {
    return number - 2;
  }
}
/*
Universe Comparator: Processing Code, Tushhar Saha
 This sketch receives data from Arduino for the number input and the
 object equivalent to the size of the ballon. This data is shown visually,
 and it is also used to calculate equivalent celestial objects.
 */
import processing.serial.*;

PFont f;
FloatDict univObjects;
float ly;
float[] diameters;
String[] objects;
StringDict findObject;

Serial myPort;  // Create object from Serial class
String val;     // Data received from the serial port
String object;
String number;
PImage[] img_objects;
PImage pic1;
PImage pic2;
PImage Uranus;
PImage Arrow;
PImage DownArrow;
PImage title;

void setup() {
  size(2500, 2000);
  // Setting up a dictionary with sizes as values
  ly = 9.5*pow(10, 12);
  univObjects = new FloatDict();
  univObjects.set("Atom", pow(10, -13));
  univObjects.set("Red Blood Cell", 8*pow(10, -9));
  univObjects.set("Thickness of a Paper", pow(10, -7));
  univObjects.set("Ant", pow(10, -5));
  univObjects.set("Mouse", pow(10, -4));
  univObjects.set("Human", 1.77/1000);
  univObjects.set("Football Field", 0.105);
  univObjects.set("Grand Canyon", 500);
  univObjects.set("Moon", 3500);
  univObjects.set("Earth", 12742);
  univObjects.set("Jupiter", 140000);
  univObjects.set("Sun", 1.39 * pow(10, 6));
  univObjects.set("Distance from the Sun to Earth", 150 * pow(10, 6)); //also equivalent to 1 AU
  univObjects.set("UY Scuti", 24 * pow(10, 9));
  univObjects.set("Diameter of Solar System", 3 * pow(10, 11));
  univObjects.set("TON 618", 4 * pow(10, 11));
  univObjects.set("1 Light Year", ly);
  univObjects.set("Distance between Proxima Centauri and the Sun", 4*ly);
  univObjects.set("Omega Centauri", 160 * ly); //largest globular cluster in Milky Way
  univObjects.set("Sombrero Galaxy", 50000 * ly);
  univObjects.set("The Milky Way", 120000*ly);
  univObjects.set("Tadpole Galaxy", 280000*ly);
  univObjects.set("The Local Group of Galaxies", pow(10, 7)*ly); //contains milky way, etc.
  univObjects.set("Bootes Void", 33*pow(10, 7)*ly); //No galaxies
  univObjects.set("Laniakea Supercluster", 5*pow(10, 8)*ly); //Made up of galaxy groups, supercluster home to Milky way
  univObjects.set("Sloan Great Wall", 1.38*pow(10, 9)*ly); //Giant wall of galaxies (maybe largest structure)
  univObjects.set("Observable Universe", 93*pow(10, 9)*ly);
  univObjects.set("Universe", pow(10, 13)*ly);

  diameters = univObjects.valueArray();
  objects = univObjects.keyArray();
  findObject = new StringDict(); //This dictionary is used to find objects that correspond to a certain size (objects are values here)

  for (int i = 0; i < 28; i = i+1) {
    findObject.set(str(diameters[i]), objects[i]);
  }

  //Images
  img_objects = new PImage[28];
  img_objects[0] = loadImage("Atom.jpg");
  img_objects[1] = loadImage("RBC.jpg");
  img_objects[2] = loadImage("Paper.jpg");
  img_objects[3] = loadImage("Ant.jpg");
  img_objects[4] = loadImage("Mouse.jpg");
  img_objects[5] = loadImage("Human.jpg");
  img_objects[6] = loadImage("Field.jpg");
  img_objects[7] = loadImage("GC.jpg");
  img_objects[8] = loadImage("Moon.jpg");
  img_objects[9] = loadImage("Earth.jpg");
  img_objects[10] = loadImage("Jupiter.jpg");
  img_objects[11] = loadImage("Sun.jpg");
  img_objects[12] = loadImage("DistES.jpg");
  img_objects[13] = loadImage("UYScuti.png");
  img_objects[14] = loadImage("Solar System.jpg");
  img_objects[15] = loadImage("Ton.png");
  img_objects[16] = loadImage("ly.jpg");
  img_objects[17] = loadImage("Proxima.png");
  img_objects[18] = loadImage("Omega.jpg");
  img_objects[19] = loadImage("Somb.jpg");
  img_objects[20] = loadImage("Milky.jpg");
  img_objects[21] = loadImage("Tadpole.jpg");
  img_objects[22] = loadImage("LocalGroup.png");
  img_objects[23] = loadImage("bootes.jpg");
  img_objects[24] = loadImage("Lani.jpg");
  img_objects[25] = loadImage("Sloan.png");
  img_objects[26] = loadImage("ObservableUniverse.png");
  img_objects[27] = loadImage("Uni.jpg");
  Uranus = loadImage("Uranus.jpg");
  Arrow = loadImage("Arrow.jpg");
  DownArrow = loadImage("DownArrow.jpg");
  title = loadImage("title.jpg");
  // Create the font
  // printArray(PFont.list());
  f = createFont("Palatino Linotype Bold", 48);

  textFont(f);

  //Data from Arduino
  String portName = Serial.list()[0]; //change the 0 to a 1 or 2 etc. to match your port
  myPort = new Serial(this, portName, 115200);
}

void draw() {
  background(0);
  textAlign(CENTER);

  //Data from Arduino
  if (myPort.available() > 0)
  { // If data is available,
    val = myPort.readStringUntil('\n'); // read it and store it in val
  }
  if (val==null) {
    return;
  }
  //Parsing Data
  String[] arduinoData = split(val, ',');
  object = arduinoData[0];
  if (arduinoData.length != 2) {
    return;
  }
  number = arduinoData[1];
  number = number.replace('\n', ' ');
  number = trim(number);
  float numI = pow(10, int(number)); // use this
  if (!univObjects.hasKey(object)) { //stops errors in reading
    return;
  }

  float ans = calculateObject(numI, object, univObjects, diameters);
  String answer = findObject.get(str(ans)); //finds equivalent object
  //find images correponding to answers
  pic1 = findImage(object);
  pic2 = findImage(answer);
  //display
  textSize(48);
  imageMode(CENTER);
  image(pic1, 0.25*width, 0.6*height, 0.4 * height, 0.4 * height); //left image
  text("is equivalent to", 0.5*width, 0.6*height);
  text(answer, 0.75*width, 0.9*height);
  drawFact(answer);
  fill(255, 255, 0);
  text("Your balloon size represents 1 of this", 0.25*width, 0.25*height);
  image(DownArrow, 0.25*width, 0.32*height, 0.1 * height, 0.1 * height);
  image(pic2, 0.75*width, 0.6*height, 0.4 * height, 0.4 * height); //right image
  image(Arrow, 0.05*width, 0.6*height, 0.1 * height, 0.4 * height);

  image(title, 0.5*width, 0.1*height, 0.8 * height, 0.175* height);
  textSize(48);
  number = calculateNumber(number);

  drawType(width*0.25, height*0.9, number + " x " + object);
}

// fun facts about objects
void drawFact(String celObj) {
  String fact = "";
  fill(0, 255, 0);
  if (celObj.equals("Sun")) {
    fact = "You can fit 1.3 million Earths in the Sun (volumetrically)";
  } else if (celObj.equals("UY Scuti")) {
    fact = "It is the largest star and can fit 3.69 billion Suns\n (volumetrically)";
  } else if (celObj.equals("Red Blood Cell")) {
    fact = "There are around 25 trillion RBCs in a human body";
  } else if (celObj.equals("Football Field")) {
    fact = "54000 people can fit in a football field";
  } else if (celObj.equals("TON 618")) {
    fact = "It is the largest black hole known to us.\n It is a little larger than our solar system.";
  } else if (celObj.equals("Distance between Proxima Centauri and the Sun")) {
    fact = "It is the closest star to the Sun";
  } else if (celObj.equals("Omega Centauri")) {
    fact = "It is the largest globular cluster of stars in the Milky Way";
  } else if (celObj.equals("Bootes Void")) {
    fact = "It is also referred to as the Great Nothing.\nIt contains almost no galaxies.";
  } else if (celObj.equals("Laniakea Supercluster")) {
    fact = "It is made up of many galaxy groups.\n It is the supercluster home to The Milky Way.";
  } else if (celObj.equals("Sloan Great Wall")) {
    fact = "It is a giant wall of galaxies, and \npossibly the largest structure in the universe.";
  } else if (celObj.equals("Observable Universe")) {
    fact = "The universe is expanding so fast that the light\nfrom beyond this point doesn't reach us.";
  } else if (celObj.equals("Universe")) {
    fact = "Who knows how much further the universe can go?";
  } else if (celObj.equals("Atom")) {
    fact = "A grain of sand has 2x10^19 atoms in it.\nThat is still lesser than the number of stars in the sky.";
  } else if (celObj.equals("Thickness of a Paper")) {
    fact = "Perhaps an ant can get a paper cut?";
  } else if (celObj.equals("Mouse")) {
    fact = "Did you know that mice can fit through a hole\nthe size of a dime?";
  } else if (celObj.equals("Human")) {
    fact = "If the every human was stacked on top of each\nother, we could reach the moon 30 times.";
  } else if (celObj.equals("Grand Canyon")) {
    fact = "You would need around 1200 humans stacked on \ntop of eachother to climb out of the Grand Canyon.";
  }
  text(fact, width*0.75, height*0.35);
}

//Number data to display from Arduino
String calculateNumber(String numb) {
  int no = int(numb);
  String ans = "";
  if ((no==0) || (no==1) || (no==2) || (no==3) || (no==4)) {
    ans = str(int(pow(10, no)));
  } else if (no < 0) {
    float ans1 = pow(10, no);
    ans = str(ans1);
  } else {
    ans = "10^" + numb;
  }
  return ans;
}

void drawType(float x, float y, String val1) {
  fill(255);
  text(val1, x, y);
}

PImage findImage(String obj) {
  for ( int i = 0; i < objects.length; i++ ) {
    if (obj.equals( objects[i] ) ) {
      return img_objects[i];
    }
  }
  return Uranus;
}

// Do math to find the closest object in dictionary to the calculated size
float calculateObject(float numI, String object, FloatDict uni, float[] diam) {
  float num2 = uni.get(object);
  float calcNum = numI * num2;
  int j = -1;
  //Finding nearest value
  for (int i = 0; i < 27; i = i+1) {
    if (calcNum>diam[i]) {
      j = i;
    }
  }

  int finalIndex;
  if (j==-1) {
    finalIndex = 0;
  } else if (j==27) {
    finalIndex = 27;
  } else {
    if ((diam[j+1] - calcNum)<(calcNum-diam[j])) {
      finalIndex = j+1;
    } else {
      finalIndex = j;
    }
  }
  return diam[finalIndex];
}