Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Thomas Burns
Published

Breathing life into an Amazon Echo device!

Amazon's Alexa is boring, so we gave it some animatronic eyes and a CRT mouth. Now my kids won't leave it alone!

IntermediateFull instructions provided27,640

Things used in this project

Hardware components

5" B&W CRT television
The cosmetic condition of the device doesn't matter, as we will be removing it from the chassis anyway. Just make sure there's a nice, bright raster on the screen. The size of the TV's board will largely define the footprint of your project, as it is most likely the largest part of the build.
×1
Echo Dot
Amazon Alexa Echo Dot
×1
M2, M3, M4 screw assortment w/nuts and washers
I used steel screws with a hex head (not countersunk). These are a great thing to have in your kit for all sorts of projects.
×1
TDA7297 audio amplifier
×2
Adafruit 16-Channel 12-bit PWM/Servo Driver
I used Adafruit's model for this because I know they make good products, but any similar servo board will do.
×1
Audio Adapter, 3.5 mm Stereo Plug to 2x Sockets
Audio Adapter, 3.5 mm Stereo Plug to 2x Sockets
×1
Audio / Video Cable Assembly, 3.5mm Slim Stereo Plug to 3.5mm Slim Stereo Plug
Audio / Video Cable Assembly, 3.5mm Slim Stereo Plug to 3.5mm Slim Stereo Plug
Any old 3.5mm stereo cables will do for this. The shorter they are, they more easy they will be to manage. I ended up buying the connectors and making my own cables to the length I needed.
×2
Arduino Mega 2560
Arduino Mega 2560
You can use any microcontroller, as long as it can sense up to 5V on the sense pin (an Arduino Nano can only accept up to 3V)
×1
Person Sensor
Useful Sensors Person Sensor
×1
4.5mm clear acrylic, laser cut to designs for the chassis
×1
brass standoffs, assortment of M3 and M4 sizes
×1
5V 8A switch-mode power supply
This should accept mains voltage for your region.
×1

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Wire Stripper & Cutter, 32-20 AWG / 0.05-0.5mm² Solid & Stranded Wires
Wire Stripper & Cutter, 32-20 AWG / 0.05-0.5mm² Solid & Stranded Wires
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

Plans for chassis: base

I recommend using 4.5mm acrylic for these cuts

Plans for chassis: Level 1

I recommend using 4.5mm acrylic for these cuts

Plans for chassis: Level 2

I recommend using 4.5mm acrylic for these cuts

Plans for chassis: Level 3

I recommend using 4.5mm acrylic for these cuts

Schematics

ALEXATRON schematics

Code

Arduino code

Arduino
#include <Wire.h>
#include <person_sensor.h>
#include <Adafruit_PWMServoDriver.h>

// How long to wait between reading the Person Sensor. The sensor can be read as
// frequently as you like, but the results only change at about 5FPS, so
// waiting for 200ms is reasonable.
const int32_t SAMPLE_DELAY_MS = 200;// default 200;

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

#define SERVOMIN 140  // this is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX 520  // this is the 'maximum' pulse length count (out of 4096)

int box_center_x;
int box_center_y;
int prev_center_x = 1;

unsigned long currentBlinkMillis = 0;
unsigned long previousBlinkMillis = 0;  // will store last time number changed
const long blinkInterval = random(5000,1200);   // period at which to change number (in milliseconds)
int blinkNumber = random(1,2);

unsigned long currentSleepMillis = 0;
unsigned long previousSleepMillis = 0;
const long sleepInterval = 20000; 

unsigned long loop_number = 0;

enum States {
  DORMANT,   // eyelids are closed, eyes are not moving
  AWAKE.     // eyelids are open, eyes are tracking faces
};

States State;

// our servo # counter
uint8_t servonum = 0;

int xval;
int yval;
int trimval;
int current_xval;
int prev_xval = 512;

int lexpulse;
int rexpulse;

int leypulse;
int reypulse;

int uplidpulse;
int lolidpulse;
int altuplidpulse;
int altlolidpulse;

int sensorValue = 0;
int outputValue = 0;
int switchval = 0;
int loopNumber = 0;

int ledPin = 3;
int sensorPin = A6;
unsigned long awakeTime = 30000;


void setup() {

  // You need to make sure you call Wire.begin() in setup, or the I2C access
  // below will fail.
  Wire.begin();
  pinMode(2, INPUT);
  Serial.begin(9600);
  pwm.begin();
  pwm.setPWMFreq(60);  // Analog servos run at ~60 Hz updates
  State = DORMANT;
  
  trimval = 500;   // this sets how wide the eyelids are positioned (higher number = wider eyes)
  trimval = map(trimval, 320, 580, -40, 40);
  uplidpulse = map(yval, 0, 1023, 400, 280);
  uplidpulse -= (trimval - 40);
  uplidpulse = constrain(uplidpulse, 280, 400);
  altuplidpulse = 680 - uplidpulse;

  lolidpulse = map(yval, 0, 1023, 410, 280);
  lolidpulse += (trimval / 2);
  lolidpulse = constrain(lolidpulse, 280, 400);
  altlolidpulse = 680 - lolidpulse;
  
  // Power up sequence to test eyes
  closeEyes();   
  delay(3000);
  wakeup();
  delay(1000);
  driftoff();
  delay(1000);

  Serial.println("setup complete");
  
}

