<![CDATA[The Chatter Telephone is a classic Fisher Price toy that has been sold since 1962. The design has evolved over the years, but in 2010 the original 'classic' Chatter phone was brought back into the limelight in the Disney / Pixar film Toy Story 3 where the phone helped Woody escape from Sunnyside Daycare.
To coincide with this movie release, Fisher Price released a new Talking Chatter Telephone – an all-plastic model that recreated the boxy shape of the 1960s original but with the addition of some voice clips from the movie controlled by the rotary dial.
I had bought one of these for my infant son (who loved the movie) but the toy didn't get him too excited. So, I decided to turn the phone into a weekend project… introducing the Talking Chatter Smartphone.
At the heart of the Chatter Smartphone I’m using a Raspberry Pi Model B+ to give the phone more brains than it was built with. A tiny WiFi dongle provides wireless network access, and a custom Python script (code below) running on startup provides all of the logic.
Wherever possible, I’ve used the original components and controls to interact with the phone. I also wanted to retain the ‘factory’ look of the phone, so I avoided adding a screen or any unnecessary buttons.
Inputs are limited to the original rotary dial plus an ‘on hook’ sensor I added. Outputs are audio through the built-in speaker and eye movements controlled by a servo I added.
The phone uses JSON format APIs to pull data from various online services. Right now it’s configured to pull from Forecast.io for weather and from Rotten Tomatoes for movie information, but it could easily be extended to use other APIs.
The push notification system uses a Twitter account to pull new alerts from. Those alerts are pushed onto the Twitter account using IFTTT – either pre-defined recipes or alerts driven by my iPhone (e.g. when I leave a geofence).
The Chatter Smartphone uses the OS9 boot sound (or, as I prefer to think of it, the Wall-E startup sound) on startup. This isn’t just for show, it’s also handy so I know when the Pi has finished booting and is ready for an SSH connection – every Pi should have this!
The phone can work with any standard Web Service (JSON API). It would be a two minute job to swap weather forecasts for a Quote of the Day API or, more usefully, a stocks or exchange rates api.
Need more volume? A fun hack would be to extend the Pi’s audio jack out to the back panel of the phone, potentially making a Fisher Price phone the centre of a high-end Hi-Fi system.
The push notification system can take any input from IFTTT. Currently, I have it alerting the phone when: I leave work; the weather forecast is for rain tomorrow; when my garden office temperature falls below 10 degrees centigrade; and when the International Space Station is soon passing overhead.
First I used a craft knife and a rotary cutter to take out the original battery box to make room for the Raspberry Pi. At the same time I removed the original PCB from above the battery box. The original set of three microswitches that read the rotary dial position were kept, but the microswitch that detected front axel movement was removed.
The rotary dial uses a clever binary-like encoding mechanism, with grooves and ridges on the dial cylinder pushing and releasing three microswitches in various sequences. The result is that the phone can detect five dial positions: at rest; position 1-2; position 3-5; position 6-8; position 9-0.
I considered improving the granularity of the dial – e.g. using barcode-style lines and an IR light/sensor – to allow all dial positions to be detected, but ultimately it felt more authentic to retain the physical limitations of the original design.
I removed the original metal bar that linked eye movements to the mechanical turning of the front axle. In its place I added a tiny 9g servo which I hot-glued to the post that carries the eyes. Using some stiff wire I created a custom linkage between the servo horn and the eyes, allowing complete control of eye movements up, down and anywhere in between.
The only other tweak was to increase the weight of the receiver so that it reliably pressed and held the on-hook sensor. Since the handset was completely sealed without any screws or other ways in, I opted to fill it with sand through the cord hole, then hot-glued the opening with the cord held in place.
The electronics in this project are almost non-existent: no resistors, diodes or anything like that. To keep things simple, I simply soldered female pin headers onto the original dial wiring and utilised the Pi’s internal pull-up resistors to make it work reliably.
The only added component is the on-hook sensor which I added to detect when the receiver is on the cradle. In version one of the project I used the original front axle microswitch for the on-hook sensor, but it didn’t work reliably so I replaced it with a simple light-action pushbutton from Maplin.
For a while I used the original internal speaker from the Fisher Price toy which I wired directly to a 3.5mm jack and into the Raspberry Pi’s audio jack. Using the Vol app (link below), I was able to boost the volume to a decent level.
But I wanted more volume, so I bought a £10 ($15) powered speaker from my local supermarket – the kind of thing usually plugged into an iPod and used to annoy everyone on a bus. The speaker takes power from USB audio in through a standard headphone jack. I disassembled the speaker and its associated PCB, discarded the housing and mounted the parts in place of the original speaker inside the phone.
A nice feature of the speaker – certainly at this low price – is that it also acts as a Bluetooth receiver. I’m not currently using that facility, but it would be fairly straightforward to add either a physical switch or software mode that turned off the “headphone in” detection on the speaker so that the phone could be used as a Bluetooth speaker for phones and computers. (Although adding an AirPlay receiver might be a simpler – and cooler – option.)
The software is written in Python. I’m pretty new to Python, so it’s probably not the best code, but it works reliably and was surprisingly quick and easy to put together. I’ve got the script automatically running at startup (via a @reboot crontab entry) and it’s been running now for several days without any problems.
#!/usr/bin/python import subprocess import RPi.GPIO as GPIO import datetime import time import os import json import urllib2 import pygame import urllib import base64 import httplib import tweepy from tweepy import OAuthHandler from random import randint #pump up the volume subprocess.call(['vol', '80' ]) #button names mapped to BCM GPIO pins dial1 = 22 dial2 = 17 dial3 = 23 cradle = 24 servo = 18 # Some variables to track state offHook = False dialled = 0 t0 = time.time() queuedMessage = " " menuLevel = 0 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(dial1,GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(dial2,GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(dial3,GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(cradle,GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(servo,GPIO.OUT) srv = GPIO.PWM(servo,50) # Set local params and API keys latlong = "[latlong]" forecastioApikey = "[insert key here]" rottentomatoesApikey = "[key here]" # Twitter keys consumer_token = "[key]" consumer_secret = "[key]" access_token = "[key]" access_secret = "[key]" # Authenticate with the Twitter API screen_name = "TalkingChatter" auth = tweepy.OAuthHandler(consumer_token,consumer_secret) auth.set_access_token(access_token,access_secret) api = tweepy.API(auth) ######################## # Function definitions # ######################## def sayThis(t): if("Rain tomorrow" in t): playSound("audio/say-rain.mp3") elif("Grant has left" in t): playSound("audio/woody-escaped.mp3") else: playSound("audio/this-lady.mp3") time.sleep(2.5) subprocess.call(['/home/pi/telephone/speech.sh', t]) def stopSounds(): if pygame.mixer.get_init(): pygame.mixer.music.stop() def playStream(url): global streamProcess stopSounds() streamProcess = subprocess.Popen(['mpg123', url]) def stopStream(): global streamProcess try: streamProcess except NameError: print "No stream running" else: print "Stream running, stop it" streamProcess.kill() def getWeather(t): try: decoded = json.load(urllib2.urlopen("https://api.forecast.io/forecast/" + forecastioApikey + "/" + latlong)) # For debugging, show json-formatted string print json.dumps(decoded, sort_keys=True, indent=4) if(t == 1): summary = decoded['minutely']['summary'] summary = "The short-term forecast is: " + summary if(t == 2): summary = decoded['hourly']['summary'] summary = "The longer-term forecast is: " + summary sayThis(summary) except (ValueError, KeyError, TypeError): print "Error: Problem parsing the weather feed." def getMovies(t): try: if(t == 1): decoded = json.load(urllib2.urlopen("http://api.rottentomatoes.com/api/public/v1.0/lists/dvds/top_rentals.json?limit=5&country=uk&apikey=" + rottentomatoesApikey)) #print json.dumps(decoded, sort_keys=True, indent=4) summarytext = "The most popular DVDs this week are: " if(t == 2): decoded = json.load(urllib2.urlopen("http://api.rottentomatoes.com/api/public/v1.0/lists/movies/box_office.json?limit=5&country=uk&apikey=" + rottentomatoesApikey)) #print json.dumps(decoded, sort_keys=True, indent=4) summarytext = "The most popular cinema releases this week are: " for x in range(0,5): summarytext += str(x+1) + ", " + decoded['movies'][x]['title'] + ". " print "Rotten Tomatoes says: ", summarytext sayThis(summarytext) except (ValueError, KeyError, TypeError): print "Error: Problem parsing the rotten tomatoes feed." def servoWiggle(): srv.start(12.5) time.sleep(0.5) srv.ChangeDutyCycle(8.5) time.sleep(0.5) srv.ChangeDutyCycle(0) def doDial(n): global menuLevel if (offHook == False): # On hook, so do default sound clips if(n == 1): playSound("audio/buzz" + str(randint(1,4)) + ".mp3") if(n == 2): playSound("audio/woody" + str(randint(1,4)) + ".mp3") if(n == 3): playSound("audio/rex" + str(randint(1,4)) + ".mp3") if(n == 4): playSound("audio/tel" + str(randint(1,4)) + ".mp3") # Debounce time.sleep(0.5) else: # Off hook, IVR menu print "Off hook mode" if(menuLevel > 0): # Menu system correctly initialised # Play a sound to indicate input received... (change from hangup noise) playSound("audio/menu-select.mp3") time.sleep(0.5) if(n == 4): # Return to main menu playSound("audio/menu1.mp3") menuLevel = 1 if(menuLevel == 1): # Main menu. Options: # 1: Get weather forecast # 2: DVDs # 3: Radio if(n == 1): playSound("audio/menu2.mp3") menuLevel = 2 if(n == 2): playSound("audio/menu3.mp3") menuLevel = 3 if(n == 3): playSound("audio/menu4.mp3") menuLevel = 4 elif(menuLevel == 2): # Stop the menu sounds stopSounds() # Weather menu. Options: # 1: Short term # 2: Longer term if(n == 1): getWeather(1) if(n == 2): getWeather(2) elif(menuLevel == 3): # Stop the menu sounds stopSounds() # Movies menu. Options: # 1: Top DVDs # 2: Top Cinema if(n == 1): getMovies(1) if(n == 2): getMovies(2) elif(menuLevel == 4): # Stop the menu sounds stopSounds() # Radio menu. Options: # 1: NPR # 2: BBC # 3: Soma if(n == 1): playStream("http://nprdmp.ic.llnwd.net/stream/nprdmp_live01_mp3") if(n == 2): playStream("http://bbcwssc.ic.llnwd.net/stream/bbcwssc_mp1_ws-eieuk") if(n == 3): playStream("http://xstream1.somafm.com:2022") # Debounce time.sleep(0.5) def checkTwitter(): global queuedMessage print "Once a minute... checking Twitter" for status in tweepy.Cursor(api.user_timeline,id=screen_name).items(1): print status.created_at print datetime.datetime.utcnow() if(status.created_at > (datetime.datetime.utcnow() - datetime.timedelta(minutes=1))): print "Tweet is new, store it and sound alert." servoWiggle() playSound("audio/alert2.mp3") queuedMessage = status.text def checkMixer(): if pygame.mixer.get_init(): if(pygame.mixer.music.get_busy() == 0): print "Nothing playing, quit mixer to reduce hiss" # Wait a fraction so the sounds doesn't get clipped time.sleep(0.2) pygame.mixer.quit() def playSound(s): pygame.mixer.init() pygame.mixer.music.load(s) pygame.mixer.music.play() def doOffHook(): global queuedMessage global menuLevel # Receiver lifted. First task: play the queued message, if any... if(queuedMessage != " "): sayThis(queuedMessage) queuedMessage = " " else: # If no queued messages, do regular menu playSound("audio/welcome.mp3") time.sleep(1) playSound("audio/menu1.mp3") menuLevel = 1 def doOnHook(): global menuLevel # Receiver replaced. playSound("audio/hangup.mp3") stopStream() menuLevel = 0 time.sleep(0.5) ############################ # End functions, main loop # ############################ # Get the sound initialised and play a launch clip playSound("audio/mac_boot.wav") while True: # On a schedule, see if we have new Tweets t1 = time.time() if (t1-t0) >= 60.0: checkTwitter() t0 = t1 t1 = time.time() if (GPIO.input(cradle) == True): if(offHook == False): print "Off hook" time.sleep(0.5) offHook = True doOffHook() else: if(offHook == True): print "On hook" offHook = False doOnHook() while (GPIO.input(dial1) == False): time.sleep(0.1) # Past first position, so log it if (dialled == 0): dialled = 1 # Past second position? if (GPIO.input(dial2) == False): if (dialled < 2): dialled = 2 time.sleep(0.1) # Past third position? if (GPIO.input(dial3) == False): if (dialled < 3): dialled = 3 time.sleep(0.1) # Past fourth position? if ( (GPIO.input(dial2) == False) and (GPIO.input(dial3) == False) ): if (dialled < 4): dialled = 4 time.sleep(0.1) # Dialling no longer in process, what was selected? if (dialled > 0): print "Dialled..." print dialled doDial(dialled) dialled = 0 checkMixer() # End of run, clean up srv.stop() GPIO.cleanup()
This code should work with just about any Raspberry Pi distribution. Personally, I always use Google Coder because it’s really easy to get the Pi up and running ‘headless’, without ever connecting it to a keyboard, mouse or monitor. Even WiFi connections are automatic with Google Coder (you enter the SSID and password via a config text file on your PC/Mac) so it’s a really simple way to get up and running fast.
As soon as the Pi has booted you can access it via a browser (it announces itself at http://coder.local) and from there you can define a password which then allows SSH and SFTP connections from any machine on your network – dead easy.
Before I took the phone apart I recorded all of the original sound clips as MP3 files. I added a mode where the phone would play these clips (acting as a completely ‘virgin’ Talking Chatter phone) whenever the receiver is on the hook.
For the interactive voice menu sounds I searched Fiverr for someone who would be able to emulate the voice that Teddy Newton did in the Toy Story 3 movie. I finally hit upon the amazingly talented Chris Cowan who I commissioned to record the voice options I wanted.
He recorded all of the menu options, the “Hey, pal…” push notification alert, plus a few common messages that the phone might need to say, e.g. “Looks like rain tomorrow” (handy in Scotland) and “Woody has escaped from Sunnyside” which I use as an alert for my family when I leave work at the end of the day.
When the phone needs to say something that I haven’t already recorded as an MP3 – like the top movies this week – the phone calls the (undocumented) Google Translate text-to-speech API to get a natural voice output. Right now Google is using a female voice for that API, so I asked Chris to record a “this lady has the details” clip so that the handover would seem smooth.
I really enjoyed working on this project. Most of my weekend projects until now have been based around Arduino, but the added flexibility of the Raspberry Pi (easy network access, built-in audio and USB power output) made it a great choice for this project.
If you build something similar inspired by this project, or if you have any questions about what I’ve done, please feel free to get in touch.