Amazing Arduino-Powered Avengers John Steed Puppet

Technology
Amazing Arduino-Powered Avengers John Steed Puppet

This is a project I have been working on for a very long time. I am finally at the point of writing it up. It’s been so long in fact I can’t even remember all the details so this will piece it together as best I can with lots of pictures and some vague descriptions of what’s going on.

Basically it’s my John Steed marionette. John Steed of course being one of the two main characters form the 60s television programme The Avengers. The other (best known) main character being Mrs Peel.

[Ed. Note: This post originally ran on Simon’s personal blog, and is reprinted here with his permission. Check out his other projects.]

This Steed marionette was inspired by the Thunderbirds puppets and is made in that style and to those dimensions (as best as I can gather). Now, I have no clue how to actually make a marionette so I made it up as I went along! He moves, if you pull his strings, and he also has an animated head. He’ll move his eyes and says many Steedisms. He’s also shy and doesn’t like when people get too close. This is what I ended up with:

YouTube player

And here are some pictures of him finished.

This slideshow requires JavaScript.

Now, how did I make him? Well, I’ll try to remember as best as I can. It seems I started him on the 24th Of May 2009 (hmmm, virtual web mistress’s birthday) although I am sure I started planning before that.  I started with the head. This I made from fibreglass. I started with foam on a stick! The foam was rough shaped then I covered it in plasticine to sculpt the actual head and face.

This slideshow requires JavaScript.

The eyes were made from sections of ping ping ball. This gave me the correct radius for later when I animated the eyes. Once I had the form correct I smeared him in Vaseline and made a two piece Plaster of Paris mould. The form was a little damaged removing it but I had a nice plaster mould.

Next I used the mould to make a fibreglass positive of the two head halves. These were removed, trimmed and the opening in the back of the head cut out. Then the two halves were joined. I added in brass mounting plates which would eventually hold the eye movement mechanism and I then covered the head in filler and sanded it then painted it with high build primer and sanded it some more.

This slideshow requires JavaScript.

The eye mechanism itself is made from aluminium and brass. The actual eyes are sections of ping pong ball. A micro servo with a cam on it pushes against a spring loaded lever to actually move the eyes left and right. To start with I just had simple place-holder eyes.

Steeds mouth is made from Fimo type modelling plastic you sculpt then put in the oven to harden. Under his neck is a 6 volt solenoid that controls the opening and closing of the mouth.The solenoid is attached to the head but actually sits inside the pullets body. The head and body are held by a simple string so the head can move freely. I believe this is how the Thunderbird puppets also worked with the solenoid being inside the body and not inside the head (one reason their heads are so large)!

This slideshow requires JavaScript.

His hands were made from similar modelling plastic. His upper arms were carved from wood. To make the elbow and wrist joints I simply used eye screws which were joined together with Chicago binding posts. This forms a simple hinge type joint.

Steeds arms, legs and feet are made from wood. I simply carved out something that seemed about right. His pelvis is also a carved block of wood with a brass insert for the hip joint. The legs pivot on brass rods that run through this insert.

I rough cut them with a jig saw then sanded them with an angle grinder then by hand. The feet were also made from wooden blocks. The knee and ankle joints were made with little aluminium fingers glued into one piece that ran in slots on the opposite piece and held with a pin so they can pivot. The slots were cut with a router. To get the feet to hang correctly I had to counterweight the heels with small ball bearings as weights (taken from empty rattle spray cans).

The legs only moves in one plane but they move very well and freely.

This slideshow requires JavaScript.

Steed’s body was made with fibreglass also but instead of making a mould I made a foam core and simple glassed over it then hacked out the foam later to leave a shell. Again this was covered in filler and sanded and painted. I was then able to assemble the head, body and legs.

With all the bits finally done I was able to fully assemble Steed.

This slideshow requires JavaScript.

Steed was actually in this state for some time and my friends started referring to him as ‘Creepy Steed’ although I can’t see why? Actually I think he looks a lot like a naked Jean-Luc Picard!

At some point I gave him his proper eyes. These were just painted by hand on ping pong ball sections then sprayed with clear-coat. Not knowing what Steed’s eyes look like I photographed my own and used them as my model!

