Instructables

SADbot: the Seasonally Affected Drawing robot

FeaturedContest Winner
SADbot was created in collaboration with Ben Leduc-Mills for the window gallery at the Eyebeam Art + Technology Center. The main idea was to use solar energy to power a drawing machine that could interact with people outside the window through light sensors. You can re-create this project to install in your own window at home.

SADbot gets its name from the source of its power: the sun. Since the motors are powered from a battery charged through a solar panel, they will run only if it has been sunny enough to store solar energy in the battery. When the battery runs low and the SADbot motors stop running, SADbot appears sad because it has to wait for the sun to come out before it can keep drawing.

Everything you need to know is in this Instructable, but if you've never used an Arduino or worked with photocells and motors, you might want some background material.  This the last project in my book, Making Things Move: DIY Mechanisms for Inventors, Hobbyists, and Artists, so all the background material can be found in there.

SADbot has already been listed as one of The 10 Robots That Rocked in 2010 and one of The 10 Coolest Kickstarter Projects of 2010.  SADbot also got some love from Adafruit Industries, IEEE Spectrum, True/Slant, Robot Living, and gizmowatch.  And when Ben and I showed it at Maker Faire NY in September 2010, our booth won a blue Editor's Choice ribbon!  Hopefully you'll make one and comment about it - the world needs more robot art.

Also see the Flickr set for higher-res images.


 
Remove these adsRemove these ads by Signing Up

Step 1: Shopping List

Electronics
  • Multimeter
  • Arduino with USB cable and AC adapter
  • Soldering iron, stand, and solder
  • Three small breadboards (like All Electronics PB-400)
  • Jumper wires (like SparkFun PRT-00124)
  • Hook-up wire: red , black, and white (SparkFun PRT-08023, PRT-08022, and PRT-08026)
  • Two stepper motors (SparkFun ROB-09238)
  • Two EasyDrivers (SparkFun ROB-09402)
  • Male header pins (SparkFun PRT-00116)
  • Four photocells (1KΩ – 10kΩ: SparkFun SEN-09088)
  • Four 1KΩ resistors (SparkFun COM-08980)
  • Photocell (10KΩ – 100KΩ, Digi-Key PDV-P9007-ND used here) and resistor (10KΩ, like SparkFun COM-08374 used here).  Note: You can also use a 1KΩ – 10kΩ photocell (SparkFun SEN-09088). In that case, you should use a 1KΩ resistor (SparkFun COM-08980) to get the best response.
  • Benchtop power supply for testing
  • 12V 5Ah SLA battery (PS-1250 F1 from Microbattery.com, www.microbattery.com)
  • Solar charge controller (SKU 06-1024 from Silicon Solar, www.siliconsolar.com)
  • 12V 7W solar battery charger panel (Silicon Solar SKU 9358)
Hardware
  • Large plywood or other wooden board to use for canvas (around 3 ft × 2 ft will work well)
  • Eight M3 screws, 20mm length, (McMaster 92095A185)
  • Eight M3 lock washers (McMaster 92148A150)
  • One pack M3 washers (McMaster 91116A120)
  • Drill (either portable or drill press) and drill bits: 3/8 in, 1/8 in
  • Diagonal cutters (like SparkFun TOL-08794)
  • Two pulleys:  Download the model file for free from Thingiverse to 3D print them yourself or buy them custom made at from my Shapeways shop.  Any pulley that fits on a 5mm motor shaft will also work.
  • Spring clamp (like McMaster 5107A1) that will hold the marker
  • Black (or any color) marker
  • Monofilament fishing line
  • Large white paper


Step 2: Prepare the motors

Prepare the two stepper motors by soldering 4-pin male headers onto the 4 wires of each motor.  This will make it easy to plug them into a breadboard in the next step.  Yes, the order of the colors does matter: see the note on the image.


Step 3: Prepare the EasyDrivers

Picture of Prepare the EasyDrivers
Solder male headers onto the EasyDriver.  A set of four lines
up with the four motor holes, a set of three lines up with the GND/STEP/DIR
holes, and a set of two lines up with the GND/M+ holes.

It’s easiest to solder if you stick the long ends of the headers into
the breadboard, slide the EasyDriver on the short ends, and then heat up the
little solder pads around the holes while you add solder. Be careful not to
add so much solder that the pins connect to each other!

Step 4: Test the motors

