Inspiration
This project was born from two desires. One, see if our dog could be taught to use buttons without spending piles of money. Two, to make use of an ancient Raspberry Pi.
Lots of people have done a project like this, and it isn’t necessary to use a Raspberry Pi. That just happened to be the only unused piece of electronics I had on hand with audio output. But, I will say that I definitely recommend this approach for a couple reasons. Firstly, it’s very easy to get running in Python with Adafruit’s Blinka library. Second, it leaves lots of room for growth (both in number of buttons and features like button presses sending digital messages).
Be sure to read all the way to the bottom for a cute puppy photo!
Materials Used
-
Raspberry Pi (low cost model A)
- Gifted by a coworker who was going to throw it out
-
16GB (full size) SD card
- Came with my 3D printer, but is overkill for that purpose
-
Audio cable
- From a box of junk computer parts
-
Audio amplifier
- Included in a box of junk that got shipped to me
-
Speakers
- From an old CRT TV
-
Jumper wires
- Reused from multiple projects
-
Keyboard breakout
- Hand made from salvaged diodes, jumper headers, perf-board, wires, and solder. Let's call it 50% recycled.
-
3D printed buttons (and some hotglue)
- Technically new, but I don't count 3D prints if they displace buying other plastic
-
Electronic buttons inside the prints (also a boot button shutdown switch)
- All salvaged from ewaste or otherwise discarded
-
Wooden mounting board
- Had to buy it from a hobby store
Salvage percentage ~83%! Hard to give an exact number... do you measure by count? By volume? By mass? By value? Do you count new tools you had to buy? Whatever, I'm going to call it 83%.
3D Files
Rather than reinvent the wheel, I found a clever 3D printable button that only needs at tiny electronic button added at the bottom. I did have to modify the design slightly to add a lip to prevent it falling through the board and add an extra ring inside the base plate to prevent the button from jamming when pressed at an angle. I also made a tiny cylinder to glue onto the button plunger to extend the length if needed. The links to my modified versions are below.
Setup Challenges
Ancient Raspberry Pi's are great for this type of project because they're relatively low power (only about 0.5A @ 5V), but they do have some development challenges.
Chicken and egg
The earliest Pis didn't have a wi-fi chip built in, you had to plug in an ethernet cable or USB wi-fi adapter. At some point, they also released a cost-reduced version of this Pi with no ethernet adapter and only one USB port instead of two. This makes setup very challenging. If you want to update the OS and install packages (like PIP), you'll need a USB wi-fi adapter plugged in and a USB keyboard from which to type the commands. Here are a few reasonable options I found while dealing with these devices:
- (beginner) Just get a USB splitter. But make sure it's a powered USB splitter because the Pi can only supply enough juice to power one device (and wi-fi adapters tend to be power hungry).
- (intermediate) Use and FTDI adapter to connect to the UART pins and connect over serial to type your commands. This leaves the one USB port available for that wi-fi adapter.
- (intermediate) If you happen to have a wi-fi adapter that works with Raspbian out-of-the-box, you could SSH into the pi from another computer and type your commands that way.
- (advanced) Use the desktop version of 32-bit Buster to ensure the PPP package is available. Use the UART pins to connect over serial, then use PPP to share your internet connection over serial. Once you've successfully updated your packages and transferred your files, clean up as much of the desktop bloat as possible. I strongly recommend avoiding this process if at all possible. The only reason to consider it is if you can't find a compatible wi-fi adapter or need to modify things to get your USB wi-fi adapter working with Raspbian. I never actually managed to get this process fully working, so if you go this route, good luck!
Beautiful chaos
I chose to keep things accessible from the underside of the board. From this angle, I can point out some of the more interesting/useful features.
The teal thing connected via USB is a wi-fi dongle.
The small perf-board with a bunch of wires going to it is the common ground for the buttons (only needed in direct wiring).
Power and audio cables are routed out a hole in the side for convenience.
The "shutdown" button is the white button at the edge of the board. The red button forces a reboot (even it it's already on). The shutdown button is handled by the same software that detects other button presses, but the reboot is done with hardware (see this guide). That makes the reboot button generally unsafe, but I added it because I got tired of having to disconnect the whole device from power after a shutdown and just wanted to be able to press a button to turn it back on.
Love (or hate) the command line
Either way, it's what you'll be using. These early Pis are powerful enough to process audio and connect to wi-fi, but you'll be squandering their finite capabilities if you ask them to also support a full desktop environment. But that's ok, you only really need to do a few things to get this running:
- Use the 32-bit Bullseye Lite OS image
- Solve your chicken and egg problem so you can update packages
- Transfer your files. Either wirelessly with something like PSFTP or with a USB flash drive (but you may have to manually mount it).
- Edit
/etc/rc.local
so your python script starts on bootup.
How Many Buttons?
There's a number of different ways to support buttons. The simplest one is a direct wiring of buttons to pins. This makes the code very simple (see Version 1 below), but limits you to about 13 buttons if you avoid using the I2C and UART pins. You'll also need to tie the grounds together since there aren't enough ground pins for that many buttons.
If you need more, but don't want to roll your own input monitoring and debounce drivers, the CircuitPython keyboard library will let you support up to 6 x 7 = 42 buttons. I'm currently working on the code for this, so I'll add a version 2 code section once it's working and tested.
More Buttons?
Why might you need more than 42 buttons? Easy, if your dog learns words at the rate of 1 a month, your puppy will need more buttons after only 3.5 years! At the moment, the available libraries aren't really designed to support this many buttons from only 13 pins, but if you don't mind writing your own drivers, here are some advanced options to support a lifetime's worth of buttons:
- Use an army of shift registers to extend the number of GPIO. The good news is you can still use the keyboard library. The bad news is you'll need a shift register for every 8 buttons.
- Charlieplex the inputs. This technique lets you support N * (N - 1) buttons from N pins, so in my case, that's 13 * 12 = 156. That's a lot of buttons! But in addition to needing custom drivers, it requires a small mountain of diodes and a complex wiring arrangement.
- Use SPI or I2C GPIO expanders. Just be sure to get one that can handle the matrix keyboard, queuing, and debounce stuff for you (see the guide below), otherwise you'll have to develop that yourself. The TCA8418 is a good choice, but since it can only have one address you'll need to use two independent I2C buses if you need more than 80 buttons.
- Use another Pi or another microcontroller. If you only need 2x your maximum number of buttons, why not just use two identical units running very similar programs? This is a simple solution, but it makes combining the audio output more complicated if you want to route both outputs to a single set of speakers. Just wiring the outputs together is likely to create a ground loop and generate noise on your speakers unless you have a special connector that is designed to combine audio signals.
#! /usr/bin/python import atexit, pickle, socket, time, subprocess import os, sys, digitalio, busio, pwmio, adafruit_ssd1306, board ## PG added from board import SCL, SDA import datetime as dt ## PG added from PIL import Image, ImageDraw, ImageFont ## PG added from adafruit_debouncer import Debouncer ## PG added from pygame import mixer ## Load the sounds: mixer.init() sound_applause = mixer.Sound('/home/pi/pianobar/wav_files/applause-1.wav') sound_play = mixer.Sound('/home/pi/pianobar/wav_files/play.wav') sound_potty = mixer.Sound('/home/pi/pianobar/wav_files/potty.wav') sound_water = mixer.Sound('/home/pi/pianobar/wav_files/water.wav') sound_walk = mixer.Sound('/home/pi/pianobar/wav_files/walk.wav') sound_later = mixer.Sound('/home/pi/pianobar/wav_files/later.wav') sound_anabelle = mixer.Sound('/home/pi/pianobar/wav_files/anabelle.wav') sound_outside = mixer.Sound('/home/pi/pianobar/wav_files/outside.wav') sound_hmmm = mixer.Sound('/home/pi/pianobar/wav_files/hmmm.wav') HOLD_TIME = 1.0 # Time (seconds) to hold select button for shut down def make_pin_reader(pin): io = digitalio.DigitalInOut(pin) io.direction = digitalio.Direction.INPUT io.pull = digitalio.Pull.UP return lambda: io.value ## define debounced buttons D4_TBD1 = Debouncer(make_pin_reader(board.D4), interval=0.02) D7_shutdown = Debouncer(make_pin_reader(board.D7), interval=0.02) #CS1 D8_TBD2 = Debouncer(make_pin_reader(board.D8), interval=0.02) #CS0 D9_TBD3 = Debouncer(make_pin_reader(board.D9), interval=0.02) #MISO D10_applause = Debouncer(make_pin_reader(board.D10), interval=0.02) #MOSI D11_play = Debouncer(make_pin_reader(board.D11), interval=0.02) #SCLK D17_potty = Debouncer(make_pin_reader(board.D17), interval=0.02) D18_water = Debouncer(make_pin_reader(board.D18), interval=0.02) D22_walk = Debouncer(make_pin_reader(board.D22), interval=0.02) D23_later = Debouncer(make_pin_reader(board.D23), interval=0.02) D24_anabelle = Debouncer(make_pin_reader(board.D24), interval=0.02) D25_outside = Debouncer(make_pin_reader(board.D25), interval=0.02) D27_hmmm = Debouncer(make_pin_reader(board.D27), interval=0.02) t2 = 0 continue_looping = True while continue_looping: D4_TBD1.update() D7_shutdown.update() D8_TBD2.update() D9_TBD3.update() D10_applause.update() D11_play.update() D17_potty.update() D18_water.update() D22_walk.update() D23_later.update() D24_anabelle.update() D25_outside.update() D27_hmmm.update() if D4_TBD1.fell: t2 = time.time() # if D7_shutdown.fell: # Handled later if D8_TBD2.fell: t2 = time.time() if D9_TBD3.fell: t2 = time.time() if D10_applause.fell: sound_applause.play() if D11_play.fell: sound_play.play() if D17_potty.fell: sound_potty.play() if D18_water.fell: sound_water.play() if D22_walk.fell: sound_walk.play() if D23_later.fell: sound_later.play() if D24_anabelle.fell: sound_anabelle.play() if D25_outside.fell: sound_outside.play() if D27_hmmm.fell: sound_hmmm.play() if D7_shutdown.fell: t = time.time() while not D7_shutdown.value: D7_shutdown.update() if (time.time() - t) >= HOLD_TIME: t2 = time.time() continue_looping = False break # needed in case button isn't released if not continue_looping: break time.sleep(1) os.system('sudo shutdown now -h') exit(0)
Version 2 (matrix keyboard)
This is the 1st draft of the matrix keyboard code. Only the currently used sounds are supported and some buttons had to be mapped to unusual pins because of a hardware issue. It also includes some experimental code for uploading the button presses to Adafruit IO. More on that coming soon ;-)
#! /usr/bin/python import atexit, pickle, socket, time, subprocess import os, sys, digitalio, busio, adafruit_ssd1306, board import keypad from board import SCL, SDA from adafruit_debouncer import Debouncer ## maybe no longer needed??? from pygame import mixer # Import Adafruit IO REST client. from Adafruit_IO import Client, Feed, Data, RequestError import datetime ## Set to your Adafruit IO key. ## Remember, your key is a secret, ## so make sure not to publish it when you publish this code! ADAFRUIT_IO_KEY = 'YOUR_AIO_KEY' ## Set to your Adafruit IO username. ## (go to https://accounts.adafruit.com to find your username) ADAFRUIT_IO_USERNAME = 'YOUR_AIO_USERNAME' ## Create an instance of the REST client. aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) try: puppy_buttons_feed = aio.feeds('puppy-buttons') except RequestError: print('could not find specified feed') ## Load the sounds: mixer.init() sound_applause = mixer.Sound('/home/pi/pianobar/wav_files/applause-1.wav') sound_play = mixer.Sound('/home/pi/pianobar/wav_files/play.wav') sound_potty = mixer.Sound('/home/pi/pianobar/wav_files/potty.wav') sound_water = mixer.Sound('/home/pi/pianobar/wav_files/water.wav') sound_walk = mixer.Sound('/home/pi/pianobar/wav_files/walk.wav') sound_later = mixer.Sound('/home/pi/pianobar/wav_files/later.wav') sound_anabelle = mixer.Sound('/home/pi/pianobar/wav_files/anabelle.wav') sound_outside = mixer.Sound('/home/pi/pianobar/wav_files/outside.wav') sound_hmmm = mixer.Sound('/home/pi/pianobar/wav_files/hmmm.wav') sound_daddy = mixer.Sound('/home/pi/pianobar/wav_files/daddy.wav') sound_mommy = mixer.Sound('/home/pi/pianobar/wav_files/mommy.wav') sound_all_done = mixer.Sound('/home/pi/pianobar/wav_files/all-done-happy-v2.wav') sound_ball = mixer.Sound('/home/pi/pianobar/wav_files/ball.wav') sound_bed_time = mixer.Sound('/home/pi/pianobar/wav_files/bed-time.wav') sound_cuddles = mixer.Sound('/home/pi/pianobar/wav_files/cuddles.wav') sound_downstairs = mixer.Sound('/home/pi/pianobar/wav_files/downstairs.wav') sound_hard = mixer.Sound('/home/pi/pianobar/wav_files/hard-best-v4.wav') sound_lazer = mixer.Sound('/home/pi/pianobar/wav_files/lazer.wav') sound_lila = mixer.Sound('/home/pi/pianobar/wav_files/lila.wav') sound_love_you = mixer.Sound('/home/pi/pianobar/wav_files/love-you.wav') sound_nap_time = mixer.Sound('/home/pi/pianobar/wav_files/nap-time.wav') sound_noise = mixer.Sound('/home/pi/pianobar/wav_files/noise.wav') sound_pets = mixer.Sound('/home/pi/pianobar/wav_files/pets.wav') sound_soft = mixer.Sound('/home/pi/pianobar/wav_files/soft.wav') sound_thea = mixer.Sound('/home/pi/pianobar/wav_files/thea.wav') sound_tug = mixer.Sound('/home/pi/pianobar/wav_files/tug.wav') sound_upstairs = mixer.Sound('/home/pi/pianobar/wav_files/upstairs.wav') sound_want = mixer.Sound('/home/pi/pianobar/wav_files/want.wav') sound_yard = mixer.Sound('/home/pi/pianobar/wav_files/yard.wav') km = keypad.KeyMatrix( row_pins=(board.D4, board.D7, board.D8, board.D11, board.D9, board.D10), column_pins=(board.D17, board.D18, board.D27, board.D22, board.D23, board.D24, board.D25), columns_to_anodes=True, ) while True: event = km.events.get() if event: print(event) value_to_send = event if event.pressed == True and event.key_number == 26: sound_later.play() if event.pressed == True and event.key_number == 33: sound_water.play() if event.pressed == True and event.key_number == 31: sound_walk.play() if event.pressed == True and event.key_number == 24: sound_potty.play() if event.pressed == True and event.key_number == 23: sound_outside.play() if event.pressed == True and event.key_number == 22: sound_play.play() if event.pressed == True and event.key_number == 21: sound_hmmm.play() if event.pressed == True and event.key_number == 5: sound_daddy.play() if event.pressed == True and event.key_number == 1: sound_mommy.play() ## Actually send the data aio.send_data(puppy_buttons_feed.key, value_to_send) if event.pressed == True and event.key_number == 0: break os.system('sudo shutdown now -h') exit(0)
There's a lot of advice online that says to start with "treat". I recommend ignoring that advice. For one, that only teaches them to push a button, and you can do that without making the button "treat". Secondly, if your dog is even slightly food motivated, it'll be the only button they want to press. And good luck taking that button away later.
Start with "potty" or whatever word you use for that. It will likely be the single most useful button they ever learn. Reward your dog for showing any interest in the button board when it's introduced. Reward your dog for interacting with the button. When ever you take the dog outside to potty, first go over and press the button, then take your dog out to potty.
The hardest part is helping them understand that they can press the button. They'll learn pretty quickly that you pressing the button is part of the going potty ritual, but helping them understand that they can make that request via the button is what takes time.
Once they've mastered "potty", you can start introducing other words, one at a time. Dogs don't have the same color perception that we do and generally learn the buttons by their relative position, so you're likely to confuse them if you rearrange the buttons.