Let me look upon you with my own eyes!

About this time the girl I was seeing helped me make some clothes for little Steed. She hand sewed the trousers and a jacket for me. Unfortunately the jacket didn’t move well although it fit very well. I ended up remaking the jacket myself based on her pattern although since I don’t know how to sew it took me 4 or 5 attempts to get something passable.

This slideshow requires JavaScript.

The hat was again made from fibreglass but I started with the brim which I cut from acrylic then hand formed using a hot air gun. The hat itself was again foam. I used plaster again to make a mould then pulled a fibreglass positive from that. The edge of the brim was made by gluing on a piece of string. The hat was painted black and a ribbon added for the band.

This slideshow requires JavaScript.

Finally I made a simple shirt front and tie as well as a carnation from red felt that I sewed to his lapel.

To hang and control Steed I made a simple vertical controller. The arms are connected to wires that can be moved up and down. The legs attach to a cross piece that can be removed from the controller to allow the legs to be worked separately from the main control. This is so you can make him walk. The controller hangs from the stand and Steed hangs from the controller. Steed can be disconnected from the electronics module to work as a stand alone puppet is need be.

He is strung up with black nylon fishing line. There are seven lines. Two to the legs (attached above the knee), two to the hands (attached at the wrists), two to the sides of the head (threaded through his bowler) and one to the middle of his back.

The electronics for Steed are controlled by and Arduino with the speech provided by an Adafruit Waveshield. This shield lets you store samples on an SD card then play them back. Steed will randomly play a sample when triggered. I currently have about 60 samples but have only so far gone through 2 (out of 52) episodes (and that’s just the Emma Peel ones)! I can just keep adding new samples. Each file is simply given a number in sequence and the code just counts up how many there are and generates a random number to pick a file to play.

The Arduino also controls the servo that moves the eyes and the solenoid that opens the mouth. In addition to those I have a 433MHz ASK receiver that listens for the remote control signals to move his eyes and make him speak. There is also a Sharp IR distance sensor from Mindkits that I use to detect if anyone gets too close to Steed. The distance is settable via a potentiometer on the main board. Power is from a simple wall wart supply.

This slideshow requires JavaScript.

There are two remote controls and they both work in the same way by sending signals of a particular period to the receiver. The Arduino detects the signal, works out the period then triggers the appropriate action based on what the period is. I simply use a 555 timer circuit in the transmitter to send three different signals depending on which button, or combination of buttons, are pressed.

The first remote is large and runs off two AA batteries.

The second one was a bit trickier. In the episode ‘How to succeed at murder’ the baddie is controlling a puppet with a remote control in a watch. I decided to do the same.

Remote watch from the 60s episode.
Remote watch from the 60s episode.

I bought some old watch cases on line and used the most suitable one to build my watch. It’s not exactly the same but it is close. I made a small board and used a SMD 555 timer and managed to cram that, three switches and the ASK transmitter into the watch along with a CR2016 3 volt battery. The front plate was machined on the lathe and the buttons are made from Chicago binding posts machined down in height. They are just a tight push fit over the button stalks. The rear of the watch simple screws on. The case of the watch is one battery terminal. The other is a simple wire with a sytrene shield between it and the electronics to prevent shorts. It all just fits in!

This slideshow requires JavaScript.

The antenna wire runs under the leather strap. The range is only about 5m but that’s more than enough. I made my own ‘winder’ button from brass and this is used as a power button. Since the battery has suck low capacity I have a switch in series with it to totally disconnect the circuit when it’s not being used. To use the watch you must hold this button in then press one, or both, of the brass face buttons. Pressing each in turn moves the eyes in that direction. Press both at the same time and Steed speaks. To vary the signal I use two resistors to change the pulse width on the 555. The resistor values were chooses so pressing each gives a distinct period. Pressing them both actually connects the resistors in parallel and this give a third, different period.

Remote control watch.
Remote control watch.

Finally I made a wooden box to house the electronics. Instead of using an Arduino board I instead make my own stand alone board using the Arduino chip.

This slideshow requires JavaScript.

The wooden box provides somewhere to mount the IR sensor (behind a red window) and Steed’s stand simply sits on top of it.