void loop() {

  
  int sensorValue = analogRead(sensorPin); // reads for voltage change in Echo LEDs. LED's come on when Alexa hears wake word
  float voltage = sensorValue * (5 / 1023.0);
  Serial.println(voltage);

  if (voltage > 1.5 && State == DORMANT) {  // if asleep and no trigger word, Alexatron stays asleep
    State = DORMANT;
    Serial.println("DORMANT");
  } else if (voltage <= 1.5 && State == DORMANT) {  // if asleep and trigger word, Alexatron wakes up
    wakeup();

  } else if (voltage > 1.5 && State == AWAKE) {  // if awake and no trigger word, Alexatron stays awake for 10 seconds
    if(millis() - awakeTime > 0) {
        awake();
        awakeTime = millis();
    } else {
      driftoff();
    }
  } else if (voltage <= 1.5 && State == AWAKE) {   // if awake and trigger word, Alexatron stays awake
      awake();
    }   
  delay(50);

}

void blink() {   // script to execute one blink
  
  trimval = 550;   // this sets how wide the eyelids are positioned (higher number = wider eyes)
  trimval = map(trimval, 320, 580, -40, 40);
  uplidpulse = map(yval, 0, 1023, 400, 280);
  uplidpulse -= (trimval - 40);
  uplidpulse = constrain(uplidpulse, 280, 400);
  altuplidpulse = 680 - uplidpulse;

  lolidpulse = map(yval, 0, 1023, 410, 280);
  lolidpulse += (trimval / 2);
  lolidpulse = constrain(lolidpulse, 280, 400);
  altlolidpulse = 680 - lolidpulse;
  
  // closes eyelids
  pwm.setPWM(2, 0, 500);
  pwm.setPWM(3, 0, 240);
  pwm.setPWM(4, 0, 240);
  pwm.setPWM(5, 0, 500);

  delay(80);

  // opens eyelids to trimval value  
  pwm.setPWM(2, 0, uplidpulse);
  pwm.setPWM(3, 0, lolidpulse);
  pwm.setPWM(4, 0, altuplidpulse);
  pwm.setPWM(5, 0, altlolidpulse);
}

void awake() {
    
  Serial.println("AWAKE");
  /*   SENSOR TAKES READING   */
  person_sensor_results_t results = {};  // reads sensor
  if (!person_sensor_read(&results)) {
    Serial.println("No person sensor results found on the i2c bus");
    return;
  }

  for (int i = 0; i < results.num_faces; ++i) {
    const person_sensor_face_t* face = &results.faces[i];
    box_center_x = (face->box_left + ((face->box_right)-(face->box_left))/2);  // turns sensor box into x-axis point for eye-contact
    box_center_y = (face->box_bottom + ((face->box_top)-(face->box_bottom))/2);  // turns sensor box into y-axis point for eye-contact
    
    prev_center_x = box_center_x;

    // SERVO POSITIONING
    xval = ((box_center_x * -4) + 1023)*1.2;   // translate sensor information into servo info
    yval = ((box_center_y * -4) + 1023)*1.2;
    
    lexpulse = map(xval, 0, 1023, 220, 440);
    rexpulse = lexpulse;
    leypulse = map(yval, 0, 1023, 250, 500);
    reypulse = map(yval, 0, 1023, 400, 280);

    trimval = 550;   // this sets how wide the eyelids are positioned (higher number = wider eyes)
    trimval = map(trimval, 320, 580, -40, 40);
    uplidpulse = map(yval, 0, 1023, 400, 280);
    uplidpulse -= (trimval - 40);
    uplidpulse = constrain(uplidpulse, 280, 400);
    altuplidpulse = 680 - uplidpulse;

    lolidpulse = map(yval, 0, 1023, 410, 280);
    lolidpulse += (trimval / 2);
    lolidpulse = constrain(lolidpulse, 280, 400);
    altlolidpulse = 680 - lolidpulse;
    pwm.setPWM(0, 0, lexpulse);
    pwm.setPWM(1, 0, leypulse);

    /*  PERIODIC BLINKING  */
    unsigned long currentBlinkMillis = millis(); // store the current time
    if (currentBlinkMillis - previousBlinkMillis >= blinkInterval) { // check if interval has passed
      previousBlinkMillis = currentBlinkMillis;   // save the last time we changed number
      blink();
    }
  }
  delay(SAMPLE_DELAY_MS);
  State = AWAKE;
  
}

