Categories
Weekend Builds

Weekend Build – Soundboard with Bluetooth Trigger

I went to a tech event and won a Raspberry Pi 3 (thanks Cisco!). That got me thinking of possible uses for this bit of kit. Something I wanted to put together for a while was a sound board for the office. Each button would play a clip pertinent to a particular person.

What I came up with took the sound board even further by adding Bluetooth based proximity detection, meaning each persons clip could play when they entered the office.

A fundamental piece of any soundboard is buttons. I ordered a selection of colored push buttons from Amazon.

I also needed a way of making sound. I had a spare speaker already, but I needed an amplifier to drive it. I found a 5v 3W amplifier on Amazon that would be more than powerful enough. As a bonus it had a volume knob that I could integrate into the system too.

I connected a 3.5mm audio jack from the Pi and soldered it on the amplifier. I hooked up the speaker and used aplay on Raspbian to load a wav file. The audio played but with a fair bit of noise. I needed to change the level of the output coming from the Pi so the signal to noise ratio was higher. Using the command amixer sset PCM,0 100% did the trick.

Next I set about making an enclosure to put all of this in. Working with 10 buttons wouldn’t be easy unless I could mount them in position first.

I created a no frills design with plenty of room to house the Pi, amplifier and speaker.

The top had holes for 10 buttons and was designed to screw in place over the base.

The top also had room for status LEDs and a grille for a microphone, which could be controlled by an 11th button on the base and be used to record clips into any of the 10 slots.

I printed both parts in black PLA plastic. The base taking a hefty 13 hours to print.

Each button would need to be wired up so that it could trigger the Raspberry Pi into playing a sound clip. One way of doing this would be for each button to be connected to an individual GPIO pin on the Pi. A slightly less obvious, yet more efficient solution is to use multiplexing.

In this arrangement the rows are each connected to a single pin and so are the columns. When the system reads a row as being activated it quickly swaps modes to then reads the columns, this then gives the X,Y coordinates for the button that was pressed. This concept is used in pinpads and some keyboards.

A fair amount of soldering later, I had the buttons all connected in a nice latticework. The buttons were wired to a prototyping shield for the Pi, these are really excellent and save a lot of time when wiring up circuits like this.

I connected the amplifier to the 5v rail, the 3 status LEDs, and added a JST connector for power. For strain relief I added a couple of zip ties.

Everything fit with room to spare. A final bit of soldering to connect the side button and speaker. I added a JST connector to a USB cable and that was used to provide power.

The top fit nicely into place and was secured with 4 self-tapping screws.

On the Pi I put together the following Python script which uses a threaded design taken from another one of my projects.

from threading import Thread
from threading import Event
from threading import Lock
from threading import current_thread
from threading import get_ident
from queue import Queue
from time import sleep 
from RPi import GPIO
import subprocess 
import errno 
import json
import signal
import sys
import os


def sigint_handler(signal, frame):
	os.system("kill -9 $(pidof aplay)")
	os.system("kill -9 $(pidof mpg123)")
	GPIO.output(PIN_GREEN, GPIO.LOW)
	GPIO.output(PIN_ORANGE, GPIO.LOW)
	GPIO.output(PIN_RED, GPIO.LOW)
	
	sys.exit(0)
	
	