Finally, once Steed himself was complete, I took a black and white picture of him and printed that myself. I sent the photograph to Patrick Macnee via his web site where he will, for a small fee (that didn’t actually cover the return postage they paid) sign things for people. He signed the picture for me “From Steed to Little Steed. Patrick Macnee”. I had this nicely framed.

I think that has been the most satisfying part of the entire project! Thank you big Steed!

So now all I need to do is keep adding samples.

Finally below is the Arduino code I wrote for Steed.

// Includes.
#include <FatReader.h>
#include <SdReader.h>
#include <avr/pgmspace.h>
#include <ServoTimer2.h>  
#include "WaveUtil.h"
#include "WaveHC.h"

// Defines.
#define MIN_PULSE  750        // Servo min pulse.
#define MAX_PULSE  2250       // Servo max pulse.
#define EYEMAXANGLE 140       // Eye servo max angle point.
#define EYEMINANGLE 22        // Eye servo min angle point.
#define EYEMIDANGLE 65        // Eye servo mid point.
#define MOUTHDELAY 5          // Controls how rapidly the mouth opens/closes.        
#define MOUTHTHRESHOLD 285    // Level of audio to trigger opening mouth.
#define MOUTHOPENMIN 50       // Minimum delay after mouth opens.
#define MOUTHOPENMAX 100      // Maximum delay after mouth opens.
#define NUMBEROFCYCLES 10     // How many cycles of data should we look for before triggering.
#define SPEAKPERIOD 1400      // Period of the speak frequency.
#define EYESLEFTPERIOD 1900   // Period of the eyes left frequency.
#define EYESRIGHTPERIOD 2900  // Period of the eyes right frequency.
#define PERIODERROR 200       // How much error in the period can we tolerate.
#define EYEMOVE 6             // How much to move the eyes per increment.
#define SCREAMCHECKTIME 10    // Time in mS between checking the scream sensor.

SdReader card;     // This object holds the information for the card.
FatVolume vol;     // This holds the information for the partition on the card.
FatReader root;    // This holds the information for the filesystem on the card.
FatReader f;       // This holds the information for the file we're play.
dir_t dirBuf;      // Directory buffer.
WaveHC wave;       // This is the only wave (audio) object, since we will only play one at a time.
ServoTimer2 servo; // Eyes servo.

const int analogInSoundPin = 0;     // Analog input pin for audio to drive mouth.
const int analogInDetectPin = 1;    // Analog input pin for range detector to trigger scream.
const int analogInThresholdPin = 5; // Analog input pin for range detector level set.
const int digitalTriggerPin = 6;    // Digital input from receiver for the speech trigger.
const int mouthOutputPin = 7;       // Digital output pin for mouth solenoid.
const int servoPin = 8;             // Digital output for eyes servo.
const int button = 9;               // Digital output for button.

int sensorValue = 0;  // Value read from audio input.
int randNumber = 0;   // General random number variable.
int numFiles = 0;     // Number of wav fils on the SD card.
int eyePosition = 60; // Eye servo position.

// --------------------------------------------------------------------------------------
// Helper function to return free RAM available.
// --------------------------------------------------------------------------------------
int freeRam( void )
{
  extern int  __bss_end; 
  extern int  *__brkval; 
  int free_memory; 
  if( (int) __brkval == 0 ) 
  {
    free_memory = ( (int)&free_memory ) - ( (int)&__bss_end ); 
  }
  else 
  {
    free_memory = ( (int)&free_memory ) - ( (int)__brkval ); 
  }
  return free_memory; 
} 

// --------------------------------------------------------------------------------------
// SD card error handling.
// Just dump out the error and stop.
// --------------------------------------------------------------------------------------
void sdErrorCheck( void )
{
  if ( !card.errorCode() ) return;
  putstring( "nrSD I/O error: " );
  Serial.print( card.errorCode(), HEX );
  putstring( ", " );
  Serial.println( card.errorData(), HEX );
  while( 1 );
}

// --------------------------------------------------------------------------------------
// Map the servo angle.
// Map the given angle based on max and min pulse length.
// --------------------------------------------------------------------------------------
int servoAngle ( int angle )
{  
  int value = 0;
  return value = map( angle, 0, 180, MIN_PULSE , MAX_PULSE ); 
}

