Make a Mac ‘Task Done’ NeoPixel notification light

Update There’s now a new post containing revised, working instructions for this project.

Note This article was written some time ago, and the libraries used do not work with recent versions of macOS.

I regularly back-up my Raspberry Pi storage card because it’s so easy to damage the card with an improper shutdown or some such. I back up to a Mac, and you can read how I do it here. This wasn’t much of a chore in the early days when I was working with 4GB cards, but now I use 16GB Micro SDs and I know of folks who have much, much larger storage capacities thanks to never-cheaper cards. All this means the back-up takes a long time. So I wondered if I could create a gadget to tell me the task was done, allowing me to get on with other jobs in the meantime.

NeoPixel/FT232H Notifier
At-a-glance notification when your Pi back-up is done, anyone?

Coincidentally, I dug out an Adafruit FT232H which I’d bought some months previously but never really played with. The FT232H is a small breakout board containing an FTDI FT232H chip. FTDI specialises in USB-to-serial converters. I use a cable containing an FTDI chip to connect Raspberry Pi UART pins to host computers, for example. But the Adafruit board goes further: it uses the FTDI tech to provide not only a serial line, but also access to GPIO pins and I²C and SPI buses.

Every one of these feeds can be controlled and interacted with by code running on your computer. All you need is the right driver software, some Python code and a USB micro cable.

NeoPixel/FT232H Notifier
Adafruit’s small FT232H breakout board

I had intended to explore the FT232H, but had been held up by the need to find an application. Now I had one: my aforementioned Raspberry Pi back-up task notifier. The key was the FT232H’s SPI support — could I use it to drive a NeoPixel?

It turns out I can. I’ve some experience of driving NeoPixels — WS2812 RGB LEDs, to be accurate — via SPI, thanks to an Electric Imp Internet of Things project I did. NeoPixels have their own bus protocol, but one that’s easy to emulate using SPI. NeoPixels also run on 5V power, so they can be driven by USB.

So here’s what I built and how.

First, the circuit. I put the FT232H and a single NeoPixel on a small solderless breadboard. The latter I hooked up to the breakout’s GND and 5V pins; I wired its Data In pin to the FT232H’s D1 pin, its SPI MOSI (Master Out, Slave In) port.

NeoPixel/FT232H Notifier
FT232H and NeoPixel wired up on the breadboard

Adafruit has a great tutorial on setting up its board for use with all the major operating systems — I’ll paraphrase the Mac OS X instructions. Note that you’ll have to install Homebrew if you haven’t done so already. Run the following from the Terminal:

brew install cmake
brew install libusb
brew install swig
brew install --build-from-source libftdi
mkdir -p ~/Library/Python/2.7/lib/python/site-packages
echo '/usr/local/lib/python2.7/site-packages' >
    ~/Library/Python/2.7/lib/python/site-packages/homebrew.pth

If you plug in the FT232H now and try to run code that does anything other than communicate with the breakout by serial, it won’t work. You’ll get an “FTDI USB open error: unable to claim usb device. Make sure the default FTDI driver is not in use (-t)” error. Adafruit’s tutorial was written a couple of years ago, and Apple’s FTDI driver has changed in the interim. What you need to do is temporarily unload it, which you can do by entering this line at the Terminal prompt:

sudo kextunload -bundle-id com.apple.driver.AppleUSBFTDI

You can check it has worked by entering:

kextstat | grep -i ftdi

which should come up blank. If it doesn’t the driver is still loaded. You can re-load the driver if you need to by restarting your Mac or using:

sudo kextload -bundle-id com.apple.driver.AppleUSBFTDI

Now use your preferred text editor to enter the following Python code:

#!/usr/bin/env python

import time
import sys
import os
import Adafruit_GPIO as GPIO
import Adafruit_GPIO.FT232H as FT232H

# This class code derivaes from Adafruit's tutorial
# at https://learn.adafruit.com/adafruit-ft232h-breakout
# I had to up the SPI speed to 75MHz to work
# cu.usbserial-141

class NeoPixel_FT232H(object):
    def __init__(self, n):
        self.ft232h = FT232H.FT232H()
        self.spi = FT232H.SPI(self.ft232h, max_speed_hz=7500000, mode=0, bitorder=FT232H.MSBFIRST)
        self.buffer = bytearray(n * 24)
        self.lookup = self.build_byte_lookup()

    def build_byte_lookup(self):
        lookup = {}
        for i in range(256):
            value = bytearray()
            for j in range(7, -1, -1):
                if ((i >> j) & 1) == 0:
                    value.append(0b11100000)
                else:
                    value.append(0b11111000)
            lookup[i] = value
        return lookup

    def set_pixel_color(self, n, r, g, b):
        # Set the pixel RGB color for the pixel at position n.
        index = n * 24
        self.buffer[index :index+8 ] = self.lookup[int(g)]
        self.buffer[index+8 :index+16] = self.lookup[int(r)]
        self.buffer[index+16 :index+24] = self.lookup[int(b)]

    def show(self):
        # Send the pixel buffer out the SPI data output pin (D1)
        self.spi.write(self.buffer)

# START

