Animatronic Door for Dr. Seuss Week

This past week was Dr. Seuss week at the school my wife teaches at. One of the ways they were celebrating and getting kids engaged in Dr. Seuss was having a door decorating contest. Each classroom would decorate their door in a Dr. Seuss theme, complete with some visuals and a quote. My wife asked for some help, and the video below shows what we came up with.

Pretty cool, right? She didn’t end up winning, but I thought I’d do a post on how we went ahead and did this. The entire project was completed over 2 evenings.

The basic design

My wife teaches elementary music, so we thought we’d stick with a music theme. Problem is, there’s oddly not a ton in the Seuss world that screams music. In fact, we could really only come up with the scene from How the Grinch Stole Christmas where the Grinch is describing all the musical instruments they use, and how much he hates the noise. A fun irony for a music teacher, right? So we decided to run with it. We started with the simple sketch below, figuring we’d need a grinch head, with some thought bubbles around him showing the different images.

Pretty fun…. but what if we could get it to move like he did in the movie?

Construction

Since I had an Arduino and some parts that were lying around not doing anything, I suggested maybe we try to make this thing move, or light up, or do something. We settled on making the eyes rotate and the mallets move back and forth if someone stepped in front of the door, similar to how the animated movie worked.

With that idea and our sketch, we knew we’d need 4 servos to move all of that, a way to determine if someone was standing in front, and make sure the items were all separate so they could move. We started with creating the assets for the door. The thought bubbles were printing and mounted onto foam core, though we decided not to have them interactive at all. Since the Grinch would be interactive, we needed his face, each eye, and each mallet to be complete separate pieces. The eyes also had to fit behind the face so when rotated, the pupils would turn correctly in the non-circular sockets. Most of this was created by handing using foam, and then mounted onto foam core with some marker coloring. The Grinch face was traced using a marker and projector (with a frame grab from the movie) to accurately draw and cut his face.

Assembling the electronics

The Arduino piece of this was actually pretty straight forward. The movement would all be driven by 4 180 degree servos. Each servo has 3 wires for power, ground, and data, and is wired similar to below.

So all we had to do was use a small breadboard (didn’t want to solder any of this) so tied together all the related ground / power wires, and then the servos ended up using 4 separate data points on the Arduino. For the distance sensing, I decided to use an Ultrasonic Ping sensor. Maybe not as useful in this case as a motion sensor, but we already had one on hand. With this sensor, the wiring is still incredibly basic, consisting of a power, ground, and then 2 separate data wires (triggering the ping and receiving the response).

With everything wired up, I’d used a total of 5 data ports on the Arduino. The more pressing issue was trying to hide as much of this as possible, so it wasn’t a door covered in wires. We decided the only place we could really do this was right behind the Grinch’s face. We also wanted to hold a battery pack so we could run this off of AAs (this didn’t end up working, but it was built in). So to hold most of this, I went ahead in Shapr3D construction a simple custom box that would hold the Arduino and a battery pack, while having holes that would allow wires to pass out the back, which would be covered in paper, hiding the wires. This was then 3d printed, and the wired, with the Grinch face taped to the face of it.

As you can see, due to time, it ended up turning into a bit of a messy rat’s nest of wires and tape. But hey, it totally worked. The tape helped keep all the wires within the box (and going out the back under the blue paper) and keep the Grinch face secured to the box. Each of the eyes and mallet were secured to the servo using small screws. The foam core allowed us to use pretty cheap lightweight servos that helped keep the weight and cost down on the project.

The Code

With the entire thing build, wired, and mounted, all that was left was getting the code to run. Like much of everything else on the project, the code is sloppy and could be refactored, but it worked. I’ve included the code below, but the main thing it needed to do was sense the distance in front of it, and if something got closer than a specified threshold (roughly 8 feet), it would trigger the entire animation, which in turn drove each of the 4 servos.

The only tricky part here was I wanted to make sure we could run each of the 4 servos completely independent of each other, which meant I couldn’t use the typical Arduino delay() function, because it would halt all processes, not just the specific animation or servo. So, without a proper threading library on the Arduino, each process I wanted to run had to keep track of the last time it was run, and an interval in order to know if it was time to run again. All of this allowed it to have the servos run at different intervals and speeds, and allow the animation to start, run for 30 seconds, and then kick off until it was triggered again.

#include <Servo.h>
#include <NewPing.h>

/*************************************** 
 * Distance
 ***************************************/

#define TRIGGER_PIN 9 // Arduino pin tied to trigger pin on the ultrasonic sensor.
#define ECHO_PIN 8 // Arduino pin tied to echo pin on the ultrasonic sensor.
#define MAX_DISTANCE 160 // Maximum distance we want to ping for (in centimeters). Maximum sensor distance is rated at 400-500cm.
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE);
unsigned long distancePreviousMillis = 0;
int distanceInterval = 50;
int distance = 0;
int previousDistance;



unsigned long animationPreviousMillis = 0;
int animationInterval = 30;
bool animationShouldRun = false;

/*************************************** 
 * Arms
 ***************************************/
