Tweety Bot

November 2015

Tweety Bot started out an a much more ambitious project.

I would hope so I guess since all he actually does is sit around and Tweet. Anyhow, the original goal was to build a birdy bot that could sit on your arm or shoulder and use a gryo/accel sensor to stay upright.

I worked up some micro servos that would allow at least two different axis of movement and made up a duck looking head for the body. That one was called JABB (Just Another Bird Bot). After messing with JABB for a while and realizing how ugly the body was going to be with a full ProMini hanging onto it and liking the idea of a little bird like robot, I thought about simplifying things. VERY simple in fact.

Tweety’s brain is a very simple ATiny85 like I used in other bots like Buford, Buzz and PuP. This allowed me to get the whole CPU, US sensor, and a light sensor to fit inside it’s little head. After messing with JABB I thought it would be fun to have a simply little tweeting bird bot that was not mobile.

In TweetyBot, a single servo allows his head to pan left and right. A small light sensor is on top of his head to know how bright things are and a yellow LED was stuffed inside his beak to activate along with his tweeting sounds so his beak lights up when he makes sounds. Very simply but kind of entertaining as well.

When TweetyBot first starts up he does a right to left pan looking for the closet object. He then moves to that spot and just starts randomly tweeting and moving his head around. The light sensor is used to “go to sleep” when it’s dark or when his little cover is put over him. Kind of like a real bird as well. There are some “features”, which to most would be “bugs”, in the code where the head doesn’t do a perfect pan on startup which actually adds a little more realisim to his actions.

Construction

Construction is very straight forward. A hacked up 3d printable owl body was used from one of the many on Thingiverse.com. I didn’t want or need the head so I chopped it off of the digital model before printing and hollowed out the back to make room for the neck servo. The servo mounts in the hollow area and the head fits on the servo output.

TweetyBot’s head is from Roxanna77, the owner of http://www.CoolKidsRobots.com, design and it just looked like a perfect bird head to me. You can find it here:  http://www.thingiverse.com/thing:1054142

TweetyBot runs off 5v or so and can be powered by most any supply including a battery pack, solar chargeable pack, or just a USB port if you want.

I wish I would have made him a little more heavy at the base or added some material as it is a little top heavy and unless you have him glued to a base he moves around and eventually finds the edge of the desk. So I guess he IS a little mobile after all.

What Does He Do?

Like most of my creations he really doesn’t do much. Just randomly moves his head and tweets sometimes. I did have to dial down the randomness some as he does get annoying if he is making a racket too often.

He does get annoyed if you get in his face and starts moving his head back and forth to get away from you. Apparently another “feature” is in there somewhere as he occasionlly doesn’t really move his head but just buzzes there for a moment. Not sure what that is but I like it too.

He also quiets down when it gets dark enough or when you put his goodnight cloth (black cloth) over him as he thinks it dark and time to shut up.
Once you take the cloth off he restarts his routine and spins his head around and starts tweeting again.

The Bad Stuff

The bad stuff mostly is my screw up in design. He really only needs three connections to/from his head. The + power, the ground and then the control going back to the neck servo. I thought it’d be simple to just add three pins sticking out his head and hook up to that feeding the plus and ground both to the inside of the head and back to the servo. It does work but it also can fail easily as the head moves back and forth eventually breaking wires.

TweetyBots Future

TweetyBot will get his wires cleaned up and mounted to a better base. Maybe a little wooden platform or something. I’ve already added a USB cable to power him so he can sit somewhere and be powered off a computer or power pack and do his thing. He seems like a reasonbly happy bird.

TweetyBot Resources

If you have some weird desire to build your own here are the modified models:

TweetyBotBody.stl – Modified from an Owl version from ThingiVerse with hollow back

TweetyBotBeak.stl – Print with zero infill to have a hollow beak for LED to shine through

Example Code

I’m sure there are still some bugs and problems here but it’s working for what I want it to do. I think there are some pieces not even used and the chirping parts are from some sample code online elsewhere.



//** TweetyBot - ATTiny85 Version of JABB
//** 
//** v1.00 - 11/1/2015 - Stephen W Nolen / aka Protowrxs

// ATTiny85 Pin Uses
// -----
// 1 Rest (5) |o | 8 Vcc
// 2 Sonar(3) | | 7 (2) LDR light D2/A1
// 3 Speak(4) | | 6 (1) Output to 2N2222 driving pager vib motor
// 4 Gnd | | 5 (0) Head Servo D0
// -----

