Max and Sam's Useful Box

From CHS Sigma
Jump to: navigation, search

Overview

The finished product.

Authors

Maxwell Patek and Sam Rimm-Kaufman, 2015

Design

Concepts and Planning

Unwired box core without the shell

At first, we wanted to make our box a perfect cube, because that tends to look the cleanest and means we will have a nice finished product. However, we also hate assembling the box design involving screwing nuts and bolts on every edge, so Instead we are making our box in the shape of a cylinder using 6in diameter PVC pipe as the shell.

Cylindrical Shape

We decided on the cylindrical shape because it had never been done before and allows for a really cool effect when taking the box apart. When making this shape, we decided to make a "core" and a "shell" that would fit together in order to make a complete box. The shell was made of 6 inch PVC pipe, painted black with a few added screws on the sides and a black acrylic bottom. The shell was made up of 3 layers of acrylic, the top layer, the middle layer, and the bottom-most layer.

Top Layer

The top layer (diameter 160mm) holds a LCD screen, a potentiometer, a blue LED, two switches (one for PID control and one for power) and mounting holes for a motor mount. The motor mount is mounted on the bottom of the black, acrylic sheet, and is a 3D modeled and printed part that holds a motor and has mounting holes for the photogate. The top panel also contains four mounting holes for the locking mechanism. See the section "locking mechanism" for more details.

Middle Layer

This layer is a 140mm circle cut out of light blue, transparent acrylic. This layer has mounting holes for a breadboard using the breadboard mount designed and 3D printed for this project. This layer also hold 7 blue LEDs (Wired in parallel, receiving 6V, controlled via transitor) and has two large holes on either side of the breadboard for the wires. This layer is separated from the top layer with 3 2" standoffs.

Bottom Layer

