[P-Comp] Final Project – Shy Guy

OVERVIEW

Meet Shy Guy. He’s a puppet able to perform a simple emotional arch in collaboration with a human scene-partner. Using an infrared sensor, a micro-servo, and Arduino, Shy Guy acts out three emotional states in reaction to the proximity of his human’s hand to his sensor: Fear, Curiosity, and Affection. 

Fear

Shy Guy’s default state. When Shy Guy is in a state of Fear, he will back away from anything that approaches him and return to his starting position as it moves away.

Fear is activated when an object––usually a hand––is within 300mm of him. If the object is outside of that range, Shy Guy will not respond.

Curiosity

When Shy Guy is in a state of Curiosity, he will slowly and incrementally approach the hand in front of him until he reaches his starting position. 

Curiosity is activated when a hand is held between 100mm and 120mm from him, aka within the Happy Zone. If the hand moves outside of the Happy Zone, Shy Guy will revert to a state of Fear.

However, if the hand stays in the Happy Zone long enough for Shy Guy to return fully to his starting position, he goes into a state of…

Affection

When Shy Guy is in a state of Affection, he will follow the hand forward if it moves forward, and back if it moves back but only as far as its starting position, thus allowing himself to be pet. 

Affection can only be activated by going through all the previous states. Shy Guy will remain in a state of Affection unless the hand moves beyond the maximum distance of 300mm. 

How It Works 

Hardware:

  • Sharp IR GP2Y0A41SK0F infrared sensor
  • TowerPro Micro Servo 99 SG90
  • Arduino Uno
  • breadboard

Circuit:

  • IR Sensor is attached to PIN A0, 5V, and GROUND
  • Micro Servo is attached to PIN 9, 5V, and GROUND

NOTE: I went through several experiments with powering the micro servo separately with a battery as  well as using a standard servo powered separately by a battery. The internet advised me that it’s best practice to power your servos separately in order to avoid complications. I found that the standard servo was too unwieldy and that powering the servo using the microcontroller had no discernible effect. 

ANOTHER NOTE: One piece of feedback from Tom Igoe after my in-class presentation was to use a decoupling capacitor on both the sensor and servo. 

Libraries Used:

  • Servo.h – To control the servo motor; included automatically with Arduino
  • SharpDistSensor.h – To turn the readings from the IR sensor into numbers a human can use; this isn’t a necessity strictly speaking as you can do your own math like this kind person did
  • QuickStats.h – For smoothing; a library developed to help do math on arrays of numbers

Shy Guy’s position is mapped to the distance of the hand from the sensor. More specifically, the angle to which Shy Guy’s servo moves is mapped to the distance the hand is from the IR sensor. 

Even more specifically, the angle to which the servo moves is mapped to the median distance of 5 readings of the sensor in a process called Smoothing which I will go into further detail about below. (For now, check out Arduino’s very helpful explanation of the principle here.)

The details of that mapping and the maths that are applied to the movement of the servo in order to evoke different emotions change according to which state Shy Guy is in. Fear does not look like curiosity does not look like affection… in life or in robotics.

Here is a link to the full code with comments. But let’s break it down piece by piece below.

Laying the Foundation with Global Variables

In the code below, I include the Servo library, instantiate my servo object, and define a starting angle, a minimum angle, and a maximum angle. The starting angle provides me with a consistent reference point to better control my servo and the minimum and maximum angles were generally recommended by the internet to cut down on jitteriness since servos tend to behave weirdly at their extremes. All angles are in degrees. 

#include <Servo.h>
Servo myServo;   

int servoAngle;
int startingAngle = 40;
int minAngle = 15;
int maxAngle = 165;

Here I include a library built specifically for my Sharp IR sensor to help with my efforts to smooth out the servo’s movements. The parameters of the sensor object are the pin it’s attached to as well as the number of sensor readings it will take a median of before writing the distance, aka the median filter window size. I also define maximum and minimum thresholds for both the total range of the sensor and the aformentioned Happy Zone where Shy Guy can be coaxed out of fear. All distances are in millimeters.

#include <SharpDistSensor.h>
const byte sensorPin = A0;
const byte medianFilterWindowSize = 5;
SharpDistSensor sensor(sensorPin, medianFilterWindowSize);

int minDist = 40; 
int maxDist = 300;
int minHappyZoneDist = 100;
int maxHappyZoneDist = 120;

I suspect this may have been pointless but, as I was experimenting along multiple fronts in my quest for a believably responsive servo, I also included a library that will do math to an array, in this case, finding the median. That is, I may be finding the median of the sensor readings twice.

#include <QuickStats.h>
QuickStats stats; 

const int numOfReadings = 5;
float readings[numOfReadings];   
float medianDist;               
int readingsIndex = 0;  