int armMaxRange = 115;
int armMinRange = 65;

int armInterval = 15;
int armSpeed = 1.5;

// Left
Servo leftArmServo;
int leftArmPos = 90;
bool leftArmDirection = false;
unsigned long leftArmPreviousMillis = 0;

// Right
Servo rightArmServo;
int rightArmPos = 90;
bool rightArmDirection = true;
unsigned long rightArmPreviousMillis = 0;



/*************************************** 
 * Eyes
 ***************************************/

int eyeMaxRange = 179;
int eyeMinRange = 1;
int eyeInterval = 15;
int eyeSpeed = 5;

// Left
Servo leftEyeServo;
int leftEyePos = 90;
bool leftEyeDirection = false;
unsigned long leftEyePreviousMillis = 0;

// Right
Servo rightEyeServo;
int rightEyePos = 90;
bool rightEyeDirection = true;
unsigned long rightEyePreviousMillis = 0;



void setup() {
 // Attach the servos
 leftArmServo.attach(13);
 leftArmServo.write(leftArmPos);

rightArmServo.attach(12);
 rightArmServo.write(rightArmPos);

leftEyeServo.attach(11);
 leftEyeServo.write(leftEyePos);

rightEyeServo.attach(10);
 rightEyeServo.write(rightEyePos);

Serial.begin(9600);
}

void loop() {

if ((unsigned long)(millis() - distancePreviousMillis) >= distanceInterval) {
 distancePreviousMillis = millis();
 previousDistance = distance;
 distance = getDistance();
 int delta = abs(distance - previousDistance);
 Serial.println(distance);

if (distance <= MAX_DISTANCE && distance > 2 && delta < 6) {
 animationShouldRun = true;
 }
 else {
 if ((unsigned long)(millis() - animationPreviousMillis) >= animationInterval * 1000) {
 animationPreviousMillis = millis();
 animationShouldRun = false;
 }
 }
 }

if (animationShouldRun) {
 runAnimation();
 }
}

int getDistance() { 
 return sonar.ping_cm();
}

void runAnimation() {
 // Animate the arm
 animateLArm();
 animateRArm();

// Animate the eye
 animateLEye();
 animateREye();
}

// Animate the arm
void animateLArm() {
 
 if ((unsigned long)(millis() - leftArmPreviousMillis) >= armInterval) {
 leftArmPreviousMillis = millis();

// One direction
 if (leftArmDirection) {
 if (leftArmPos < armMaxRange) {
 leftArmServo.write(leftArmPos);
 leftArmPos += armSpeed;
 }
 else {
 leftArmDirection = !leftArmDirection;
 }
 }

// Other direction
 else {
 if (leftArmPos > armMinRange) {
 leftArmServo.write(leftArmPos);
 leftArmPos -= armSpeed;
 }
 else {
 leftArmDirection = !leftArmDirection;
 }
 }
 
 }
}

void animateRArm() {

if ((unsigned long)(millis() - rightArmPreviousMillis) >= armInterval) {
 rightArmPreviousMillis = millis();

// One direction
 if (rightArmDirection) {
 if (rightArmPos < armMaxRange) {
 rightArmServo.write(rightArmPos);
 rightArmPos += armSpeed;
 }
 else {
 rightArmDirection = !rightArmDirection;
 }
 }

// Other direction
 else {
 if (rightArmPos > armMinRange) {
 rightArmServo.write(rightArmPos);
 rightArmPos -= armSpeed;
 }
 else {
 rightArmDirection = !rightArmDirection;
 }
 } 
 }
}

void animateLEye() {
 if ((unsigned long)(millis() - leftEyePreviousMillis) >= eyeInterval) {
 leftEyePreviousMillis = millis();

// One direction
 if (leftEyeDirection) {
 if (leftEyePos < eyeMaxRange) {
 leftEyeServo.write(leftEyePos);
 leftEyePos += eyeSpeed;
 }
 else {
 leftEyeDirection = !leftEyeDirection;
 }
 }

// Other direction
 else {
 if (leftEyePos > eyeMinRange) {
 leftEyeServo.write(leftEyePos);
 leftEyePos -= eyeSpeed;
 }
 else {
 leftEyeDirection = !leftEyeDirection;
 }
 }
 }
}

void animateREye() {
 if ((unsigned long)(millis() - rightEyePreviousMillis) >= eyeInterval) {
 rightEyePreviousMillis = millis();

// One direction
 if (rightEyeDirection) {
 if (rightEyePos < eyeMaxRange) {
 rightEyeServo.write(rightEyePos);
 rightEyePos += eyeSpeed;
 }
 else {
 rightEyeDirection = !rightEyeDirection;
 }
 }

// Other direction
 else {
 if (rightEyePos > eyeMinRange) {
 rightEyeServo.write(rightEyePos);
 rightEyePos -= eyeSpeed;
 }
 else {
 rightEyeDirection = !rightEyeDirection;
 }
 } 
 }
}

Like I said, messy, but it worked. It was also great to be able to change the timing and how far things moved after it was all assembled via software updates.