Introduction: Moosetar (Zoltar) Fortune Teller

This is an animatronics project where I took a toy moose (it was a Christmas decoration that sang "Blue Christmas") and turned it into Moosetar the fortune telling moose.  The moose takes a coin, talks, kind of dances, and prints out a fortune.  This may look complicated, but it was fairly easy.  

Step 1: Parts

  • Toy that talks using DC motors (in my case I chose a singing Christmas moose).
  • Arduino (I used a Mega)
  • Adafruit Motor Shield
  • Emic2 text to speach module
  • Coin Acceptor (I used this one https://www.sparkfun.com/products/11719)
  • Bag to catch coins
  • Connection wire
  • Thermal Printer (I used this one https://www.sparkfun.com/products/10438)
  • A few power supplies
  • A USB mini plasma ball
  • A USB hub
  • Power Strip
  • A basketball display case (I used this one http://www.containerstore.com/shop/collections/display/cubesCases?productId=10001589&N=233)
  • Some wood for the case (I made mine about 5 ft tall, you can make yours whatever height you'd like)
  • Trim for the case
  • Wood Glue
  • Screws
  • Hinge
  • Paint

Step 2: Control the Toy

The Moose I used had two motors, on that controls the mouth and the second controls side to side movement (kind of dancing) the moose does.  All you need to do is cut the wires that connect to the controller, and connect them to the motor shield.  It probably won't matter which wire you connect to ground and power, even though it will change your sketch.  I connected my motors to M1 and M2.  I also connected a 9v power supply to drive poser to the motors.  The motor shield comes with a good example sketch (MotorParty) for driving motors.  Try that to verify you can move the motors as you'd like.  Here are the important parts:

Choose the motor to move:
AF_DCMotor motor(2); 

Releases the motor:
motor.setSpeed(200);
motor.run(RELEASE);

Run the motor Forward:
motor.run(FORWARD);

Run the motor Backward:
motor.run(BACKWARD);
motor.setSpeed(i);

This will give you an idea of how you need to run the motors to make the do what you want.

Step 3: Coin Acceptor

I used this coin acceptor from Sparkfun https://www.sparkfun.com/products/11719 .  The coin acceptor uses pulse width modulation to tell you what coin is sent over.  To keep things simple I just used an interrupt on one of the interrupt pins to detect any signal coming over.  I also connected the power to a DC power supply and also connected the ground to ground on the Arduino.  Another option is use this coin acceptor https://www.sparkfun.com/products/11636 that can send serial data.

Important parts of the final sketch that relate to the coin acceptor (the white "coin" wire is connected to pin 21

coin is a global variable, if it's 1 in loop then I know a coin has come through and it should give a fortune

In setup I did this to create the interrupt:
attachInterrupt(2, sendcoin, RISING); 

Here's my sendcoin function
void sendcoin()
{
  type += 1;
  unsigned long curTime = millis();
  if ((curTime - time) > 50)
  {
    coin = 1;
  }
  else
  {
    coin = 2;
  }
  time = curTime;
}


in loop I do this:

detachInterrupt(2); // this makes sure we don't get a lot of interrupts if people put a bunch of coins in, this could mess up the serial printer trying to print

At the end of loop() I set coin to 0 and re-attach the interrupt:
coin = 0;
attachInterrupt(2, sendcoin, RISING);

That's basically it for the coin accepting code.


Step 4: Serial Printer

I used this serial printer https://www.sparkfun.com/products/10438

I used Serial1 on the Mega to talk to the printer, the relevant pieces of code are:

Serial1.begin(19200);

I have several helper methods to run the printer and print images, etc:

For the most part I just write to the serial port like this:
Serial1.print(" Moosetar Says: BEWARE!");

For printing images I do this:
printBitmap(moose_outline_width, moose_outline_height, moose_outline_data, true); 
and include the data as a .h file.  More information on how to do this can be found here http://learn.adafruit.com/mini-thermal-receipt-printer/bitmap-printing

Which then get's printed by the printer.  I found some helper methods in Lady Ada's library.  Because I was using hardware serial I had some issues with her code, but I changed them and included them in the sketch.  Here are the relevant methods.


void writePrintMode()
{
   Serial.println(printMode);
   Serial1.write(27);
   Serial1.write(33);
   Serial1.write(printMode);
}

void setPrintMode(uint8_t mask) {
  printMode |= mask;
  writePrintMode();
  //charHeight = (printMode & DOUBLE_HEIGHT_MASK) ? 24 : 48;
  //maxColumn  = (printMode & DOUBLE_WIDTH_MASK ) ? 16 : 32;
}

void setNormalMode()
{
  printMode = 0;
  writePrintMode();
  Serial1.write(27);
  Serial1.write(45);
  Serial1.write(zero);
  Serial1.write(27);
  Serial1.write(32);
  Serial1.write(zero);
  Serial1.write(29);
  Serial1.write(33);
  Serial1.write(zero);
}

void unsetPrintMode(uint8_t mask)
{
   printMode &= ~mask;
   writePrintMode();
}


void printerLineFeed(int lines)
{
   for(int i = 0; i < lines; i++)
   {
     Serial1.write(10);
   }
}

void writeBytes(uint8_t a) {
  Serial1.write(a);
}

void writeBytes(uint8_t a, uint8_t b) {
  Serial1.write(a);
  Serial1.write(b);
  delay(300);
}

void writeBytes(uint8_t a, uint8_t b, uint8_t c) {
  Serial1.write(a);
  Serial1.write(b);
  Serial1.write(c);
  delay(300);
}

void writeBytes(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
  Serial1.write(a);
  Serial1.write(b);
  Serial1.write(c);
  Serial1.write(d);
  delay(300);
}

void printBitmap(Stream *stream) {
  uint8_t  tmp;
  uint16_t width, height;

  tmp    =  stream->read();
  width  = (stream->read() << 8) + tmp;

  tmp    =  stream->read();
  height = (stream->read() << 8) + tmp;

  printBitmap(width, height, stream);
}

void printBitmap(int w, int h, Stream *stream) {
  int rowBytes, rowBytesClipped, rowStart, chunkHeight, x, y, i, c;

  rowBytes        = (w + 7) / 8; // Round up to next byte boundary
  rowBytesClipped = (rowBytes >= 48) ? 48 : rowBytes; // 384 pixels max width

  for(rowStart=0; rowStart < h; rowStart += 255) {
    // Issue up to 255 rows at a time:
    chunkHeight = h - rowStart;
    if(chunkHeight > 255) chunkHeight = 255;

    writeBytes(18, 42, chunkHeight, rowBytesClipped);

    for(y=0; y < chunkHeight; y++) {
      for(x=0; x < rowBytesClipped; x++) {
        while((c = stream->read()) < 0);
        Serial1.write((uint8_t)c);
      }
      for(i = rowBytes - rowBytesClipped; i>0; i--) {
        while((c = stream->read()) < 0);
      }
    }
    //timeoutSet(chunkHeight * dotPrintTime);
  }
  //prevByte = '\n';
}


void printBitmap(
int w, int h, const uint8_t *bitmap, bool fromProgMem) {
  int rowBytes, rowBytesClipped, rowStart, chunkHeight, x, y, i;

  rowBytes        = (w + 7) / 8; // Round up to next byte boundary
  rowBytesClipped = (rowBytes >= 48) ? 48 : rowBytes; // 384 pixels max width

  for(i=rowStart=0; rowStart < h; rowStart += 255) {
    // Issue up to 255 rows at a time:
    chunkHeight = h - rowStart;
    if(chunkHeight > 255) chunkHeight = 255;

    writeBytes(18, 42, chunkHeight, rowBytesClipped);

    for(y=0; y < chunkHeight; y++) {
      for(x=0; x < rowBytesClipped; x++, i++) {
        Serial1.write(fromProgMem ? pgm_read_byte(bitmap + i) : *(bitmap+i));
      }
      i += rowBytes - rowBytesClipped;
    }
    //timeoutSet(chunkHeight * dotPrintTime);
  }
  //prevByte = '\n';
}

Step 5: Emic2

I tried a couple of ways to get the moose to talk.  First I used a second Arduino with a Wave Shield from Adafruit.  Then I decided to go with an Emic2 text to speech unit.  This worked really well and made it kind of funny since it sounds a bit like a robot.  Also I liked this better than the Wave Shield because I didn't have to pre-record anything and I could easily have it say whatever I wanted.

I got mine here https://www.sparkfun.com/products/11711 but you can also order them directly from Parallax or a bunch of other places.  Basically you send it serial commands and it will talk.  Here are the important parts of the sketch:

Define the software serial interface I used:
#define rxPin 51    // Serial input (connects to Emic 2 SOUT)
#define txPin 53    // Serial output (connects to Emic 2 SIN)
SoftwareSerial emicSerial =  SoftwareSerial(rxPin, txPin);

In setup() we get ready to talk to the Emic2
emicSerial.begin(9600);
  emicSerial.print('\n');             // Send a CR in case the system is already up
  while (emicSerial.read() != ':');   // When the Emic 2 has initialized and is ready, it will send a single ':' character, so wait here until we receive it
  delay(10);                          // Short delay
  emicSerial.flush();                 // Flush the receive buffer

I used the following method to take some text, send it to the Emic2 and run the moose mouth and body while it talked:
void mooseTalk(String phrase)
{
     emicSerial.println("P1");
     delay(100);
     clearEmicSerialRead();
     emicSerial.println("W75");
     delay(100);
     clearEmicSerialRead();
     emicSerial.println("V0");
     delay(100);
     clearEmicSerialRead();
     emicSerial.println("N0");
     delay(100);
     clearEmicSerialRead();
     emicSerial.print('S');
     //emicSerial.print("\\/\\/");
     emicSerial.println(phrase);
     Serial.println(phrase);

     int i = 0;
     int u = 0;
     for(int j = 0; j <  phrase.length(); j++)
     {
         if(phrase[j] == ' ')
         {
           i++;
         }
         if(phrase[j] == '_')
         {
            u++;
         }
     }

     i = i + u/2;
     i = i - i * .25;
     delay(300);
     for(int j = 0; j < i; j++)
     {
       mooseMove(random(250,450));
       delay(random(150,300));
     }
     clearEmicSerialRead();
     //while (emicSerial.available() > 0)
     //{
        //Serial.println(emicSerial.read());
     //}
     //while (emicSerial.read() != ':');
}

I also created this method to have it sing after it gave it's fortune.  The songs in the sketch are "hit me with your best shot", "Edelweiss", and "Daisy Bell"

void singSong() {
  emicSerial.println("V0");
  delay(100);

  emicSerial.println("P0");
  int song = random(3);
  if (song == 0)
  {
    emicSerial.println("S[:rate 230][:n0][:dv ap 200 sm 100 ri 100][WEH<125,12>LX<75>YXOR<200,14>AH<200,17>AXRIY<300,21>LL<100>TAH<300>F<100>KUH<200,19>KIY<200,17>WIH<125>TH<75>AX<200,14>LLAO<300,17>NX<100>HXIH<200>STOR<200,14>IY<400,19>_<500> AH<125,17>V<75> BRREY<200,19>K AH<125,17>N<75>LLIH<200,19>DXEL<200,17>HXAR<300,19>TS<100>LLAY<125>K<75>DHAX<200,17>WAH<125,19>N<75> IH<125>N<75>MIY<400,21>_<400>DHAE<200>TS<100>OW<200,19>KEY<400,17>LLEH<125,14>TS<75>SIY<400,17>HXAW<200>YU<200,14>DUW<400,19>IH<125,17>T<75>_<400>PUH<125,19>T<75>AH<125>P<75>YXOR<200,17>DUW<450,19>KS<150>LLEH<300,19>TS<100>GEH<125>T<75>DAW<300>N<100>TUW<400,21>IH<200,19>T<100>HXIH<125,21>T<75>MIY<200,19>WIH<125>TH<75>YXOR<200,17>BEH<300>ST<100>SHAO<300,21>T<100>_<1100>WAY<200,17>DOW<125,19>N<75>CHAX<200,17>HXIH<70,21>T<30>MIY<100,19>WIH<125>TH<75>YXOR<200,17>BEH<300,19>ST<100>SHAO<200,14>AO<300,12>T<100>_<1200>HXIH<125,21>T<75>MIY<200,19>WIH<125>TH<75>YXOR<200,17>BEH<300>ST<100>SHAO<300,21>T<100>_<800>FAY<200>RR<200,22>AX<200,24>WEH<400,19>EY<900,17>][:n0]");
    for(int i = 0; i < 22; i++)
    {
       mooseMove(random(500,1450));
       delay(random(150,300));
    }
    clearEmicSerialRead();
  }
  else if(song == 1)
  {
    emicSerial.println("S [:phone arpa speak on][:rate 180][:n0][:dv hs 95 br 0 as 90 ap 90 sm 90 ri 100][EY<800,15>DEL<400,18>VAY<900,25>S<300>EY<800,23>DEL<400,18>VAY<900,16>S<300>EH<800,15>VRIY<400>MOR<400>NIH<250,16>NX<150>YU<400,18>GRIY<1100,20>T<100>MIY<800,18>_<400>SMAO<500,15>LX<300>AE<250,18>N<100>D<50>WAY<1100,25>T<100>KLLIY<500,23>N<300>AE<250,18>N<100>D<50>BRAY<1100,16>T<100>YU<800,15>LLUH<300,18>K<100>HXAE<400>PIY<400,20>TUW<400,22>MIY<1000,23>TMIY<900,23>_<800>BLLAO<800,25>SAH<125,18>M<75>AH<250>V<150>SNOW<400,22>MEY<400,20>YU<400,18>BLLUW<500,15>M<300>AE<250,18>N<100>D<50>GROW<900,23>_<300>BLLUW<500,20>M<300>AE<250,23>N<100>D<50>GROW<800,25>FOR<400,23>EH<1200,22>VRR<800,18>_<400>EY<800,15>DEL<400,18>VAY<900,25>S<300>EY<800,23>DEL<400,18>VAY<900,16>S<300>BLLEH<600,15>S<200>MAY<400,18>HXOW<250>M<150>LLAE<250,20>N<100>D<50>FOR<400,22>EH<1200,23>VRR<800>][:n0]");
    for(int i = 0; i < 40; i++)
    {
       mooseMove(random(500,1450));
       delay(random(150,300));
    }
    clearEmicSerialRead();
  }
  else //if (song == 2)
  {
     emicSerial.println("D1");
     for(int i = 0; i < 22; i++)
     {
       mooseMove(random(500,1450));
       delay(random(150,300));
     }
     clearEmicSerialRead();
  }
  /*else
  {
    emicSerial.println("S [:phone arpa speak on][:rate 190][:n2][:dv ap 200 sm 100 ri 100][R EY<200,17>N<100>DRAO<200,24>PS<100>AO<200>N<100>ROW<300,19>ZIX<200,17>Z<100>AE<150>N<100>D<50>WIH<300,12>SKRR<200,17>Z<100>AO<200>N<100>KIH<300,19>TAH<150,17>N<100>Z<50>_<300>BRAY<200>T<100>KAO<300,24>PRR<300>K EH<300,19>TEL<200,17>Z<100>AE<150>N<100>D<50>War<200,12>M<100>WUH<300,17>LL EH<200>N<100>MIH<300,19>TAH<150,17>N<100>Z<50>_<300>BRAW<200>N<100>PEY<300,24>PRR<300,22>PAE<300,17>KIH<300,19>JHIX<200,15>Z<100>TAY<200>D<100>AH<200,22>P<100>WIH<200,20>TH<100>STRIH<300,13>NX<200>Z<100>_<300>DHIY<200,12>Z<100>AR<300,13>AX<300,15>FYU<300,17>AH<200,18>V<100>MAY<300,20>FEY<300,22>VRR<300,24>EH<200,22>T<100>THIH<500,16>NX<300>Z<100>][:n0]");
    emicSerial.println("S [:phone arpa speak on][:rate 190][:n2][:dv ap 200 sm 100 ri 100][KRIY<200,17> M<100> KAH<300,24> LLRR<200> D<100> POW<300,19> NIY<200,17> Z<100> AE<150> N<100> D<50> KRIH<200,12> SP<100> AE<300,17> PEL<300> STRUW<300,19> DXEL<300,17> _<300> DOR<300> BEH<150,24> LX<100> Z<50> AE<150> N<100> D<50> SLLEY<300,19> BEH<150,17> LX<100> Z<50>AE<150> N<100> D<50> SHNIH<200,12> T<100> SEL<300,17> WIH<200> TH<100> NUW<300,19> DXEL<200,17> Z<100> _<300>WAY<150,17> LL<100> D<50> GIY<200,24> S<100> DHAE<200,22> T<100> FLLAY<300,17> WIH<200,19> TH<100> DHAX<300,15> MUW<200> N<100> AO<200,22> N<100> DHER<300,20> WIH<300,13> NX<200> Z<100> _<300>DHIY<200,12> Z<100> AR<300,13> AX<300,15> FYU<300,17> AH<200,19> V<100> MAY<300,20> FEY<300,22> VRR<300,24> EH<200,22> T<100> THIH<500,16> NX<300> Z<100>][:n0]");
    emicSerial.println("S [:phone arpa speak on][:rate 190][:n2][:dv ap 200 sm 100 ri 100][GRR<150,17> LL<100> Z<50> IH<200,24> N<100> WAY<200> T<100> DRREH<300,19> SIX<200,17> Z<100> WIH<200> TH<100> BLLUW<300,12> SAE<300,17> TAH<200> N<100> SAE<300,19> SHIX<200,17> Z<100> _<300> SNOW<300> FLLEY<200,24> KS<100> DHAE<200> T<100> STEY<300,19> AO<200,17> N<100> MAY<300> NOW<200,12> Z<100> AE<150,17> N<100> D<50> AY<300> LLAE<300,19> SHIX<200,17> Z<100> _<300>SIH<200> LL<100> VRR<300,24> WAY<200,22> T<100> WIH<200,17> N<100> TRR<200,19> Z<100> DHAE<200,15> T<100> MEH<150> LL<100> T<50> IH<200,22> N<100> TUW<300,20> SPRIH<400,13> NX<200> _<300> DHIY<200,12> Z<100> AR<300,13> AX<300,15> FYU<300,17> AH<200,19> V<100> MAY<300,20> FEY<300,22> VRR<300,23> EH<200,24> T<100> THIH<400,25> NX<100> Z<100> _<200>][:n0]");
    emicSerial.println("S [:phone arpa speak on][:rate 190][:n2][:dv ap 200 sm 100 ri 100][WEH<200,24> N<100> DHAX<300> DAO<500> G<100> BAY<200,17> TS<100> _<300> WEH<200,22> N<100> DHAX<300> BIY<600> STIH<150,16> NX<100> Z<50> _<300> WEH<200,20> N<100> AY<200> M<100> FIY<600> LLIH<200,12> NX<100> SAE<1400,17> D<100> AY<300> SIH<200> M<100> PLLIY<300,19> RIY<300,17> MEH<200,19> M<100> BRR<300,17> MAY<300,19> FEY<300,20> VRR<300,22> EH<200,20> T<100> THIH<300,22> NX<200> Z<100> AE<150,20> N<100> D<50> DHEH<200,24> N<100> AY<300,25> DOW<150,24> N<100> T<70> FIY<600,25> LX<300> _<1000> SOW<900,24> BAE<2000,20> D<100>_<40>][:n0]");
    clearEmicSerialRead();
    for(int i = 0; i < 180; i++)
    {
       mooseMove(random(500,1450));
       delay(random(150,300));
    }
  }*/
  clearEmicSerialRead();

  emicSerial.println("P1");
  clearEmicSerialRead();

}

Step 6: Making the Sound and Movements Work Together

This was an interesting, kind of automated puppetry part I though was interesting.  I didn't want to have to code out a sequence of opening and closing his mouth for every different bit of text he would say.  Instead I wanted a method that could figure out how it could open his mouth.  I found by parsing the incoming string and looking for spaces and __ (double underscores, which emphasize words) I could figure out how to open his mouth.  For singing I did basically the same thing, but I just opened his mouth longer.  Here's the revelvant code:

for(int j = 0; j <  phrase.length(); j++)
     {
         if(phrase[j] == ' ')
         {
           i++;
         }
         if(phrase[j] == '_')
         {
            u++;
         }
     }

     i = i + u/2;
     i = i - i * .25;
     delay(300);
     for(int j = 0; j < i; j++)
     {
       mooseMove(random(250,450));
       delay(random(150,300));
     }

As you can see I take the number of spaces, add half the number of underlines, then subtract 25%.  In almost every case he opens his mouth very close to what was being said by the Emic2.  I used random delays when opening and closing his mouth to make it seem a bit more real.  I also did some little back and forth dancing moves when the printer was going and he wasn't talking.

Step 7: Wooden Case

I kept the case pretty simple.  The basketball enclosure I was using was about 10" x 10", so I cut the wood to be the same size so it could sit right under it.  For part of the box I used MDF, which worked well, for the other (lower) section I used pine  The nice thing about MDF is there are no spliters, and it's pretty cheap.  I'm not sure it would survive a good kick though.  I used both glue and screws to hold the sides together to make the box.  I painted the whole thing black with spray paint.  I cut a door in the back and connected hinges so I could open it.

I drilled a couple of holes in the bottom of the enclosure and screwed them onto the wooden box I built.  I put trim around the top section and some around the middle with the two parts of the stand come together.  I cut a 2" by 4" hole towards the back of the basketball enclosure for the moose to come up through.

Last I ran the USB plasma ball cord through the hold the moose was coming through.  I connected to other side to a USB hub that was powered off a cell phone charger.  I connected the Arduino to the same USB hub.

Step 8: Stickers and Decorations

To decorate the box I got some red felt and some gold rope to give him a backdrop.  I also got some shiny scrapbooking paper on the back wall and bottom of the basketball enclosure.

I went to a craft store and got letters to put on the front of the basketball enclosure to say "Moosetar".  I also put a sign that said pull up because the printer doesn't rip as well pulling down.

Step 9: Video and Code

Here's a video of moosetar working.  As you can see you put a coin in, then it talks and gives you a fortune, then you rip it off.  I've attached the code here.  The .h files are different images of moose

This is an early video before it was painted and used the Emic2


Here's with the Emic2
Puppet Contest

Finalist in the
Puppet Contest

Epilog Challenge V

Participated in the
Epilog Challenge V