Inspiration
This project is an extension of the original doggy buttons project where I'll be showing how to make a public dashboard that publishes everything my dog says. This project is just one example of how to extend the features of my doggy buttons and perhaps one of the silliest IOT devices ever made. If you want to know about the buttons themselves, see the original project writeup.
With a project like this, it's also important to think about privacy. I don't want my dog making it possible to tell when we're away from home, so I'll also show how to add a delay so that activity is only published once it's sufficiently stale.
AdafruitIO Is Awesome
Free (as in beer)
This is a silly project and I can't reasonably justify spending money to publish my dog's requests online. Fortunately AdafruitIO has a very generous free tier. If at some point I want her to be able to text me, the Adafruit IO+ account is reasonably priced.
Easy
This is my first Internet Of Things (IOT) project and I was a little worried I wouldn't be able to make it work without investing waaaaay to much effort. But nope, there are excellent guides and code examples you can pretty much copy-paste onto your device to get started!
Powerful
As we'll see, AdafruitIO has all kinds of useful features. It can collect data from devices, send data to devices, and with a little cleverness, perform translation and delayed publication to a public dashboard while keeping the live feed private.
Configuring AdafruitIO
Raw Feed
The first thing we'll need is the raw feed of data from the Pi. This feed should be set to private since it sends data in real time. You'll need to at least setup a feed before you jump to the coding part because you'll need the secret key in order to publish your data. You can do the remaining setup steps before you have any actually data, or after.
But wait, you say. This is just a list of numbers! How do I know what my dog is saying?
Well, don't worry, we can perform the translation at the same time we do the delay.
Public Feed
The next thing is to set up a public feed that we can publish the translated words to and use to support the public dashboard. Every public dashboard is built on top of at least one public feed.
Actions
Now we just need to set up some actions to translate the data from the private feed and republish it to the public feed with a delay. I couldn't find a way to put complex conditional logic in an action, so I just created an action for each raw value with simple logic to translate it to text.
I have occasionally had the code that publishes the button presses mysteriously stop working, so I also created an action that functions as a dead man's switch. This will display an error message if no button presses have been detected in 24 hours.
The setup for a single translate-and-delay action looks like this:
All together, the set of actions looks like this:
Dashboard
The last thing is to setup a public dashboard where we can configure how the messages are displayed. Create a tile (I used a "stream"), connect the delayed feed, and configure the settings. Most of this is straight forward, but I've included a picture of the settings I used below in case you want to reproduce the look and feel of my dashboard.
Setup Challenges
Concurrency
The first version of my code simply added a call to aio.send_data(...)
in the same loop that processed button presses, but I quickly discovered that the upload process could take 3 - 4 seconds leading to very delayed sounds when more then one button was pressed. Things like "water outside walk later" became "water ... outside ... walk ... later". This was starting to confuse my dog because the sounds weren't immediately connected with the button presses and would sometimes sound when she had gone to push something else entirely because the button "wasn't working".
To fix this it was going to be necessary to introduce separate queues, one for playing sounds that could respond quickly, and one for uploading button presses that could slowly upload data to AdafruitIO in the background.
User Space vs Admin Space
The first version of my program was kicked off by /etc/rc.local
on boot, but this had the side effect of running the program as root
. That was fine for my original project, but I had followed best practice and not used sudo
when pip3 install
ing the Python libraries needed for loading data to AdafruitIO. If I logged in to run the program as my user, it would end when I logged off but root
no longer had all the Python libraries needed to run.
Lennart (Binary Labs) suggested the simple and elegant solution of adding sudo -u MY_PI_USERNAME
before the command that would launch my code in /etc/rc.local
. This allows the program to be run as my user without having to login and was much simpler and cleaner than the other solutions I was considering.
Code
The code below is what I currently use to have responsive buttons and upload data to AdafruitIO in the background. It's a little more advanced than the original doggy button code but it's an excellent foundation for adding additional features without disrupting the original purpose. A big thank you to Thomas over at the Embedded podcast slack for helping architect this threading system and adding a nice refactor to simplify the main loop!
#!/usr/bin/env python3 import sys import time import os import board import keypad import queue import threading from pygame import mixer ## Use the first argument as the delay wait_seconds_raw = sys.argv[1] wait_seconds = int(wait_seconds_raw) # Import Adafruit IO REST client. from Adafruit_IO import Client, RequestError ## 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_KEY_HERE' ## Set to your Adafruit IO username. ## (go to https://accounts.adafruit.com to find your username) ADAFRUIT_IO_USERNAME = 'YOUR_USERNAME_HERE' ## Create an instance of the REST client. aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) ## Sleep for a long time on bootup to give the wi-fi time to connect time.sleep(wait_seconds) ## argument via command line 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, ) event_map = { 1: sound_mommy, 5: sound_daddy, 21: sound_hmmm, 22: sound_play, 23: sound_outside, 24: sound_potty, 25: sound_water, 27: sound_walk, 26: sound_later, } def sound_player(play_queue, quit): while True: sound = play_queue.get() if quit.is_set(): return sound.play() def event_logger(log_queue, quit): while True: event_number = log_queue.get() if quit.is_set(): return aio.send_data(puppy_buttons_feed.key, event_number) log_queue = queue.Queue() play_queue = queue.Queue() quit_flag = threading.Event() threads = [threading.Thread(target=sound_player, args=(play_queue, quit_flag)), threading.Thread(target=event_logger, args=(log_queue, quit_flag))] ## Code to actually ".Start()" the threads for t in threads: t.start() try: while True: event = km.events.get() # Maybe print the event here?... if not (event and event.pressed): continue if event.key_number == 0: break if event.key_number in event_map: play_queue.put(event_map[event.key_number]) log_queue.put(event.key_number) finally: quit_flag.set() # Put "None" into the queues to wake up the threads blocking in queue.get() log_queue.put(None) play_queue.put(None) for t in threads: t.join() os.system('sudo shutdown now -h') exit(0)