#include <SoftwareServo.h> 
SoftwareServo headServo; 

int speakerPin = A3;
int headServopin = 0;
int motorPin = 1; // Yes, looks the same as the ADC but that is apparently how it works

int headPosition;
int headCenter = 90;
int headLeft = 130;
int headRight = 50;

// vars used to determine random moves of the head and direction
int headOffset;
int headDirection;

// US sensor used for location and annoying him
byte TRIGGER_PIN = 3;
byte ECHO_PIN = 3;
int uScm;
int closePosition;
int uScmClose;

boolean activeMode; // if we are active or not. Used by light sensor to shutup in the dark or be more active in the light

int lightPin = 1; // LDR Input pin on A1 (ATTiny85 pin 7)
int lightLevel; // LDR Light level as read - gets quite at dark, active in light by changing the random high/low values?

// Timers used for various random activity
long generalTimer1;
long generalTimer2;
long generalTimer3;
long nextHeadMove;

//** H)ead, T)weet, L)owC)hirp and H)ighC)hirp random low/highs to allow light controls
//** eventually we want to increment / decrements these based on the ambient light levels to be more active during bright light
int Hlow = 5000;
int Hhigh = 60000;
int Tlow = 10000;
int Thigh = 20000;
int LClow = 10000;
int LChigh = 20000;
int HClow = 10000;
int HChigh = 20000;

long myDelay;

int currentPhase; // we step through phases for chirps - not sure why, might be best to randomize?

void setup()
{
// This is from an AdaFruit example here: 
//https://learn.adafruit.com/trinket-gemma-servo-control/code
// Set up the interrupt that will refresh the servo for us automagically
OCR0A = 0xAF; // any number is OK
TIMSK |= _BV(OCIE0A); // Turn on the compare interrupt (below!)

//Setup speaker output
pinMode(speakerPin,OUTPUT);
pinMode(motorPin, OUTPUT);

//Psudeo randomness
randomSeed(analogRead(0));

//Setup this for ADC input but I think ADread does it already
pinMode(lightPin,INPUT); //LDR light sensor

//This may not be needed either
pinMode(headServopin, OUTPUT); //This should be done in servo code though

//** Attach the head servo
headServo.attach(headServopin);
// headServo.setMaximumPulse(2200); // do not think we need this as it's working as is

headServo.write(headCenter);

// Tweet, find closest objecgt and then chirp on start up
// Also does similar when the light returns from being dark
tweet(random(2,4),random(1,5)); // startup tune
findCloseObject();
highChirp(random(1,5), random(1,10));

peck(250,200);

generalTimer1 = millis();
generalTimer2 = millis();
generalTimer3 = millis();

activeMode = true;

}

void loop()
{
uScm = ReadSonar();

if (uScm < 10)
{ 
headServo.write(headLeft);
headPosition = headLeft;
tweet(random(1,3),random(1,5)); 
}

if (uScm < 5) 
{
headServo.write(headRight);
headPosition = headRight;
highChirp(random(1,5), random(1,5)); 
}

lightLevel = analogRead(lightPin);

if (activeMode)
{
// if it gets dark then shut up after last chirp
if (lightLevel < 150)
{ 
activeMode = false;
lowChirp(5,5);
}

// if really bright just keep on chirping
if (lightLevel > 900)
{
highChirp(5,5);
}

//If random timer is up then jump to the next phase of randomness
if (millis() > myDelay)
switch (currentPhase)
{
case 0: 
highChirp(random(2,6),random(1,6));
headPosition = random(headRight,headLeft);
headServo.write(headPosition);
myDelay = millis() + random(HClow,HChigh); 
currentPhase++;
break;
case 1:
lowChirp(random(2,6),random(1,10));
headPosition = random(headRight,headLeft);
headServo.write(headPosition);
myDelay = millis() + random(LClow,LChigh); 
currentPhase++;
break;
case 2: 
tweet(random(2,4),random(1,5));
headPosition = random(headRight,headLeft);
headServo.write(headPosition);
myDelay = millis() + random(Tlow,Thigh); 
currentPhase++;
break;
case 3: 
headPosition = random(headRight,headLeft);
headServo.write(headPosition);
peck(random(100,400),random(75,500));
myDelay = millis() + random(Tlow,Thigh); 
currentPhase = 0;
break;
} // End Case
} // end mydelay timer

// Else we are inactive so see if we have enough light to wake up or not
else
{
if (lightLevel > 150)
{
activeMode = true;
tweet(5,5);
findCloseObject();
delay(2000); //just sit there a few and look at that thing
tweet(5,5);
}

} // End Active if/then
delay(10); 

} //End main loop