// --------------------------------------------------------------------------------------
// Should we scream or not?
// --------------------------------------------------------------------------------------
void CheckScream ( void )
{
  // Check the scream detector.
  int screamSensorValue = analogRead( analogInDetectPin );  
  // Check the threshold level setting.
  int thresholdValue = analogRead( analogInThresholdPin );

  // Serial.print( "Detector: " ); 
  // Serial.println( sensorValue );  

  // Are we over the threshold to scream?
  if ( screamSensorValue >= thresholdValue )
  {
    if ( wave.isplaying ) // Already playing something, so stop it.
    {
      wave.stop(); 
    }
    // Play the scream.
    playScream();
  }
}

// --------------------------------------------------------------------------------------
// Is the button pressed?
// --------------------------------------------------------------------------------------
bool IsButtonPressed (  )
{ 
  // Check the button state.
  return !digitalRead( button );
}

// --------------------------------------------------------------------------------------
// Plays a scream.
// --------------------------------------------------------------------------------------
void playScream() 
{
  // Play the file and open the mouth.
  playfile("scream.wav"); 
  openMouth(); 

  // While playing jiggle the eyes in an amusing/frightening manner.
  while ( wave.isplaying ) 
  {
      eyePosition = random( 30, 90 );
      servo.write(servoAngle(eyePosition));       
  }

  // Shut mouth.
  digitalWrite( mouthOutputPin, LOW ); 

  // Centre eyes.
  eyePosition = EYEMIDANGLE;
  servo.write(servoAngle(eyePosition));
  delay(100);
}

// --------------------------------------------------------------------------------------
// Should we trigger a play?
// --------------------------------------------------------------------------------------
unsigned long triggerPlay ()
{
  boolean bPlay = true;
  unsigned long time1;
  unsigned long time2;
  unsigned long initialPeriod;
  unsigned long period;
  unsigned long returnVal = 0;

  // Check for a signal from the receiver. 
  for ( int i = 0; i < NUMBEROFCYCLES; i++ )
  {  
    // Wait till the pin goes low. 
    while ( digitalRead( digitalTriggerPin ) != 0 ){} 
    // Get the time now. 
    time1 = micros();

    // Wait till the pin goes high again. 
    while ( digitalRead( digitalTriggerPin ) != 1 ){}

    // Wait till the pin goes low again. 
    while ( digitalRead( digitalTriggerPin ) != 0 ){} 
    // Get the time now. 
    time2 = micros();

    period = time2 - time1;
    if ( i == 0 )
    {
       initialPeriod = period; 
    }

    if ( (period < ( initialPeriod - PERIODERROR ) ) || (period > ( initialPeriod + PERIODERROR ) ) )
    {
      // The times we outside the bounds so break out of the for loop since we have no signal.
      returnVal = 0;
      break;
    }

    // If we haven't broken out then we much have the signal so return true.
    returnVal = initialPeriod;      

  } // End for.

  return returnVal;  
}

// --------------------------------------------------------------------------------------
// Plays a full file from beginning to end with no pause.
// --------------------------------------------------------------------------------------
void playcomplete( char *name ) 
{
  // Call our helper to find and play this name.
  putstring_nl( "Playing: " );
  Serial.println( name );

  // Play the file.
  playfile( name );

  // While playing animate!
  while ( wave.isplaying ) 
  {

    // Check the scream detector.
    CheckScream();

    // Read the analog in value.
    sensorValue = analogRead( analogInSoundPin );  

    //Serial.print( "Audio: " ); 
    //Serial.println( sensorValue );  

    // Check is we need to animate anything.
    if( sensorValue > MOUTHTHRESHOLD )
    {  
      // Open the mouth.
      openMouth();     

      // Leave mouth open for a bit. 
      randNumber = random( MOUTHOPENMIN, MOUTHOPENMAX );      
      delay( randNumber );   

      // Randomly move the eyes.
      randNumber = random( 30, 90 ); 
      if ( randNumber % 2 == 0 )
      {
        eyePosition = randNumber;
        servo.write( servoAngle(eyePosition) );
      }        
    }
    else
    { 
      // Close the mouth again.      
      closeMouth();
      delay( 5 );                  
    }             
  } // End while.

  // Now its done playing close gob.
  digitalWrite( mouthOutputPin, LOW );      

  // Centre eyes.
  eyePosition = EYEMIDANGLE;
  servo.write( servoAngle(eyePosition) );
  delay(100);  
}