Picture of Test the motors
Create two EasyDriver/breadboard/motor assemblies.  Plug the stepper motor header into the breadboard in line with the motor pins on the EasyDriver. The red and green wires should be next to A on theEasyDriver, and the blue and yellow wires next to B.  Use a benchtop power supply to get 12V power and ground to the GND and M+ pins on each of the EasyDrivers. Set them up to interface with the Arduino as follows:

For the left motor:
• Arduino GND to GND on left EasyDriver
• Arduino pin 11 goes to DIR
• Arduino pin 12 goes to STEP

For the right motor:
• Arduino GND (one of the two GND pins left) to GND on right EasyDriver
• Arduino pin 6 goes to DIR
• Arduino pin 7 goes to STEP

Make sure the two stepper motors work. Type in the following code, verify it,
and upload it to the Arduino.

/*
Driving two stepper motors with an Arduino through
Sparkfun's EasyDriver v4.3
By Ben Leduc-Mills and Dustyn Roberts
Created: 2010.06
*/
#include Stepper.h  //import stepper library

#define STEPS 200 // 360/1.8 (step angle) = 200 steps/revolution
//declare new stepper objects from stepper library (one per motor)
Stepper right_motor(STEPS, 6, 7); //6=DIR, 7=STEP
Stepper left_motor(STEPS, 11, 12); //11=DIR, 12=STEP

void setup() {
//set motor speeds (in RPM)
right_motor.setSpeed(200);
left_motor.setSpeed(200);
}

void loop() {
//step each motor every time through the loop
right_motor.step(10);
left_motor.step(10);
delay(10); //gives the motor a chance to get to new step
}

If the code works, your motors should just start spinning slowly. Attach some tape
flags to the motor shafts as shown to help indicate what’s going on.

Step 5: Let there be photocells

Get out the third breadboard and wire up the photocells. Each photocell should have one leg connected to the power column and one leg connected to the ground column through a resistor.

The leg going to ground should also go to one of the ANALOG IN pins on the
Arduino. From left to right, connect the ground legs of the photocells to pins
0, 1, 2, and 3 on the Arduino, which correspond with up, down, left, and
right in the code, respectively.

Jump power and ground to the breadboard from the Arduino GND and
5V pins.

Step 6: Motors and photocells

Picture of Motors and photocells
Now we will try some code that uses the photocells to move the stepper
motors. Type in the following code, verify it, and upload it to the Arduino.  Once that is done, try covering up the photocells one at a time.
• When you cover the up photocell, the left motor should turn counterclockwise
and the right motor should turn clockwise.
• When you cover the down photocell, the left motor should turn clockwise
and the right motor should turn counterclockwise.
• When you cover the left photocell, both motors should turn
counterclockwise.
• When you cover the right photocell, both motors should turn clockwise.

/*
Using photocells to drive two stepper motors
with an Arduino through Sparkfun's EasyDriver v4.3
CC-GNU GPL by Ben Leduc-Mills and Dustyn Roberts
Created: 2010.06
*/

#include //import stepper library
#define STEPS 200 // 360/1.8 (step angle) = 200 steps/revolution

//declare new stepper objects from stepper library (one per motor)
Stepper right_motor(STEPS, 6, 7); //6=DIR, 7=STEP
Stepper left_motor(STEPS, 11, 12); //11=DIR, 12=STEP

int distance; // how far motors should go
int lowest; // variable to store lowest photocell value
int i; // for looping
// variables for 4 photocell values
int photo_up;
int photo_down;
int photo_left;
int photo_right;

void setup() {
Serial.begin(9600); //start serial printout so we can see stuff
// set motor speeds (in RPM)
right_motor.setSpeed(200);
left_motor.setSpeed(200);
}

void loop() {
//read and print all photocell values from analog pins 0-3
photo_up = analogRead(0);
Serial.print("up");
Serial.println(photo_up);

photo_down = analogRead(1);
Serial.print("down");
Serial.println(photo_down);

photo_left = analogRead(2);
Serial.print("left");
Serial.println(photo_left);

photo_right = analogRead(3);
Serial.print("right");
Serial.println(photo_right);

delay(1000); //give me time to read them in the monitor

//store photocell values in an array
int photoValues[]= {photo_up, photo_down, photo_left, photo_right};
lowest = 9999; //set this higher than possible photocell values

//loop to find lowest photocell value
for(i = 0; i < 4; i++) //4 = number of photocells
{
Serial.println(photoValues[i]); //prints out photoValue array
//assign actual photocell value to "lowest" variable if it's lower
//than whatever "lowest" is set to (starts at 9999)
if (lowest >= photoValues[i] ) {
lowest = photoValues[i];
}

//print it out to confirm that the lowest value is being selected
Serial.print("lowest:");
Serial.println(lowest);
delay(1000); //wait one second before looping so we can read the values
}//end for

distance = lowest; //set travel distance variable = lowest value
//find the sensor that matched the lowest, go that direction
//see below for what the up, down, left, right functions do
if (lowest == photoValues[0]) {
up( distance );
}
else if (lowest == photoValues[1]) {
down( distance );
}
else if (lowest == photoValues[2]) {
left( distance );
}
else if (lowest == photoValues[3]) {
right( distance );
}
}//end loop