void buzzMotor(int chirpsNumber)
{

int i;
int x;

for (int veces=0; veces<=chirpsNumber;veces++)
{
for (i=0; i<200; i++)
{
digitalWrite(motorPin, HIGH);
delayMicroseconds(i);
digitalWrite(motorPin, LOW);
delayMicroseconds(i);
}

for (i=90; i>200; i--)
{
digitalWrite(motorPin, HIGH);
delayMicroseconds(i);
digitalWrite(motorPin, LOW);
delayMicroseconds(i);
}
}
}


// Low chirp sound
void lowChirp(int intensity, int chirpsNumber)
{
int i;
int x;

for (int veces=0; veces<=chirpsNumber;veces++)
{
for (i=0; i<200; i++)
{
digitalWrite(speakerPin, HIGH);
delayMicroseconds(i);
digitalWrite(speakerPin, LOW);
delayMicroseconds(i);
}

for (i=90; i>200; i--)
{
digitalWrite(speakerPin, HIGH);
delayMicroseconds(i);
digitalWrite(speakerPin, LOW);
delayMicroseconds(i);
}
}
}

//High chirp sound
void highChirp(int intensity, int chirpsNumber)
{
int i;
int x;

for (int veces=0; veces<=chirpsNumber;veces++)
{
for (i=100; i>0; i--)
{
for (x=0; x<intensity; x++)
{
digitalWrite(speakerPin, HIGH);
delayMicroseconds(i);
digitalWrite(speakerPin, LOW);
delayMicroseconds(i);
}
}
}
}

// Tweet sound
void tweet(int intensity, int chirpsNumber)
{
int i;
int x;

for (int veces=0; veces<=chirpsNumber;veces++)
{
for (i=80; i>0; i--)
{
for (int x=0; x<intensity; x++)
{
digitalWrite(speakerPin, HIGH);
delayMicroseconds(i);
digitalWrite(speakerPin, LOW);
delayMicroseconds(i);
}
}
}
}

// ** returns the ET in millis for timer variable passed
long getET(long mytimer){
long result;
result = (millis() - mytimer);
return result;
}

// just move randomly a little bit - not used at the moment
void RandomHeadMove()
{
headOffset = random(0,2);
headDirection = random(1,10);
if (headDirection <= 5)
{
headPosition = headPosition - headOffset;
}
else
{
headPosition = headPosition + headOffset;
}
headServo.write(headPosition);
generalTimer1 = millis();
SoftwareServo::refresh();
}

long ReadSonar()
{
long duration, distance;

pinMode(TRIGGER_PIN, OUTPUT);
digitalWrite(TRIGGER_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIGGER_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIGGER_PIN, LOW);

pinMode(ECHO_PIN, INPUT);

duration = pulseIn(ECHO_PIN, HIGH);
distance = duration / 29 / 2;
if (distance == 0)
{
distance = 9999;
}
return distance;

}

// This is from an AdaFruit example here: 
//https://learn.adafruit.com/trinket-gemma-servo-control/code
// We'll take advantage of the built in millis() timer that goes off
// to keep track of time, and refresh the servo every 20 milliseconds
// The SIGNAL(TIMER0_COMPA_vect) function is the interrupt that will be
// Called by the microcontroller every 2 milliseconds
volatile uint8_t counter = 0;
SIGNAL(TIMER0_COMPA_vect) {
// this gets called every 2 milliseconds
counter += 2;
// every 20 milliseconds, refresh the servos!
if (counter >= 20) {
counter = 0;
SoftwareServo::refresh();
}
}

void findCloseObject()
{
int pos;
uScmClose = 9999; //Start with way out there, otherwise it's zero and that doesn't work silly
for (pos = 0; pos <= 180; pos += 1) 
{ // goes from 0 degrees to 180 degrees
headServo.write(pos);
delay(10); //wait for slow servo stuff to move
uScm = ReadSonar(); 
if (uScm < uScmClose)
{
closePosition = pos; //Save the position for now
uScmClose = uScm; //Save the closest value to check
}
}
headServo.write(closePosition);
}

void peck(int peckSpeed, int peckTime)
{
analogWrite(motorPin, peckSpeed);
delay(peckTime);
analogWrite(motorPin,0);
}
Website Security Test