The final piece of the global puzzle was defining variables to help me keep track of each state. This was the start of my heartache. Apparently I did this the hard way, but it works so here we go:

boolean handIsInRange = false;    
boolean shyGuyIsCurious = false;    
boolean shyGuyIsAffectionate = false;
unsigned long happyZoneCounter = 0;  
int handIsInHappyZone = 0;

Set Up

The set up looks pretty much the same as any other Arduino set up, attaching the servo to a digital pin and starting Serial so I can see what’s going on in my sketch. There are two big additions:

  1. The sensor library requires I specify which model I’m using in order to correctly calibrate. 
  2. I needed to define the array of sensor readings from which the median would be found. To do this I created a for() loop that would loop once to create the first index of the array. I’m actually not sure why it’s necessary to do a for() loop here but it’s what they did here in the Smoothing tutorial that I was basing this part of the code architecture on and it worked so…
void setup() {

  myServo.attach(9);           
  Serial.begin(9600);
  Serial.println("Reading...");

  sensor.setModel(SharpDistSensor::GP2Y0A41SK0F_5V_DS);

  for (int thisReading = 0; thisReading < numOfReadings; thisReading++) {
    readings[thisReading] = 0;
  }

  myServo.write(startingAngle);

}

I also wrote my servo to a starting angle so that it would always start in the same place. I based that angle on the position of Shy Guy to the edge of his platform. 

Loop

My loop() function is where the states are defined. One important detail that took me a few iterations and an office hours with Tom Igoe to land on is that the distance is being read and calculated constantly outside of any of the states. The difference would be in how those readings are written to the servo. This principle was foundational once I understood it: take as many readings as you can but only write however many you need.

While the sensor was reading constantly, it was also calculating the median of every 5 readings. I chose that number because it created the least amount of lag. Here is the process by which the median distance is calculated. It was adapted from the Smoothing tutorial.

  unsigned int distance = sensor.getDist();

  // Current reading pushed to the current index in the array
  readings[readingsIndex] = distance;

  // Advance to the next position in the array:
  readingsIndex = readingsIndex + 1;

  // If we're at the end of the array...
  if (readingsIndex >= numOfReadings) {
    // ...wrap around to the beginning:
    readingsIndex = 0;
  }

  // Calculate the median distance:
  medianDist = stats.median(readings, numOfReadings); 

  // Map the distance to the servo angle
  servoAngle = map(medianDist, minDist, maxDist, maxAngle, 
  startingAngle);

My loop() function contains a series of mini-loops within which the movement of the servo is defined according to the emotion it is supposed to evoke. Each emotion is its own self-contained loop that uses a boolean switch to change states. Here’s how that goes:

Fear

In this code, the servo will only write if a hand is detected within the maximum allowable range. This also sets off the first trigger: now the “hand is in range.” 