/*
Here are the directional functions. Loop size = distance.
Positive step numbers are clockwise, negative counterclockwise
*/

void up(int distance) {
for( i = 0; i < distance; i++){
right_motor.step(10);
left_motor.step(-10);
}
}

void down(int distance) {
for( i = 0; i < distance; i++){
right_motor.step(-10);
left_motor.step(10);
}
}

void left(int distance) {
for( i = 0; i < distance; i++){
right_motor.step(-10);
left_motor.step(-10);
}
}

void right(int distance) {
for( i = 0; i < distance; i++){
right_motor.step(10);
left_motor.step(10);
}
}

Step 7: Mount the motors and pulleys

Picture of Mount the motors and pulleys
4687953889_a6562f511f.jpg
4682779081_f1e44bfe0d.jpg
4683410916_0829d8e121.jpg
Get out your plywood board and put on your safety glasses. The stepper
motor data sheet from SparkFun indicates that the motor mounting holes are
31mm apart in a square, with the shaft at the center. Make pencil marks up
on the corners of your board where you want the motor shafts to go, and
then measure and make marks at each corner of a 31mm square centered on
that motor shaft mark. Plan your motor mount so one motor is in each of the top corners of your canvas.

Use the 1/8 in drill bit to drill out the clearance holes for the M3 screws the
motor will mount with. Use the 3/8 in drill bit to drill out the center hole. Remove the screws that came with the motor from the shaft side so we can use those holes as mounting holes with the longer screws.  Now mount your motors with the M3 screws, lock washers, and washers creating a sandwich with your canvas board. You may need fewer washers, depending on the board thickness you’re using.

Once both motors are mounted, confirm the circuit still works as intended and
none of the wires in your circuit have come loose. Press the pulleys onto each
motor shaft.

Step 8: Go solar

Picture of Go solar
Now let’s get SADbot running off the battery charged by the solar panels
through the charge controller (the Arduino should plug into the wall through
the AC adapter). First, simplify the wiring by choosing one EasyDriver board as
the power hub (see picture). Designate one side of the breadboard as the hub, and then use small jumper wires to jump the GND and M+ pins of
the EasyDriver to this hub. Connect the power and ground columns of this
hub to GND and M+ on the other EasyDriver breadboard with long lengths of
hook-up wire.

Step 9: Solar charger setup

Picture of Solar charger setup
4687957649_1db54b0409.jpg
4688590340_61de949d66.jpg
4688589432_47f3aa99dd.jpg
Cut some more hook-up wire and connect this power hub to the two far-left
screw terminals on the charge controller.

Solder hook-up wire to the battery terminals (red for positive and black for
negative). Use the screw terminals in the center of the charge controller to
hold the stripped ends of these wires.

Cut off the RC plug that comes on the solar panel wires. Separate the two
wires and strip the insulation off the ends to expose about 1/4 in of wire. To
figure out which one is positive and which one is negative, use your multimeter.
The black lead should be in the COM connection, and the red lead in the
voltage-measuring connection, just as for testing batteries. Now touch the red lead to one of the solar panel wires and the black lead to the other. If the reading on your multimeter is positive, you guessed right. If not, you guessed wrong.

Do this with both solar panels and squish both negative wires in the charge
controller on the remaining negative terminal. Each positive wire gets its own
screw terminal on the far right of the charge controller.

Step 10: Staging and testing

This step will change a little bit depending on your setup.  You can set SADbot up in a window, a table, or anywhere you want really.  These pictures show SADbot being setup inside a window with the photocells pressed up against the inside of the glass, but you can keep the photocells on the breadboard if you want a desk-sized controller.  Just cut and route wires accordingly to accommodate your installation.

In the setup below, the Arduino was mounted between the two EasyDriver breadboards.

Once you've double (and triple) checked all your wiring, turn on the power supply to your Arduino (just plug it in if you're using an AC adapter) and turn the solar charger on.  If you’ve done everything right, your motors should be moving!  The battery comes charged so you'll be fine for a few hours, but you’ll want to put your solar panels in a sunny spot so the battery can charge as the motors use up the initial charge.