// --------------------------------------------------------------------------------------
// Play the given wav file.
// --------------------------------------------------------------------------------------
void playfile( char *name ) 
{
  // See if the wave object is currently doing something.
  if ( wave.isplaying ) 
  {
    // Already playing something, so stop it.
    wave.stop(); 
  }

  // Look in the root directory and open the file.
  if ( !f.open( root, name ) ) 
  {
    putstring( "Couldn't open file " ); 
    Serial.print( name ); 
    return;
  }

  // Read the file and turn it into a wave object.
  if ( !wave.create( f ) ) 
  {
    putstring_nl( "Not a valid WAV" );
    return;
  }

  // Start playback.
  wave.play();

}

// --------------------------------------------------------------------------------------
// Sweep the eyes.
// --------------------------------------------------------------------------------------
void sweepEyes ()
{
  for ( int i = EYEMINANGLE; i <= EYEMAXANGLE; i = i + 10 )
  {    
    eyePosition = i;
    servo.write(servoAngle(eyePosition));
    delay(100);  
  }

  for ( int i = EYEMAXANGLE; i >= EYEMINANGLE; i = i - 10 )
  {
    eyePosition = i;
    servo.write(servoAngle(eyePosition));
    delay(100); 
  }  

  // Centre eyes.
  eyePosition = EYEMIDANGLE;
  servo.write(servoAngle(eyePosition));
  delay(100);  
}

// --------------------------------------------------------------------------------------
// Open the mouth.
// --------------------------------------------------------------------------------------
void openMouth ()
{  
  for ( int i = 1; i < MOUTHDELAY; i++ )
  {
    digitalWrite( mouthOutputPin, HIGH );
    delay( i );
    digitalWrite( mouthOutputPin, LOW );
    delay( MOUTHDELAY - i );    
  }

  digitalWrite( mouthOutputPin, HIGH );
}

// --------------------------------------------------------------------------------------
// Close the mouth.
// --------------------------------------------------------------------------------------
void closeMouth()
{  
  for ( int i = 1; i < MOUTHDELAY; i++ )
  {    
    digitalWrite( mouthOutputPin, LOW );
    delay( i );
    digitalWrite( mouthOutputPin, HIGH );  
    delay( MOUTHDELAY - i );   
  }

  digitalWrite( mouthOutputPin, LOW );
}

// --------------------------------------------------------------------------------------
// Main setup routine.
// --------------------------------------------------------------------------------------
void setup() 
{
  // Initialize serial communications at 9600 bps.
  Serial.begin( 9600 ); 

  putstring( "Free RAM: " );       // This can help with debugging, running out of RAM is bad
  Serial.println( freeRam() );      // if this is under 150 bytes it may spell trouble!

  // Set the output pins for the DAC control. This pins are defined in the library
  pinMode( 2, OUTPUT );
  pinMode( 3, OUTPUT );
  pinMode( 4, OUTPUT );
  pinMode( 5, OUTPUT );

  // Pin controlling mouth solenoid.
  pinMode( mouthOutputPin, OUTPUT );  

  // Pin controling the servo.
  servo.attach( servoPin ); 

  // Initialise the wave shield.
  //  if (!card.init(true)) { // Play with 4 MHz spi if 8MHz isn't working for you.
  if ( !card.init() )         // Play with 8 MHz spi (default faster!).  
  {
    putstring_nl( "Card init. failed!" );  
    sdErrorCheck();
    while( 1 );                           
  }

  // Enable optimize read - some cards may timeout. Disable if you're having problems.
  card.partialBlockRead( true );

  // Now we will look for a FAT partition.
  uint8_t part;
  for ( part = 0; part < 5; part++ ) // We have up to 5 slots to look in.
  {  
    if ( vol.init( card, part ) ) 
    break; // Found a slot so break.
  }
  if ( part == 5 ) // No slot found.
  { 
    putstring_nl( "No valid FAT partition!" );
    sdErrorCheck();      
    while( 1 );                           
  }

  // Lets tell the user about what we found.
  putstring( "Using partition " );
  Serial.print( part, DEC );
  putstring( ", type is FAT" );
  Serial.println( vol.fatType(), DEC );     // FAT16 or FAT32?

  // Try to open the root directory.
  if ( !root.openRoot( vol ) )
  {
    putstring_nl( "Can't open root dir!" ); 
    while( 1 );                            
  }

  root.ls();

  numFiles = 0;
  root.rewind();  
  while ( root.readDir( dirBuf ) > 0 ) 
  {    
    // Skip it if not a subdirectory and not a .WAV file
    if ( !DIR_IS_SUBDIR(dirBuf) && (strncmp_P((char *)&dirBuf.name[8], PSTR("WAV"), 3)) == 0 ) 
    {
      numFiles++;
    }
  }

  Serial.print( "Number of files: " );
  Serial.println( numFiles );

  // Whew! We got past the tough parts.
  putstring_nl( "Ready!" );

  // Test eyes and mouth.
  openMouth();
  sweepEyes();
  closeMouth();

  // Centre eyes.
  eyePosition = EYEMIDANGLE;
  servo.write(servoAngle(eyePosition));
  delay(100);
}

