I wrote Make a Mac ‘Task Done’ NeoPixel notification light just over four years ago. Not long after the post was penned, macOS’ security provisions became tighter and so the unsigned third-party kernel extensions used to drive the NeoPixel were no longer available.
That was that. I marked the post as ‘no longer working’ and put the project aside.
Skip forward four years. While reading a blog post about something else entirely, I spotted a comment referencing a new product for delivering GPIO functionality on a Mac (or other computer) via USB. It uses a Microchip MCP2221 USB interface chip; the Adafruit FT232H Breakout used in my original post was based on an FTDI part. I’d like to continue to using the FT232H because it supports SPI and the MCP2221 doesn’t, and why waste old kit if you don’t need to?
So I did a bit of digging and discovered that the FT232H isn’t quite as dead and unsupported as it was a year or so ago when I last looked.
Adafruit has added some instructions for driving the FT232H using Python, specifically its own MicroPython derivative, CircuitPython. Adafruit is putting a lot of effort into CircuitPython-compatible device driver libraries, which makes my NeoPixel notifier code more compact, streamlines the installation process and — best of all — removes the need for macOS kernel extensions.
So here is the new setup sequence. Run each step at the command line.
brew install libusb
pip3 install pyftdi adafruit-blinka adafruit-circuitpython-neopixel-spi
echo 'export BLINKA_FT232H=1' >> ~/.bash_profile
echo '0.0.0.0' > "$HOME/.status"
Note The above instructions assume you’re on a Mac; you’ll probably use apt rather than brew on another Unix-based machine, and you might not have a .bash_profile
file. For other folks, here are Windows instructions.
This is how you wire up the FT232H and the NeoPixel:
I’ve taken the opportunity to not only rewrite my notification driver code to support Adafruit’s new tools, but also to improve how it operates: it’ll do three parallel notification colours now. The code is below, but you can also find it on my GitHub repo, which also contains a bash script you can use to control the notification light in your own scripts.
Here’s the driver code:
#!/usr/bin/env python """ TaskLight - a notification light controller Requires Adafruit's FT232H Breakout (https://www.adafruit.com/product/2264) and a single NeoPixel. """ # IMPORTS import time import sys import os import board import neopixel_spi as neopixel # CONSTANTS FLASH_DELAY = 0.3 MAX_COLOURS = 3 # FUNCTIONS def clear(ps): # Turn the NeoPixel off ps[0] = (0, 0, 0) ps.show() def cycle(c): # Increment c and cycle to 0 if necessary c += 1 if c >= MAX_COLOURS: c = 0 return c # START if __name__ == '__main__': # Set up the NeoPixel array, as per the library and clear it # NOTE There's only one NeoPixel pixels = neopixel.NeoPixel_SPI(board.SPI(), 1, pixel_order = neopixel.GRB, auto_write = False) clear(pixels) # Initialize key variables filename = os.path.expanduser("~") + '/.status' brightness = 30 # Brightness control as a percentage notification = [False, False, False] colour = [0, 0, 0] count = 0 flashState = True # Run the loop while True: try: # Set the colours of each component in the NeoPixel # This will alternate between 2 and 3 colours, and # alternate one colour with black (off) numberOfAlerts = 0 for i in range(0, MAX_COLOURS): colour[i] = 0 if notification[i] is True: numberOfAlerts += 1 if i == count and flashState is True: colour[i] = 255 if numberOfAlerts == 1: flashState = not flashState count = notification.index(True) elif numberOfAlerts > 1: if not flashState: flashState = True while True: count = cycle(count) if notification[count] is True: break # Draw and display the Neopixel (first and only item in 'pixels' array) pixels[0] = (int(colour[0] * brightness / 100), int(colour[1] * brightness / 100), int(colour[2] * brightness / 100)) pixels.show() # Check the status file if os.path.exists(filename): file = open(filename) text = file.read() file.close() # File contains four values, eg. 0.0.0.0 # First is a marker for the red light (1 = on; 0 = off), etc. # Fourth is a 'stop script' marker for debugging items = text.split('.') if len(items) > MAX_COLOURS: if items[MAX_COLOURS] == '1': # Halt called, so switch off the LED and bail clear(pixels) sys.exit(0) # Check each item -- can do better error checking here! for i in range(0, MAX_COLOURS): if items[i] != '0': # Status file indicates notification light should be on # NOTE Any value other that '0' will work if notification[i] is False: notification[i] = True else: # Turn of the light if it's currently on if notification[i] is True: notification[i] = False else: # No status file - warn the user print('[ERROR] No .status file') sys.exit(1) time.sleep(FLASH_DELAY) except KeyboardInterrupt: clear(pixels) sys.exit(-1)
To run the code save it as, say, task.py and then run it in the background using
python task.py &
Here’s the controller script:
#!/usr/bin/env bash # Manage a TaskLight .status file # Version 1.0.0 # Function to show help info - keeps this out of the code function showHelp { echo -e "\ntask 1.0.0\n" echo -e "Manage a TaskLight .status file\n" echo -e "Usage:\n task.sh [-r value] [-g value] [-b value]\n" echo "Options:" echo " -r / --red [value] Set the red pixel on (1) or off (0)." echo " -g / --green [value] Set the green pixel on (1) or off (0)." echo " -b / --blue [value] Set the blue pixel on (1) or off (0)." echo " -o / --off Turn all the pixels off (eg. clear notifications)." echo " -h / --help This help screen." echo } # Initialize variables argCount=0 argIsAValue=0 red=0 blue=0 green=0 dostop=0 statusfile="$HOME/.status" # Get the current values if [ -e "$statusfile" ]; then line=$(head -n 1 "$statusfile") IFS='.' read -r -a parts <<< "$line" red=${parts[0]} green=${parts[1]} blue=${parts[2]} fi # Parse the command line arguments for arg in "$@"; do if [[ "$argIsAValue" -gt 0 ]]; then # The argument should be a value (previous argument was an option) if [[ ${arg:0:1} = "-" ]]; then # Next value is an option: ie. missing value echo "Error: Missing value for ${args[((argIsAValue - 1))]}" exit 1 fi # Set the appropriate internal value case "$argIsAValue" in 1) red=$arg ;; 2) green=$arg ;; 3) blue=$arg ;; *) echo "Error: Unknown argument"; exit 1 ;; esac argIsAValue=0 else # Make the argument lowercase arg=${arg,,} if [[ $arg = "-r" || $arg = "--red" ]]; then argIsAValue=1 elif [[ $arg = "-g" || $arg = "--green" ]]; then argIsAValue=2 elif [[ $arg = "-b" || $arg = "--blue" ]]; then argIsAValue=3 elif [[ $arg = "-s" || $arg = "--stop" ]]; then dostop=1 elif [[ $arg = "-h" || $arg = "--help" ]]; then showHelp exit 0 elif [[ $arg = "-o" || $arg = "--off" ]]; then echo "0.0.0.0." > "$statusfile" exit 0 else echo "[ERROR] Unknown switch: $arg" exit 1 fi fi ((argCount++)) if [[ "$argCount" -eq $# && "$argIsAValue" -ne 0 ]]; then echo "[Error] Missing value for $arg" exit 1 fi done # Write out the file echo "$red.$green.$blue.$dostop." > "$statusfile"
Save it as task.sh
and then make it executable with
chmod +x task.sh
You can set the light flashing red and blue by calling
./task.sh --red 1 --blue 1
Turn off blue, by calling
./task.sh --blue 0
Make all the colours flash with
./task.sh --green 1 --blue 1
And turn off the three notifications with
./task.sh -off
It’s pretty easy to control the notification lights from other applications and scripts by making calls like those presented above.
This is just one possible use of the FT232H Breakout, and I’ll be covering some further applications of this great bit of kit in future posts.