def get_key():
	keyVal = None
	while keyVal == None:
	
		#print("get_key -> Setting cols to low")
		for j in range(len(COLS)):
			GPIO.setup(COLS[j], GPIO.OUT)
			GPIO.output(COLS[j], GPIO.LOW)
			
		#print("get_key -> Setting ROWSs to input")
		for i in range(len(ROWS)):
			GPIO.setup(ROWS[i], GPIO.IN, pull_up_down=GPIO.PUD_UP)
			
		rowVal = -1
		while rowVal == -1:
			for i in range(len(ROWS)):
				tmpRead = GPIO.input(ROWS[i])
				if tmpRead == 0:
					rowVal = i
					#print("get_key -> ROWS pushed ", i)
					break
					
			if rowVal == -1:
				sleep(0.008)	
				
		print("get_key -> rowVal = ", rowVal)		 
 
		#print("get_key -> Setting cols to input")
		for j in range(len(COLS)):
				GPIO.setup(COLS[j], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
	 
		GPIO.setup(ROWS[rowVal], GPIO.OUT)
		GPIO.output(ROWS[rowVal], GPIO.HIGH)

		colVal = -1
		for j in range(len(COLS)):
			tmpRead = GPIO.input(COLS[j])
			if tmpRead == 1:
				colVal=j
				break
		
		print("get_key -> colVal = ", colVal)	
				
		if colVal > -1:
			keyVal = KEYS[rowVal][colVal]
		
		if GPIO.input(PIN_REC) == 1:
			keyVal = keyVal + 100
	
	return keyVal
	
	
def wait_for_clear():	
	#print("wait_for_clear -> Setting cols to low")
	for j in range(len(COLS)):
		GPIO.setup(COLS[j], GPIO.OUT)
		GPIO.output(COLS[j], GPIO.LOW)
	 
	#print("wait_for_clear -> Setting ROWS to input")
	for i in range(len(ROWS)):
		GPIO.setup(ROWS[i], GPIO.IN, pull_up_down=GPIO.PUD_UP)
		
	rowVal = 0
	while rowVal >= 0:
		rowVal = -1
		for i in range(len(ROWS)):
			tmpRead = GPIO.input(ROWS[i])
			if tmpRead == 0:
				rowVal = i
				#print("wait_for_clear -> ROWS pushed ", i)
							
		if rowVal >= 0:
			sleep(0.1)	
			
	print("wait_for_clear -> KEYS clear")		
	
	
	
def reader(reader_q):
	pqid = 0
	exit_thread = False
	
	print("reader -> is up")

	while (exit_thread == False):
		pqid += 1
		msg = {}
		msg['id'] = pqid
		msg['key'] = get_key()
		msg['action'] = "play"

		jdata = json.dumps(msg)	  
		reader_q.put(jdata)
		wait_for_clear()
		print("reader -> got digit", msg['key'])
			
		sleep(0.05)
	
	print ("reader -> is dead")	

def ledplay(ledplay_e):
	print("ledplay -> is up")
	while True:
		if (isrunning("aplay") == True) or (isrunning("mpg123") == True):
			GPIO.output(PIN_ORANGE, GPIO.HIGH)
		else:
			GPIO.output(PIN_ORANGE, GPIO.LOW)
			
		sleep(2)
		
	print("ledplay -> is dead")
	
def isrunning(name):
	try:
		output = subprocess.check_output(["pidof",name])
		running = True 
		
		id = int(output)
		for line in open("/proc/%d/status" % id).readlines():
			if line.startswith("State:"):
				if (line.split(":",1)[1].strip().split(' ')[0] == "Z"):
					running = False
					os.system("kill -9 "+ str(id))
					
	except subprocess.CalledProcessError as e:
		output = e.output.decode()
		running = False
	return running
		
def feedback(feedback_q):
	exit_thread = False
	print("feedback -> is up")
	while (exit_thread == False):
		jdata = feedback_q.get()
		msg = json.loads(jdata)
		print("feedback -> Got msg: " + str(jdata))
		if (msg['action'] == "play"):
			print("feedback -> playing sound")
			os.system("kill -9 $(pidof aplay)")
			os.system("kill -9 $(pidof mpg123)")
			if os.path.isfile('/opt/game/sounds/' + str(msg['key']) + '.mp3'):
				proc = subprocess.Popen(['mpg123', '/opt/game/sounds/' + str(msg['key']) + '.mp3'])
			else:
				proc = subprocess.Popen(['aplay', '/opt/game/sounds/' + str(msg['key']) + '.wav'])
			
			GPIO.output(PIN_ORANGE, GPIO.HIGH)
	
	print ("feedback -> is dead")   

		
def main(reader_q, feedback_q):
	print("main -> is up")	
	while True:
		jdata = reader_q.get()
		msg = json.loads(jdata)
		feedback_q.put(jdata)
		print("main -> Got msg: " + str(jdata))

	print("main -> is dead")


print("Engine loading...")
q_reader = Queue()
q_feedback = Queue()
e_ledplay = Event()

t_main = Thread(target=main, args=(q_reader,q_feedback,))
t_reader = Thread(target=reader, args=(q_reader,))
t_feedback = Thread(target=feedback, args=(q_feedback,))
t_ledplay = Thread(target=ledplay, args=(e_ledplay,))
signal.signal(signal.SIGINT, sigint_handler)

print("Setting up GPIO...")
PIN_RED = 22
PIN_ORANGE = 4
PIN_GREEN = 27
PIN_REC = 19

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN_GREEN, GPIO.OUT)
GPIO.setup(PIN_ORANGE, GPIO.OUT)
GPIO.setup(PIN_RED, GPIO.OUT)
GPIO.setup(PIN_REC, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.output(PIN_GREEN, GPIO.HIGH)
GPIO.output(PIN_ORANGE, GPIO.LOW)
GPIO.output(PIN_RED, GPIO.LOW)

print("Setting volume...")
os.system("amixer sset PCM,0 100%")	
		
KEYS = [
	[1,6],
	[2,7],
	[3,8],
	[4,9],
	[5,10]
]
COLS = [17,18]
ROWS = [6,12,16,13,5]

print("Starting threads...")
t_main.start()
t_reader.start()
t_feedback.start()
t_ledplay.start()

The script has a thread for reading and handling the button state, handling playback, and for the status lights. Messages are passed between the threads in a JSON format.

Each button (or KEY) was given a number between 1 and 10. When a button was pressed the script would look for a corresponding wav or mp3 file in a predefined location.

A little while later I realized the Pi MK3 came with integrated Bluetooth. With this I could gather Bluetooth mac addresses (which are the physical addresses of the Bluetooth chip on your phone), and then using the hcitool utility I could try and “ping” this address. Getting a response would tell me the phone is in range.

I put together another python script to handle the Bluetooth scanning and sound triggering.

from time import sleep 
import os
import subprocess
from datetime import datetime
from time import time

targets = ['AA:AA:AA:AA:AA:AA', 'BB:BB:BB:BB:BB:BB', 'CC:CC:CC:CC:CC:CC', 'DD:DD:DD:DD:DD:DD', 'EE:EE:EE:EE:EE:EE']
names = ['andy', 'bob', 'chris', 'dave', 'erica']
found = [0, 0, 0, 0, 0]
lastplay = [0, 0, 0, 0, 0]


abort = False
while abort == False:
	check_count = 0
	time_now = time()
	for index in range(len(targets)):
		if (found[index] == 0):
			if ((time_now - lastplay[index]) > 43200):
				print("Checking for: %s" % (targets[index]))
				check_count += 1
				result = None
				result = subprocess.check_output(['hcitool',  'name', targets[index]])
				if len(result) > 0:
					print("found %s" % (names[index]))
					#print(result)
					found[index] = 1
					lastplay[index] = time_now
					if os.path.isfile('/opt/game/sounds/' + str(names[index]) + '.wav'):
						sleep(3)
						proc = subprocess.Popen(['aplay', '/opt/game/sounds/' + str(names[index]) + '.wav'])
						sleep(20)
					if os.path.isfile('/opt/game/sounds/' + str(names[index]) + '.mp3'):
						sleep(3)
						proc = subprocess.Popen(['mpg123', '/opt/game/sounds/' + str(names[index]) + '.mp3'])
						sleep(20)
	sleep(10)

	hour = datetime.now().strftime('%H')
	if int(hour) == 20:
		print("Time up. Resetting")
		for index in range(len(found)):
			found[index] = 0
		

In this way I can detect the first time the system sees a person in a given day and trigger their allocated clip. The system resets at 8pm.

It may come as a surprise to learn that without any interaction from yourself your smartphone (or other devices) can be tracked in this way. This is actually quite prevalent in the retail industry with shoppers often being tracked in real-time around stores. There was a famous story of “smart” bins tracking Londoners in the early days of smartphones.

This system worked brilliantly. The range was a good 10 meters, which was more than enough to cover our front door. I even added some delays to the audio start as it was catching people as they were still in the lift.