// --------------------------------------------------------------------------------------
// Main loop.
// --------------------------------------------------------------------------------------
void loop() 
{  

  //putstring_nl( "Main loop." );

  int randFile = random( 0, numFiles - 1 ); // Numfiles -1 to account for the scream wav file.
  //Serial.println( randFile );
  String fileNumber = String( randFile );
  String fileToPlay = String( fileNumber + ".wav" );
  //Serial.println( fileToPlay );
  char fileName[13];
  fileToPlay.toCharArray( fileName, 13 );
  unsigned long period = 0;
  unsigned long time = millis();

  // Look for a signal.
  while ( period == 0 )
  {

    // Check the scream sensor. We only do this every 100mS since it is a cycle intensive thing and
    // it messes up the rest of the loop!
    if ( millis() - time > SCREAMCHECKTIME )
    {
      CheckScream();  
      time = millis();
    }

    // Check the button next.
    if ( IsButtonPressed() )
    {
      // Play!    
      playcomplete( fileName ); 
      break;
    } 
    // Look for a remote signal.
    period = triggerPlay();
  }

  // Serial.print( "Period: " );
  // Serial.println( period );

  // Speak!
  if ( (period > (SPEAKPERIOD - PERIODERROR)) && (period < (SPEAKPERIOD + PERIODERROR)) ) 
  {
    // Play!    
    playcomplete( fileName );
  }

  // Eyes left.   
  else if ( (period > (EYESLEFTPERIOD - PERIODERROR)) && (period < (EYESLEFTPERIOD + PERIODERROR)) ) 
  {      
    eyePosition = eyePosition + EYEMOVE;
    if ( eyePosition > EYEMAXANGLE )
    {
      eyePosition = EYEMAXANGLE;
    }
    servo.write(servoAngle(eyePosition));
    delay(100);  
  }

  // Eyes right.
  else if ( (period > (EYESRIGHTPERIOD - PERIODERROR)) && (period < (EYESRIGHTPERIOD + PERIODERROR)) ) 
  {
    eyePosition = eyePosition - EYEMOVE;
    if ( eyePosition < EYEMINANGLE )
    {
      eyePosition = EYEMINANGLE;
    }
    servo.write(servoAngle(eyePosition));
    delay(100);  
  }

}

What will the next generation of Make: look like? We’re inviting you to shape the future by investing in Make:. By becoming an investor, you help decide what’s next. The future of Make: is in your hands. Learn More.

Tagged

Ken is the Grand Nagus of GeekDad.com. He's a husband and father from the SF Bay Area, and has written three books filled with projects for geeky parents and kids to share.

View more articles by Ken Denmead
Discuss this article with the rest of the community on our Discord server!

ADVERTISEMENT

Escape to an island of imagination + innovation as Maker Faire Bay Area returns for its 16th iteration!

Prices Increase in....

Days
Hours
Minutes
Seconds
FEEDBACK