void wakeup() {   //  Creature wakes up from sleep (blinks and looks around)
  
    Serial.println("WAKEUP");  
    xval = 500;   // translate sensor information into servo info
    yval = 500;
    
    lexpulse = map(xval, 0, 1023, 220, 440);
    rexpulse = lexpulse;
    leypulse = map(yval, 0, 1023, 250, 500);
    reypulse = map(yval, 0, 1023, 400, 280);

    trimval = 650;   // this sets how wide the eyelids are positioned (higher number = wider eyes)
    trimval = map(trimval, 320, 580, -40, 40);
    uplidpulse = map(yval, 0, 1023, 400, 280);
    uplidpulse -= (trimval - 40);
    uplidpulse = constrain(uplidpulse, 280, 400);
    altuplidpulse = 680 - uplidpulse;

    lolidpulse = map(yval, 0, 1023, 410, 280);
    lolidpulse += (trimval / 2);
    lolidpulse = constrain(lolidpulse, 280, 400);
    altlolidpulse = 680 - lolidpulse;
    pwm.setPWM(0, 0, lexpulse);
    pwm.setPWM(1, 0, leypulse);

  pwm.setPWM(2, 0, uplidpulse);  //  opens eyelids
  pwm.setPWM(3, 0, lolidpulse);
  pwm.setPWM(4, 0, altuplidpulse);
  pwm.setPWM(5, 0, altlolidpulse);

  delay(100);

  blink();
  delay(500);
  blink();
  delay(500);

  pwm.setPWM(0, 0, 450); // eyes glance right
  delay(800);
  pwm.setPWM(0, 0, 220); // eyes glance left
  delay(1000);
  pwm.setPWM(0, 0, 330); // eyes look forward
  delay(1000);

  blink();
  delay(200);
  blink();

  State = AWAKE;
}



void driftoff() {   //  Slowly closes creature's eyes + eyeballs roll up
  
  pwm.setPWM(0, 0, 330);  // centeres eyes on x-axis
  // blink();
  //blink();
  for (int i = 1; i <= 50; i++) { // closes eyes slowly
    const double a = i / 50.0;
    pwm.setPWM(2, 0, uplidpulse + (400-uplidpulse) * (a)); 
    pwm.setPWM(3, 0, lolidpulse + (240-lolidpulse) * (a));
    pwm.setPWM(4, 0, altuplidpulse + (240-altuplidpulse) * (a));
    pwm.setPWM(5, 0, altlolidpulse + (400-altlolidpulse) * (a));
    pwm.setPWM(1, 0, 400 + (i)); // eyes roll up
    delay(40);
  }
  pwm.setPWM(2, 0, 460); // closes eyelids completely
  pwm.setPWM(3, 0, 240);
  pwm.setPWM(4, 0, 240);
  pwm.setPWM(5, 0, 460);
  delay(1000);
  State = DORMANT;
}




void closeEyes() {
  Serial.println("closeEyes start");
    xval = 500;   // translate sensor information into servo info
    yval = 500;
    
    lexpulse = map(xval, 0, 1023, 220, 440);
    rexpulse = lexpulse;
    leypulse = map(yval, 0, 1023, 250, 500);
    reypulse = map(yval, 0, 1023, 400, 280);

    trimval = 650;   // this sets how wide the eyelids are positioned (higher number = wider eyes)
    trimval = map(trimval, 320, 580, -40, 40);
    uplidpulse = map(yval, 0, 1023, 400, 280);
    uplidpulse -= (trimval - 40);
    uplidpulse = constrain(uplidpulse, 280, 400);
    altuplidpulse = 680 - uplidpulse;

    lolidpulse = map(yval, 0, 1023, 410, 280);
    lolidpulse += (trimval / 2);
    lolidpulse = constrain(lolidpulse, 280, 400);
    altlolidpulse = 680 - lolidpulse;
    pwm.setPWM(0, 0, lexpulse);
    pwm.setPWM(1, 0, leypulse);
    pwm.setPWM(2, 0, 460);
    pwm.setPWM(3, 0, 240);
    pwm.setPWM(4, 0, 240);
    pwm.setPWM(5, 0, 460);  


   delay(100);
     Serial.println("closeEyes finish");
}

// you can use this function if you'd like to set the pulse length in seconds
// e.g. setServoPulse(0, 0.001) is a ~1 millisecond pulse width. its not precise!
void setServoPulse(uint8_t n, double pulse) {
  double pulselength;

  pulselength = 1000000;  // 1,000,000 us per second
  pulselength /= 60;      // 60 Hz
  Serial.print(pulselength);
  Serial.println(" us per period");
  pulselength /= 4096;  // 12 bits of resolution
  Serial.print(pulselength);
  Serial.println(" us per bit");
  pulse *= 1000000;  // convert to us
  pulse /= pulselength;
  Serial.println(pulse);
}


  

Credits

Thomas Burns

Thomas Burns

4 projects • 62 followers
Father, maker, and lover of analog electronics. Check out more of my builds on my YouTube channel!

Comments