One thing to note is that in all states, I make sure to print everything to Serial so I can make sure the program is running properly. 

  if (medianDist >= minDist && medianDist <= maxDist) {

    handIsInRange = true;

    Serial.println("in range"); 
    Serial.print("Distance in MM: ");
    Serial.print(medianDist);
    Serial.print(", ");
    Serial.print(servoAngle);
    Serial.println(" degrees");

    myServo.write(servoAngle);
    delay(50); 

Curiosity

It took me at least a week to figure out what I wanted Curiosity to look like, and about three weeks to wrap my head around the switches that would allow me to switch from a state of Fear to a state of Curiosity.

It is important to note that I did this the hard way. For some reason I couldn’t wrap my head around how to apply Arduino’s built in switch case function so I did this instead. One could view this as a stretched out version of what switch case does––I just coded my own switches. 

So what do my switches look like? The conditional statement at the top is key. I had to literally spell out everything the state of Curiosity was and wasn’t before it would work. Once all those parameters are met, a counter starts.

(NOTE: The counter idea came directly from Dom Barrett during office hours. He suggested that the scale of my program was small enough that it wouldn’t be noticeably affected by the use of an internal clock as opposed to the millis() function. He was right.)

Once that counter reaches 100, a hard-coded set of servo movements are executed to simulate shy curiosity and the second switch is activated: now “Shy Guy is curious.”

THIS PART TOOK ME FOREVER TO FIGURE OUT: I included here the parameter that Shy Guy wasn’t in Affection mode. I found that without it, the Curiosity sequence would repeat for eternity. 

After the hard-coded sequence is completed, the third switch is activated: now “Shy Guy is affectionate.”

    // if hand is in happy zone...   
    if (medianDist >= minHappyZoneDist && medianDist <= 
       maxHappyZoneDist && !shyGuyIsAffectionate) {

      //...start the counter...
      happyZoneCounter++;

      Serial.println(happyZoneCounter); //debugging

      //...if the hand remains in the happy zone until the 
      //counter reaches 100 and Shy Guy is not in AFFECTION...
      if (happyZoneCounter >= 100 && !shyGuyIsAffectionate) {

        shyGuyIsCurious = true;
        Serial.println("curious"); //debugging

        // ...move the servo incrementally.
        // servo makes quick small move
        servoAngle = servoAngle - random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // wait 2 sec
        delay(2000);

        // servo makes another quick small move
        servoAngle = servoAngle - random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // wait
        delay(500);

        // servo makes another quick small move
        servoAngle = servoAngle - random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // wait
        delay(1000);

        // servo makes another quick small move
        servoAngle = servoAngle - random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // servo makes another quick small move
        servoAngle = servoAngle - random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // wait
        delay(3000);

        // servo makes another quick small move
        servoAngle = servoAngle + random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // wait
        delay(3000);

        // servo makes another quick small move
        servoAngle = servoAngle - random(2, 20);
        myServo.write(servoAngle);

        Serial.print("Distance in MM: ");
        Serial.print(medianDist);
        Serial.print(", ");
        Serial.print(servoAngle);
        Serial.println(" degrees");

        // wait
        delay(2000);

        // servo makes a slower move
        for (servoAngle = servoAngle; servoAngle >= 
        startingAngle; servoAngle -= 1) {
          myServo.write(servoAngle);

          Serial.print("Distance in MM: ");
          Serial.print(medianDist);
          Serial.print(", ");
          Serial.print(servoAngle);
          Serial.println(" degrees");

          delay(50);
        }

        Serial.println("starting angle"); //debugging

        shyGuyIsAffectionate = true;

      }
    }
    // if hand moves outside of happy zone...
    else {
      //...servo resets to FEAR mode
      happyZoneCounter = 0;
      shyGuyIsCurious = false;
      Serial.println("afraid"); //debugging
    }

Another important note: if you moved your hand during the hard-coded Curiosity sequence, nothing would happen. Because of the built-in delays, the sensors weren’t reading. I would like to go back and fix that. 

That said, the else() statement was still essential because it defined Fear as Shy Guy’s default state.

Affection

    // once Shy Guy gets to his starting angle, he interacts with AFFECTION,
    // and stays in that mode until the hand leaves the range
    while (servoAngle < startingAngle && shyGuyIsAffectionate) {
      
      // Get distance from sensor
      distance = sensor.getDist();

      // Assign current distance to the current index in the array of readings
      readings[readingsIndex] = distance;

      // Advance to the next position in the array:
      readingsIndex = readingsIndex + 1;

      // If we're at the end of the array...
      if (readingsIndex >= numOfReadings) {
        // ...wrap around to the beginning:
        readingsIndex = 0;
      }

      // Calculate the median distance:
      medianDist = stats.median(readings, numOfReadings); // Median filter from QuickStats.h line 25

      // follow hand forward only
      servoAngle = map(medianDist, minDist, maxDist, startingAngle - 10, 15);

      myServo.write(servoAngle);

      Serial.println("love"); //debugging

      Serial.print("Distance in MM: ");
      Serial.print(medianDist);
      Serial.print(", ");
      Serial.print(servoAngle);
      Serial.println(" degrees");
    }
  }
  // if hand moves outside of range, servo resets to starting angle
  else {
    myServo.write(startingAngle);
    shyGuyIsAffectionate = false;

    //    Serial.println("...goodbye.");
  }

What It Looks Like

Rough Draft:

Final Iteration:

The jitteriness in the final iteration presented in class was not intentional. I attribute it to mechanical issues: I didn’t take into account the way the servo’s movements shake the base in which it is embedded. In the paper version, I hadn’t securely built Shy Guy’s base at all so it shook along with the servo, absorbing the shakes. Looking at the video above, you can clearly see it swaying. 

While the shakiness actually ads to Shy Guy’s character, I would like to be able to control it. Right now, you miss some of the smaller, more timid movements he makes in Curiosity. I will try to counterbalance the horn of the servo with more weight and see how it goes.

Lessons and Conclusions

I am slow as molasses going uphill in January, as my mother would say. It seems to take me going through a heckuva lot of ideation and experiments before landing on a solid idea. This isn’t alway the case––sometimes a fully formed vision appears in my head and I set about creating it––but this time I definitely spent a good two weeks not being sure what I was making. When I did finally set myself down and rapidly prototype just so I could start experimenting with movement, I wound up designing the thing exactly as I ended up using it. The lesson I takeaway from this for me personally is that things happen when you make. It doesn’t have to be perfect or even completely thought out. Just build it or code it and see what happens. 


Comments

Leave a Reply