Cheerlights on the Playbulb candle

Project background

For this project I had two goals in mind. Automate the $20 Playbulb bluetooth candle and test out the new Raspberry Pi 2 which was purportedly double the speed of its predecessor.

I purchased the $35 Raspberry Pi 2 which worked well for this project, however, it's horribly slow from the UI. It's practically unusable for internet browsing and everything takes a painful 3-10 secs to start after you click on it. However, it turned out to be perfect for a cheerlights server, as the UI is not needed.

Another option would have been to go with the $5 Pi zero. If you go that route, you'll need to buy some micro to USB adapters to connect the peripherals and you only get two USB ports so you'll definitely need a USB hub. It only has mini HDMI so you may need an adapter for that too which may bring the price closer to $35. However, if you already have the peripherals, you could bring cheerlights to your home for $25. Once setup, you could just leave it running as a headless cheerlights server.

Pi does not have bluetooth on board (or wifi for that matter) so I purchased the Plugable USB Bluetooth 4.0 Low Energy Micro Adapter from Amazon, which I can't say I recommend given the issues. For the wifi, I purchased the KEEBOX W150NUIEEE Wireless-N 150 USB Adapter which worked well. I'm using the NOOBS distro which is already expanded, a Sandisk class 10 card, and I'm running Jessie:

	uname -a
	Linux raspberrypi 4.1.13-v7+ #826 SMP PREEMPT Fri Nov 13 20:19:03 GMT 2015 armv7l GNU/Linux

Bluetooth fun

After I had the Pi running, I installed bluetooth and bluez:
	sudo apt-get install bluetooth bluez
And that's when I started making negative progress. I couldn't get my wireless keyboard or mouse to be identified using the hcitool:
	hcitool scan
The log showed an error:
	dmesg | grep -i blue
	[    6.018894] Bluetooth: Core ver 2.20
	[    6.019050] Bluetooth: HCI device and connection manager initialized
	[    6.019082] Bluetooth: HCI socket layer initialized
	[    6.019107] Bluetooth: L2CAP socket layer initialized
	[    6.019158] Bluetooth: SCO socket layer initialized
	[    6.071937] Bluetooth: hci0: BCM: chip id 63
	[    6.073980] Bluetooth: hci0: BCM20702A1 (001.002.014) build 0000
	[    6.077185] bluetooth hci0: Direct firmware load for brcm/BCM20702A1-0a5c-21e8.hcd failed with error -2
	[    6.077204] Bluetooth: hci0: BCM: Patch brcm/BCM20702A1-0a5c-21e8.hcd not found
	[   10.792578] Bluetooth: BNEP (Ethernet Emulation) ver 1.3
	[   10.792603] Bluetooth: BNEP filters: protocol multicast
	[   10.792630] Bluetooth: BNEP socket layer initialized
After some searching, I found a post regarding copying a new profile called fw-0a5c_21e8.hcd onto the Pi and renaming it to BCM20702A1-0a5c-21e8.hcd in the /lib/firmware/brcm folder.

That fixed the bluetooth load error but it still couldn't find my wireless keyboard or mouse so next I downloaded and built bluez 5.31 courtesy of Johnas Cukier. Thanks goes to him for that post.

	sudo apt-get install libglib2.0-dev libdbus-1-dev libudev-dev libical-dev libreadline-dev
	wget https://www.kernel.org/pub/linux/bluetooth/bluez-5.31.tar.xz
	tar xvf bluez-5.31.tar.xz
	cd bluez-5.31
	./configure --prefix=/usr --mandir=/usr/share/man --sysconfdir=/etc --localstatedir=/var --disable-systemd --enable-experimental --enable-maintainer-mode
	make
	sudo make install
	sudo cp attrib/gatttool /usr/bin
That didn't fix the keyboard and mouse issue so I decided to focus on the candle as it was now being found:
	sudo hcitool lescan
	LE Scan ...
	31:82:4B:0B:AC:E6 (unknown)
	31:82:4B:0B:AC:E6 PLAYBULB CANDLE
BTW, Blueman provides a UI to manage bluetooth devices:
	sudo apt-get install blueman

How long does it take to turn on a light bulb?

With the bluetooth able to see the candle, it was a matter of writing a small Python script to get the cheerlights color and send it to the candle. However this led to another problem, all of the examples showed writing to address 0x16 which didn't work on my Playbulb. In the end I discovered my candle's registers are offset by three for some reason, so register 0x19 is the color register. So for red it would be:
	gatttool -b 31:82:4B:0B:AC:E6 --char-write -a 0x0019 -n 00FF0000 