Step 11: Make it interesting

Now let’s make SADbot draw something interesting. Cut two approximately 5
ft lengths of fishing line and tie each on through the hole in the center of the
pulleys. Tie the other ends of the line to a spring clamp and clamp onto a
marker. Mount the white paper on your canvas board with tape or clamps.

Let’s tell the stepper motors to draw in random directions by default, and then
to behave as they did before when someone is covering a photocell. This will
create a drawing like the ones SADbot made in Eyebeam’s window gallery.   Enjoy! Play with the photocells so you can interact with SADbot and make interesting drawings.

/*
SADbot v.03
SADbot will draw a random distance in random direction until a
photocell is blocked. When SADbot detects a photocell has been blocked, it
will draw towards it. Stepper motors are driven through Sparkfun's
EasyDriver v4.3
CC-GNU GPL by Ben Leduc-Mills and Dustyn Roberts
Created: 2010.06
*/

#include //import stepper library
#define STEPS 200 // 360/1.8 (step angle) = 200 steps/revolution
//declare new stepper objects from stepper library (one per motor)
Stepper right_motor(STEPS, 6, 7); //6=DIR, 7=STEP
Stepper left_motor(STEPS, 11, 12); //11=DIR, 12=STEP
int distance; // how far motors should go
int lowest; // to store lowest photocell value
int i; // for looping

// variables for 4 photocells
int photo_up;
int photo_down;
int photo_left;
int photo_right;

// Set canvas size. 1000 steps is roughly .4 inch
#define CANVASWIDTH 32000
#define CANVASHEIGHT 20000

//total distance for bounds checking
//SADbot starts at center (canvaswidth/2 and canvasheight/2)
float totalWidth = CANVASWIDTH /2;
float totalHeight = CANVASHEIGHT /2;
int randomDirection;
int randomDistance;

void setup() {
Serial.begin(9600); //start serial printout so we can see stuff
// set motor speed (in RPM)
right_motor.setSpeed(200);
left_motor.setSpeed(200);
//use random seed to get better random numbers
//*set to an analog pin that you're not using
randomSeed(analogRead(4));
}// end setup