if __name__ == '__main__':
    FT232H.use_FT232H()

    pixel_count = 1
    pixels = NeoPixel_FT232H(pixel_count)

    filename = '/Users/smittytone/.status' # Enter your own path here
    brightness = 30 # Brightness control as a percentage
    delay = 0.1

    redVal = 252
    greenVal = 0
    blueVal = 0

    redDel = 2
    greenDel = 2
    blueDel = 2

    redOn = True
    greenOn = False
    blueOn = False

    saveRed = 252
    saveGreen = 0
    saveBlue = 0

    noteOne = False

    print 'Ctrl-C to quit'

    while True:
        try:
            pixels.set_pixel_color(0, (redVal * brightness / 100), (greenVal * brightness / 100), (blueVal * brightness / 100))
            pixels.show()

            if noteOne:
                # Notification flag set
                if redVal != 0:
                    redVal = 0
                else:
                    redVal = 255
            else:
                # Notification flag unset; run a colour sweep effect
                if redOn:
                    redVal = redVal + redDel
                    if redVal > 254:
                        redVal = 254
                        redDel = -2
                        greenOn = True

                    if redVal < 1:
                        redDel = 2
                        redOn = False
                        redVal = 0

                if greenOn:
                    greenVal = greenVal + greenDel
                    if greenVal > 254:
                        greenDel = -2
                        blueOn = True
                        greenVal = 254

                    if greenVal < 1:
                        greenDel = 2
                        greenOn = False
                        greenVal = 0

                if blueOn:
                    blueVal = blueVal + blueDel
                    if blueVal > 254:
                        blueDel = -1
                        redOn = True
                        blueVal = 254

                    if blueVal < 1:
                        blueDel = 2
                        blueOn = False
                        blueVal = 0

            # Check file
            if os.path.exists(filename):
                file = open(filename)
                text = file.read()
                file.close()
                # File contains three values, eg. 0.0.0
                # First is a marker for the red light (1 = on; 0 = off)
                items = text.split('.')
                if items[0] != '0':
                    # Status file indicates notification light should be on
                    if noteOne == False:
                        # So turn it on only if it's currently off
                        noteOne = True
                        saveGreen = greenVal
                        greenVal = 0
                        saveRed = redVal
                        redVal = 255
                        saveBlue = blueVal
                        blueVal = 0
                        delay = 0.5
                else:
                    # Disable the notification...
                    if noteOne:
                        # ...but only if it's actually on
                        noteOne = False
                        redVal = saveRed
                        greenVal = saveGreen
                        blueVal = saveBlue
                        delay = 0.1
            else:
                # No file error - warn the user
                print 'No status file'
                noteOne = False
                redVal = saveRed
                greenVal = saveGreen
                blueVal = saveBlue
                delay = 0.1

            time.sleep(delay)
        except KeyboardInterrupt:
            for i in range(pixel_count):
                pixels.set_pixel_color(0, 0, 0, 0)
            pixels.show()
sys.exit(-1)

Next, create a hidden file called .status on your machine. Save it where you like, but make sure you enter the path correctly in the code above (line 51). Put the characters 0.0.0 in the first and only line.

Finally, edit your .bash_profile file — it’s in your home folder but hidden, so you may need to access it through the Terminal nano — and add the following lines:

alias taskdone='echo "1.0.0" > ~/.status'
alias taskclear='echo "0.0.0" > ~/.status'
alias taskset='sudo kextunload -bundle-id com.apple.driver.AppleUSBFTDI;
    python ~/Dropbox/Programming/Python/statuslight.py'

Save the file and you’re ready to go. Plug in the hardware and enter taskset at the command line. Your light should start running a sweep through the colours of the spectrum. You can test out the notification by entering:

sleep 4;taskdone

After four seconds, the light will flash red. Enter:

taskclear

to clear the notification.

Now when you come to do a backup of your Raspberry Pi’s storage card — or run any other Terminal task that takes a long time to execute — add ;taskdone to the end of the command line. For example:

sudo dd if=/dev/rdisk2 of=pi.img bs=1m ; taskdone

When the task completes, the light will flash red to let you know.

Once I’d got it working, I mounted the breadboard inside a small project box and cut a piece of translucent plastic to fit on top. The are many other ways you could mount the light on or by your computer. And the code above is easily customised for other roles and modes. You might simply want it off when there’s no notification to signal.

NeoPixel/FT232H Notifier
The Notifier looks even better when cased

I didn’t used two of the three digits in the source file in this project, but they’re there to be used, and the “0.0.0” sequence can be extended even further if you wish. You code just needs to check the appropriate entry in the item[] array and cue up a suitable notification colour. You’ll want to adapt your code to be able to signal multiple notifications, of course, so that one doesn’t ‘overwrite’ another. For example, it’s not hard to get red and blue to flash alternately, signalling two notifications.

A couple of things to note: the Apple FTDI is always loaded at a restart — hence the unload code in the taskset alias. And quitting the Python code and restarting it will generate an error unless you unplug and replug the FT232H from USB. If you forget to run taskclear before starting a new task, just open another Terminal tab and enter taskclear there.

NeoPixel/FT232H Notifier

I bought my Adafruit FT232H from Pimoroni. Ditto the NeoPixels