It turns out white is a separate byte which would be:
	gatttool -b 31:82:4B:0B:AC:E6 --char-write -a 0x0019 -n FF000000 
To turn off the effects (rainbow, flash, etc.):
	gatttool -b 31:82:4B:0B:AC:E6 --char-write -a 0x001b -n 00

Is it blown out?

One neat feature that I like about the Playbulb is you can just blow on it to turn the candle off. However, the Pi will just turn it back on so to accommodate this feature, it was necessary to determine which register to read from to determine whether the bulb should be on or off. While Wireshark would have been a good tool for this, I simply wrote a python script that continually compared values so I could identify the Playbulb's values. This wasn't perfect as the initial read did not always get good values, but it worked well enough. Using this program, I discovered that when you blow out the candle, it just sets register 0x19 to zeroes. The fix is to simply read register 0x19 before writing to it:
	gatttool -b 31:82:4B:0B:AC:E6 --char-read -a 0x0019

Retry logic

The candle has a decent bluetooth range of 25 feet or so, but it does occasionally encounter errors especially if it is far from the Pi so I added some retry logic.

	2016-02-07 11:03:37.949486> Got hex FFA500 from cheerlights API
	2016-02-07 11:03:37.950130> gatttool -b 31:82:4B:0B:AC:E6 --char-write -a 0x0019 -n 00FFA500
	2016-02-07 11:04:07.999003> gatttool -b 31:82:4B:0B:AC:E6 --char-read -a 0x0019
	2016-02-07 11:04:08.479197> connect error: Function not implemented (38)
	2016-02-07 11:04:08.479731> retrying

More than one candle?

I only have one candle so my script only updates one candle. However, if you have multiple candles, it should be a simple matter of updating the setCandle method in the script below to update all of them.

Complete Python script

And here is my complete python script. Thanks goes to Hans Scharler for creating Cheerlights!
	#This script will fetch the current color from the cheerlights API and
	#set the color of the playbulb candle.  You need to set the candle's address
	#below.  To get the address:
	#
	#sudo hcitool lescan

	addresses = ['31:82:4B:0B:AC:E6','FD:F7:4B:0D:AC:E6'] #two candles

	import requests
	import subprocess
	import shlex
	import datetime
	from subprocess import call
	from time import sleep

	def log(msg):
	    if len(msg) < 1:
	        return

	    date = datetime.datetime.now()
	    print str(date) + '> ' + msg

	#get current value to see if it has been blown out
	def checkBlown(address):
	    try:
	        cmd = 'gatttool -b ' + address + ' --char-read -a 0x0019'
	        log(cmd)
	        args = shlex.split(cmd)
	        output,error = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	        log(error)

	        if 'busy' in error or 'not implement' in error or 'timed out' in error: #retry
	            log('retrying')
	            sleep(1)
	            output,error = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	            
	            #if still failing, assume it was blown out 
	            if(error):
	                log(error)
	                return True
	            

	        ans = output.replace('Characteristic value/descriptor: ','')
	        ans = ans.replace(' \n','')
	        
	        if str(ans) == '00 00 00 00':
	            log('No update.  Candle has been blown out.')
	            return True

	        return False
	    except:
	        return True

	#set the candle
	def setCandle(address, args):
	    try:
	        cmd = 'gatttool -b ' + address + ' ' + args
	        log(cmd)
	        args = shlex.split(cmd)
	        
	        output,error = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	        log(output)
	        log(error)

	        if 'busy' in error or 'not implement' in error or 'timed out' in error: #retry
	            log('retrying')
	            sleep(1)
	            output,error = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
	            log(output)
	            log(error)
	    except:
	        pass
	    
	################################
	# main logic here
	################################
	#default candle to red on so it doesn't appear blown out on start
	for address in addresses:
	    setCandle(address, '--char-write -a 0x0019 -n 00FF0000') 

	while 1:
	    try:
	        for address in addresses:
	            ans = checkBlown(address)
	            
	            if(ans == False):
	                url = 'http://api.thingspeak.com/channels/1417/field/2/last.txt'
	                r = requests.get(url)
	                hex = r.text.replace('#','')
	                log('Got hex ' + hex + ' from cheerlights API')

	                #set the color, white is a separate byte
	                if hex == 'FFFFFF': #white?
	                    args = '--char-write -a 0x0019 -n FF000000'
	                else:
	                    args = '--char-write -a 0x0019 -n 00' + hex

	                setCandle(address, args)
	                
	        sleep(30)
	    except: #requests.ConnectionError: 
	        pass

2450 people have been romanced by the candle since 7/16.