void loop() {
//read and print all sensor values from analog pins 0-3
photo_up = analogRead(0);
Serial.print("up");
Serial.println(photo_up);

photo_down = analogRead(1);
Serial.print("down");
Serial.println(photo_down);

photo_left = analogRead(2);
Serial.print("left");
Serial.println(photo_left);

photo_right = analogRead(3);
Serial.print("right");
Serial.println(photo_right);

delay(1000); //give me time to read them in the monitor

//before drawing, check our totalHeight and totalWidth
Serial.print("totalHeight:");
Serial.println(totalHeight);
Serial.print("totaWidth:");
Serial.println(totalWidth);
delay(1000); //give me time to read them in the monitor

//store photocell values in an array
int photoValues[]= {photo_up, photo_down, photo_left, photo_right};
lowest = 9999; //set this higher than possible photocell values
//loop to find lowest photocell value

for(i = 0; i < 4; i++) //4 = number of sensors
{
Serial.println(photoValues[i]); //prints out photoValue array
//assign actual photocell value to "lowest" variable if it's lower
//than whatever "lowest" is set to (starts at 9999)
if (lowest >= photoValues[i] ) {
lowest = photoValues[i];
}
//print it out to confirm that the lowest value is being selected
Serial.print("lowest:");
Serial.println(lowest);
delay(1000); //wait one second before looping so we can read the values
}//end for

distance = lowest; //set travel distance = lowest value

//if lowest value indicates a covered photocell, draw towards lowest
//change this threshold depending on lighting
if (lowest < 550 )
{
//find the sensor that matched the lowest, go that direction,
//but only if SADbot is within the bounds of the canvas
if ((lowest == photoValues[0]) && ((totalHeight + distance) <
CANVASHEIGHT))
{
up( distance );
totalHeight += distance; //increment totalHeight variable
}
else if ((lowest == photoValues[1]) && ((totalHeight - distance) > 0))
{
down( distance );
totalHeight -= distance; //decrement totalHeight variable
}
else if ((lowest == photoValues[2]) && ((totalWidth - distance) > 0))
{
left( distance );
totalWidth -= distance; //decrement totalWidth variable
}
else if ((lowest == photoValues[3]) && ((totalWidth + distance) <
CANVASWIDTH))
{
right( distance );
totalWidth += distance; //increment totalWidth variable
}
}//end if

//otherwise, no one is covering any sensors, draw according to random
else
{
//pick random number 1 through 9 to map to direction
randomDirection = random(1, 9);
Serial.print("random direction:");
Serial.println(randomDirection);

//pick random number 1 through 200 to map to distance
randomDistance = random(1, 200);
Serial.print("random distance:");
Serial.println(randomDistance);
//directions for any randomDirection value generated
switch (randomDirection)
{
case 1: //go up
if((totalHeight + randomDistance) < CANVASHEIGHT)
{
up(randomDistance);
totalHeight += randomDistance;
}
break;

case 2: //go down
if((totalHeight - randomDistance) > 0)
{
down(randomDistance);
totalHeight -= randomDistance;
}
break;

case 3: //go left
if((totalWidth - randomDistance) > 0)
{
left(randomDistance);
totalWidth -= randomDistance;
}
break;

case 4: //go right
if((totalWidth + randomDistance) < CANVASWIDTH)
{
right(randomDistance);
totalWidth += randomDistance;
}
break;

case 5: //go upRight
if(((totalWidth + randomDistance) < CANVASWIDTH) && ((totalHeight
+ randomDistance) < CANVASHEIGHT))
{
upRight(randomDistance);
totalWidth += randomDistance;
totalHeight += randomDistance;
}
break;

case 6: //go upLeft
if(((totalWidth - randomDistance) > 0) && ((totalHeight +
randomDistance) < CANVASHEIGHT))
{
upLeft(randomDistance);
totalWidth -= randomDistance;
totalHeight += randomDistance;
}
break;

case 7: //go downRight
if(((totalWidth + randomDistance) < CANVASWIDTH) && ((totalHeight - randomDistance) > 0))
{
downRight(randomDistance);
totalWidth += randomDistance;
totalHeight -= randomDistance;
}
break;

case 8: //go downLeft
if(((totalWidth - randomDistance) > 0) && ((totalHeight - randomDistance) > 0))
{
downLeft(randomDistance);
totalWidth -= randomDistance;
totalHeight -= randomDistance;
}
break;

default: //just in case
left(0);
} //end switch
} //end else
} //end loop()

/*
Here are the directional functions. Loop size = distance.
Positive step numbers are clockwise, negative counterclockwise
*/

void up(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(1);
left_motor.step(-1);
}
}

void down(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(-1);
left_motor.step(1);
}
}

void left(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(-1);
left_motor.step(-1);
}
}

void right(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(1);
left_motor.step(1);
}
}

void upRight(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(2);
left_motor.step(-.2);
}
}

void upLeft(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(.2);
left_motor.step(-2);
}
}

void downRight(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(-.2);
left_motor.step(2);
}
}

void downLeft(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(-2);
left_motor.step(.2);
}
}
mikegbeck2 years ago
I have a question about the direction functions. You have given a non-integer value for the .step() and the arduino standard requires an integer.
i.e.
void downLeft(int distance)
{
for( i = 0; i < distance; i++) {
right_motor.step(-2);
left_motor.step(.2);//non-integer value given here
}
}

Is this an undocumented feature of the .step() function?
dustynrobots (author)  mikegbeck2 years ago
Nice catch - I think that may be a typo. I believe the . is a typo in both instances, not sure how that got in there.
Acutally, what I'm really wondering is that the upLeft, upRight, downLeft, downRight functions supposed to produce diagonal lines?

Thanks.
Euphy3 years ago
This is a brilliant how-to for making a drawbot like this, clear instructions. I really could have done with this a few months ago!
dustynrobots (author)  Euphy3 years ago
Thankssssss post a picture/link if you make one!
jam BD3 years ago
Solar panels =D Nice touch.
laxap3 years ago
Good job!

How about TSP art to draw non-random pictures?
dustynrobots (author)  laxap3 years ago
That would be excellent! If you come up with a mod to the code you should post it!
The TSP calculation would be way too much resource hungry for an Arduino.
Cooperation with a PC seems necessary.

The Concorde solver can produce coordinate files.
martzsam3 years ago
Cool Bot! Great Ible!

Off topic;

How many people after reading the name immediately thought of Marvin the Robot when you saw this title?

Sad Robot...
dustynrobots (author)  martzsam3 years ago
haha I love a good hitchhiker's reference, thanks
bertus52x113 years ago
Impressive!
I am not ready to try a project like this but it looks a great combination of electronic and art. Really nice work.