Like the middle layer, this layer is a 140mm hole cut from light blue, transparent plastic. This layer has mounting holes for an arduino (when 1/2" standoffs are used) and mounting holes for a slightly modified 9V battery holder. The battery holder was modified to fit 2 9V batteries wired in parallel, stacked on top of each other. This positioning provides a more current (still just at 9V) to run all of the LEDs and the motor. The bottom layer, like the middle layer, has 7 blue LEDs wired in parallel that receive 6V.

Locking Mechanism

The locking mechanism is a 150mm ring that is 5mm thick. The circle has four holes equally spaced along the ring. The bottom of the ring has cutouts that are T-shaped that allow the "lockers" some room to move. There are also locking bumps that restrict the movement of the mechanism, but at this point they are too large and not necessary so they are not used. The locking mechanism works using a friction fit. The shell has four equally spaced holes that have #4-40 screws inside. The nuts on the inside are exactly the right size to slide into the T-shaped cutout of the ring, and allow the ring (and the connected top) to spin around, sliding along with the nuts inside the ring. Pulling the shell out involves turning the ring to a point where it can escape the nut, and then removing the ring from the shell. because the ring is connected to the top layer, and the rest of the core is connected to the top layer, removing the ring mechanism can remove the entire core, allowing for easy wiring or code access. As always, a picture is worth a thousand words. See the pictures for more details (and watch our super cool promo to see the locking mechanism in action).

Assembly

Assembly of the box is relatively easy. First, the arduino, the LEDs and the battery pack are mounted to the bottom layer and the standoffs are screwed into the holes provided on that layer. The second layer can be placed and secured onto those standoffs. On this layer, the breadboard can then be mounted to the middle layer ajnd the LEDs mounted around it. The three standoffs for this layer have holes so that they can meet perfectly with the top layer. The top layer must be assembled off of the standoffs and then screwed on. First, the LCD is put into the top, then the LED, potentiometer and the two switches are mounted in their respective holes. From the bottom of the top layer, the motor mount is attached. From the bottom, the locking ring can be screwed on and then tested. The entire top then can be mounted on the standoffs and the core is complete. The shell has four holes drilled into the sides. Screws are mounted in those holes and then nuts are placed on those screws. An acrylic bottom is then glued onto the pipe and the shell is completed. Relatively speaking, the shell requires little assembly. See pictures of the final box for clues on how it was assembled.

Problems

Core Too Tall with LEDs

Once the LEDs were added, the core was a few millimeters too tall to fit in the already made shell because of the added space for the LED casing. First we considered replacing the bottom of the shell with a few layers of "spacer" acrylic, but then realized that it was a waste of material. Instead, we reprinted three of the standoffs in order to make them a bit shorter. This solution, shortening the bottom standoffs by half an inch (1.5 inch instead of 2 inch), made it so that when the LEDs were mounted, the entire core fit perfectly in the shell.

LCD Up-Side-Down

Originally, the design included the LCD screen upside down. There was no easy way to fix this in code because in the LCD library, a user can only create up to 8 of their own characters. Instead, we removed the LCD (an arduous process), and flipped it around 180 degrees in order to make the LCD right. This was a pain but it was worth it to avoid some gross code stuffs.

Lock Wear

The way the locking mechanism was designed means that over time the lock will wear out. With excessive rubbing of the nut and the slight bending that the box goes through every time the core is taken out, the locking mechanism will eventually break down. The expected life for the locking mechanism is around 1000 unlocks, so assuming the box is not stretched beyond that, the current locking mechanism works perfectly. In future models, the locking mechanism would be strengthened or perhaps made out of a higher strength material such as specially cast acrylic. For the scope of this project however, using the 3D printed model is sufficient.

Photos and Renders

Programming

PIDGIF.gif

Green is input speed. Red is actual speed. Blue is adjusted power.

The internal lighting looks really cool in the dark!
A mysterious glow emanates from just below the IO.

The Arduino code consists of two distinct modes, PID closed-loop and non-PID open-loop. Every 1/2 second, the the code detaches the interrupter so that it can do calculations. First it check to see what mode it's in by doing an digital read on PIDpin. If PID mode is off, the arduino doesn't bother calculating errors or adjusted and it only maps, writes and prints. Otherwise, the Arduino is in PID mode and it calculates error. Error and derivative of error don't need global variables because each calculation is unique to that half-second; however, the integral of error requires a pre-declared global variable because each new calculation of integral relies on the previous value. After error calculations, the Arduino sums all the errors by "adjustedSpeed = inputSpeed - 0.00*error - 0.1*dError - 0.45*errorSum;" The coefficients of the weighted sum were simply found by trial and error. The adjusted speed is also constrained to 0 to 255 so that the automatic modular nature of analogWrite doesn't write unexpected speeds (writing 256 is the same as writing 0). Finally the Arduino combines error and input speed to calculate an nice speed for the aesthetic lighting pulse.

Problems

Abrupt Lighting Pulse Rate Changes

Originally, because the Arduino only updates actual speed and adjusted speed every half-second, the pulse rate of the aesthetic lighting also only updated every half-second. This meant that whenever the user drastically changed the input speed and the error skyrocketed, the lighting would abruptly start pulsing very quickly and abruptly slow down as the motor approached it's target speed. This was not very smooth or natural, so an extra layer was added between error and pulse rate. There is a instead of pulse rate being determined by error, a new variable, rateTarget, is determined by error. The Arduino uses the number of 50 milliseconds between now and the next total update to calculate a dRate, which is the amount the rate will have to change every 50 milliseconds in order to reach the target rate in exactly 500 milliseconds. This was, the pulse rate of the aesthetic lighting changes linearly over time rather than jumping sporadically.

Arduino Code

 #include <Wire.h>
#include <LCD.h>
#include <LiquidCrystal_I2C.h>
 
LiquidCrystal_I2C lcd(0x3F,2,1,0,4,5,6,7); 
 
// Pins:
int potPin = 1;
int PIDpin = 12;
int bottomLEDpin = 11;
int topLEDpin = 10;
int PIDledPin = 9;
int motorPin = 6;
int photoInputPin = 2;
 
// Passes:
volatile int passes = 0;
volatile unsigned long long int totalPasses = 0;
 
// PID Stuff:
int inputSpeed = 120;
int prevError = 0;
int errorSum = 0;
 
// LED Stuff:
float rate = 0;
float rateTarget = 0;
float dRate = 0;
float count = 0;
 
 
void setup() {
  	attachInterrupt(photoInputPin-2, photoOn, RISING);
  	lcd.begin (16,2); // for 16 x 2 LCD module
  	Serial.begin(9600);
	lcd.setBacklightPin(3,POSITIVE);
	lcd.setBacklight(HIGH);
	pinMode(photoInputPin, INPUT);
	pinMode(motorPin, OUTPUT);
	pinMode(PIDpin, INPUT);
	lcd.setCursor(0,0);
	lcd.print("Hello World");
	delay(1000);
}
 
 
void loop() {
	if(millis() % 50 == 0) 
		rate += dRate; // this makes the led pulse rate change slowly over time
	count += rate; // rate is basically delta-x in sampling a trig function, mimicking true frequency.
	int topLEDstate = map(sin(count)*1000, -1000, 1000, 0, 255); // multiply by 1000 to keep map() from truncating
	int bottomLEDstate = map(cos(count)*1000, -1000, 1000, 0, 255);// otherwise trig values will 'round' to 0 and -1
 
	analogWrite(topLEDpin, topLEDstate);  // this is just for aesthetic lighting
	analogWrite(bottomLEDpin, bottomLEDstate);
 
	if (millis()%500 == 0) {  // every half a second
		detachInterrupt(photoInputPin-2); // do this 1st to avoid disruptive interrupts
 
//-- PID Mode --//
		if (digitalRead(PIDpin) == HIGH) { 
			Serial.print(1); // so processing knows that we're in PID mode
			Serial.print(","); // comma separated values
 
        // get input
			int val = analogRead(potPin);
			inputSpeed = map(val, 0, 1023, 0, 200); // 200 is not true maximum. Leaves a 55 buffer for PID control
 
        // calculate actual speed
			int actualSpeed = map(passes, 0, 432, 0, 255); // 432 was measured to be the maximum interrupts per half-second
 
        // calculate error and calculus
			int error = actualSpeed - inputSpeed;
			int dError = error - prevError;
			if (millis() > 2000) // give motor a second to spin up before summing error
				errorSum += error;
			errorSum = constrain(errorSum, -450, 450); // no point in letting errorSum get so large that it takes forever to back down if it can't have any more effect on the adjustedSpeed.
 
        // print stuff
			 Serial.print(inputSpeed); // for processing
			lcd.setCursor(0,0);
			lcd.print("User Input: ");
			lcd.print(map(inputSpeed,0, 200, 0, 100)); // map to 100 for percent
			lcd.print("    "); // clear the rest of the line
			Serial.print(","); // comma separated values for processing
			Serial.print(actualSpeed); // for processing
			lcd.setCursor(0,1);
			lcd.print("Actual Speed:");
			lcd.print(map(actualSpeed,0, 200, 0, 100)); // map to 100 for percent
			lcd.print("    "); // clear the rest of the line
 
        // adjust speed by PID
			int adjustedSpeed = inputSpeed - 0.00*error - 0.1*dError - 0.45*errorSum;
			adjustedSpeed = constrain(adjustedSpeed, 0, 255);
 
        // determine aesthetic lighting pulse frequency
			rate = rateTarget; // if dRate went wrong, reset to where rate should've gone
			rateTarget = (float)abs(error*2)/10000 + (float)inputSpeed/200000; // very small because rate increments count every loop() (very often)
			dRate = (rateTarget - rate)/10; // how much rate should change every 50 milliseconds to reach the target
 
			Serial.println(); // for processing
 
			analogWrite(motorPin, adjustedSpeed);
			prevError = error;
		}
 
//-- Open Loop Mode --//
		else {
			Serial.print(0); // so processing knows we're in Open Loop mode
			Serial.print(","); // CSV
 
			int actualSpeed = map(passes, 0, 432, 0, 255); // 432 was measured to be the maximum interrupts per half-second
 
			int val = analogRead(potPin);
 
			lcd.setCursor(0, 0);
			lcd.print("User Input: ");
			lcd.print(map(val, 0, 1023, 0, 100));
			lcd.print("   ");
			lcd.setCursor(0, 1);
			lcd.println("                ");
			lcd.setCursor(0,1);
			lcd.print("Actual Speed:");
			lcd.print(map(actualSpeed,0, 200, 0, 100));
			lcd.print("    ");
 
			inputSpeed = map(val, 0, 1023, 0, 255);
 
			Serial.print(inputSpeed); // for processing
			Serial.print(","); // CSV
			Serial.println(actualSpeed); // for processing
 
			analogWrite(motorPin, inputSpeed);
 
			rate = rateTarget; // if dRate went wrong, reset to where rate should've gone
			rateTarget = (float)actualSpeed/20000.0; // very small because rate increments count every loop() (very often)
			dRate = (rateTarget - rate)/10; // how much rate should change every 50 milliseconds to reach the target
		}
		passes = 0; // Reset passes for new actual speed measurement. totalPasses is not reset.
		attachInterrupt(photoInputPin-2, photoOn, RISING); // re-attach to continue calculating actual speed
		delay(1); //  to keep millis()%500 == 0 from being true multiple times per millisecond
	}
}
 
//-- Interrupt Function --//
 void photoOn() {
 	passes++;
 	totalPasses++;
        // blink IO mounted LED once every 32 revolutions (2 passes per rev)
 	if (totalPasses%64 == 0)
 		digitalWrite(PIDledPin, HIGH);
 	if (totalPasses%64 == 32)
 		digitalWrite(PIDledPin, LOW);
 }

Processing Code

The graphical processing involved in the graphs is handled by an object detailed here.

import processing.serial.*;
Serial myPort;
 
String myStr;
boolean PID;
String[] strArray;
int input;
int actual;
int error, prevError = 0, dError, errorSum;
int adjusted;
Graph inputHistory;
Graph actualHistory;
Graph adjustedHistory;
Graph errorHistory;
Graph dErrorHistory;
Graph errorSumHistory;
int halfSecs = 0;
 
void setup() {
 
  size(1000, 350);
  noSmooth();
  frame.setResizable(true);
  background(0,0,0);
  println("Available serial ports:");
  println(Serial.list());
  myPort = new Serial(this, Serial.list()[1], 9600);
  inputHistory = new Graph(width, height, 0, 255, 50, color(0,255,0));
  actualHistory =  new Graph(width, height, 0, 255, 50, color(255,0,0));
  adjustedHistory =  new Graph(width, height, 0, 255, 50, color(0,0,255));
  errorHistory =  new Graph(width, height, 0, 255, 50, color(255,0,0));
  dErrorHistory =  new Graph(width, height, 0, 255, 50, color(255,0,0));
  errorSumHistory =  new Graph(width, height, 0, 255, 50, color(255,0,0));
  delay(1000);
}
 
void draw()
{
  delay(500);
  if ( myPort.available() > 0) 
  { 
    myStr = myPort.readStringUntil('\n');
    if(myStr != null) 
    {
      myStr = myStr.trim();
      strArray = split(myStr, ",");
      //printArray(strArray);
      PID = Integer.parseInt(strArray[0]) == 1;
      input = (int)Float.parseFloat(strArray[1]);
      actual = Integer.parseInt(strArray[2]);
      error = actual - input;
      dError = error - prevError;
      if (millis() > 2000 && PID)
        errorSum += error;
      errorSum = constrain(errorSum, -450, 450);
      adjusted = (int)(input - 0.00*error - 1.1*dError - 0.4*errorSum);
      halfSecs++;
        inputHistory.setSize(width, height);
          actualHistory.setSize(width, height);
            adjustedHistory.setSize(width, height);
              errorHistory.setSize(width, height);
                dErrorHistory.setSize(width, height);
                  errorSumHistory.setSize(width, height);
      inputHistory.append(input);
      actualHistory.append(actual);
      adjustedHistory.append(adjusted);
      errorHistory.append(error);
      dErrorHistory.append(dError);
      errorSumHistory.append(errorSum);
        background(0,0,0);
      inputHistory.disp();
      actualHistory.disp();
      adjustedHistory.disp();
      //print(inputHistory.get(inputHistory.size() - 1));
 
      prevError = error;
    }
  } 
}
 
 
public class Graph extends IntList {
 
  int myWidth, myHeight, yMin, yMax, spacing;
  color rgb;
 
  public Graph(int screenWidth, int screenHeight, int yMin, int yMax, int spacing, color rgb) {
    super();
    setSize(screenWidth, screenHeight);
    this.yMax = yMin;
    this.yMin = yMax;
    setSpacing(spacing);
    setColor(rgb);
  }
 
  public void disp() {
    if (size() > 0) {
      int x, y;
      noFill();
      stroke(rgb);
      beginShape();
      x = myWidth - (size()-1)*spacing;
      y = (int)map( get(0) , yMin, yMax, 0, myHeight);
      curveVertex(x, y);
      for (int i = size()-1; i >= 0; i--) {
        x = myWidth - i*spacing;
        y = (int)map( get(size()-i-1) , yMin, yMax, 0, myHeight);
        curveVertex(x, y);
      }
      x = myWidth;
      y = (int)map( get(size() - 1) , yMin, yMax, 0, myHeight);
      curveVertex(x, y);
      endShape();
    }
  }
 
  @Override
  public void append(int a) {
    super.append(a);
 
    if (size() > 2000)
      super.remove(0);
  }
 
  public void setSize(int screenWidth, int screenHeight) {
    myWidth = screenWidth;
    myHeight = screenHeight;}
 
  public void setSpacing(int spacing) {
    this.spacing = spacing;}
 
  public void setColor(color rgb) {
    this.rgb = rgb;}
}

Wiring

Note the bent modified female-female header connecting the LCD and its backpack.

Subsystems

Motor Feedback System

This subsystem consists of the motor, it's transistor, and the photo-interrupter. The 6V motor is fed off the 6V voltage regulator side of the breadboard, while the photo interrupter is on the opposite side for easy access to 5V straight from the Arduino, as the photo-interrupter does not require much current. This system was tested before assembly of the box; click here to see a gallery of that test.

The Interface

The Interface consists of the LCD, it's backpack, a potentiometer, the on/off switch, the PID switch, and an LED to indicate that the photo-interrupter is working. The entire IO system works on the 5V side of the breadboard because it does not draw much current. The global power switch is directly connected to the 9V battery in the middle, and the side is split between the Arduino 9V input and the breadboard, where 9V goes into the voltage regulator to provide 6V.

Internal Lighting

The internal lighting wiring system was suprising tricky. To provide enough voltage for the 14 interior LEDs, they had to be wired in parallel, meaning that they draw more current. Because of this current requirement, we decided to run the interior LEDs off the 6V from the voltage regulator and the battery instead of thorugh the Arduino like the IO LED. This means that each loop of interior LEDs needed its own transistor to convert arduinio 5V PWM to 6V for the LED's. Using Ohms law and resistance in parallel circuits equations, we calculated that given 6V, each row of 7 LEDs in parallel would need only a single 220ohm resistor to keep the LED from frying. Note that blue LEDs have a diferent resistance and voltage requirement than green and red LEDs, so different calculations should be made for similar LED setups in different color.

Cool Conveniences

Using Only 9V Batteries

Rather than using a bulky and awkward split 6xAA better pack (or worse, both a 6xAA pack and a 9V battery... this), we decided to only use 9V batteries and to use a 6V voltage regulator to get the proper voltage for the motor. Originally, we planned and modeled for only one 9V battery, but this ended up not supplying enough voltage. However, it was very simple to just "piggyback" another 9V battery on top of the first. Wiring the two batteries in parallel boosts the available current. We think that using only 9V batteries saved us from a lot of complicated modelling, mounting, and wiring, and we recommend this solution to anyone using a 6V motor.

LCD Backpack Header

Another great idea which saved us a lot of trouble was using a modified female-female header to connect the LCD to its backpack. We neither wanted to take up space with a long ribbon-cable nor solder the backpack directly to the LCD, so our solution was to solder the male ends of two male-female headers together, essentially creating a female-female header. We wrapped the exposed male headers in electrical tape to prevent short circuits, and we bent the female-female header in the middle to create a 90 degree angle for easy mounting.

Problems

15 Blue LEDs

Firstly, the sigma lab didn't even have 15 blue LEDs. We had to order some just for this project. Also, when originally wired in series, they required such an incredible amount of voltage to light up, that we switched our original plans to wire the LEDs in parallel. After a few tries, we connected all of the bottom 7 LEDs in parallel and then connected the middle 7 in parallel. This configuration draws more current but requires less voltage and is within the current range of two 9V batteries in parallel.

Potentiometer Omelette

Sam fried the potentiometer. Like 4 or 5 times. Turns out those guys are really sensitive. When soldering to the potentiometer, the soldering iron was too hot and touched the delicate components inside. This fried the resistor every time. Heat shrinking the wires didn't help with this problem either. When the potentiometer was fried it would read seemingly random values that fluctuated more based on the acceleration of the potentiometer than on the position of the dial. Key point here: when soldering sensitive electronic components, move fast. And when that doesn't work, move faster. And if that still doesn't work, let Max solder it.

Photos and Diagrams

Miscellaneous

Hand courtesy of Mary Stelow! Check out her and Chris's PID Motor project.
Core resting on the brim of the shell.

Motor & Phototransistor Wiring Gallery

Click here to see a gallery of how our motor and photo-interrupter were wired.