Skip to content

Instantly share code, notes, and snippets.

@maxious
Created December 11, 2018 12:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maxious/581ac7213497fa76bd71bd7b1b893448 to your computer and use it in GitHub Desktop.
Save maxious/581ac7213497fa76bd71bd7b1b893448 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ------------------------------------------------------- #
import os, threading, re, sys, time, math, unicodedata
from collections import deque
# import non-standard libraries
for libraryName in ['dothat','dothat.lcd','dothat.backlight']:
try:
libraryObject = __import__(libraryName)
except ImportError:
print("Error: unable to load library: '%s', have you installed it?" % libraryName)
print(" try: sudo pip3 install %s" % libraryName)
sys.exit(1)
else:
globals()[libraryName] = libraryObject
# ------------------------------------------------------- #
DOTHAT_LEDS = 5
DOTHAT_ROWS = 3 # number of rows for the do3k display
DOTHAT_COLUMNS = 16 # number of columns, should be the same as the cava config 'bars' setting
DOTHAT_ROW_LEVEL = 8 # LCD pixels per character (8 on my old dothat)
DOTHAT_SLEEP_TIMER = 30 # number of seconds of no output when the screen goes to sleep
MAX_CAVA_LEVEL = 96 # a nice multiple of DOTHAT_ROW_LEVEL (set at 96)
SLEEP = 0.01 # time to sleep the while True loops (0.01 is good)
METADATA_DISPLAY_TIME = 20 # time in seconds to display the metadata (20 is good)
METADATA_SCROLL_SPEED = 5 # frames/second that the metadata moves when scrolling (3-6 is good)
# ------------------------------------------------------- #
# display a warning if not root
if os.getuid() != 0:
print("Error: need to be root to access the Display-o-Tron!")
sys.exit(2)
# ------------------------------------------------------- #
class Visualiser(threading.Thread): # subclass of threading
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
# our cava command with dothat-specific setup
self.command = '/usr/local/bin/cava -p /root/.config/cava/config'
self.lock = threading.Lock()
self.fifo = deque([ [[0]*DOTHAT_COLUMNS] ], maxlen = 1) # only one at a time, avoids catchup / lag
def get_output(self): # can return NoneType (!)
with self.lock:
try: return self.fifo.popleft()[0] # return the first and only entry in the deque ...
except: pass # ... or fail gracefully
def run(self):
try:
process = os.popen(self.command,mode='r')
while True:
time.sleep(SLEEP)
output = process.readline().rstrip()
if output:
if re.match('0;{DOTHAT_COLUMNS}',output): continue # skip further processing if matches 'empty' string
matched = re.findall('(\d+)',output) # matches all digits
if matched and len(matched) == DOTHAT_COLUMNS: # should be 16 'columns' of data
for i in range(len(matched)): matched[i] = int(matched[i]) # convert string to integers
with self.lock: self.fifo.append([matched]) # append to deque
except OSError as error:
print("Error:", error)
sys.exit(1)
# ------------------------------------------------------- #
class Metadata(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
# need to install https://github.com/mikebrady/shairport-sync-metadata-reader
self.command = "/usr/local/bin/shairport-sync-metadata-reader < /tmp/shairport-sync-metadata"
self.lock = threading.Lock()
self.metadata = {'Title': '', 'Artist': ''}
def get_metadata(self):
with self.lock: return self.metadata
def run(self):
try:
process = os.popen(self.command,mode='r')
while True:
time.sleep(SLEEP)
output = process.readline().rstrip()
if output:
for key in ["Artist","Title"]:
regex = key + ': "(.*)".'
match = re.match(regex,output)
if match:
# clean the special characters from the metadata as dothat can't display "Með blóðnasir by Sigur Rós"!
clean_match = unicodedata.normalize('NFKD', match.group(1)).encode('ASCII', 'ignore').decode('UTF-8')
self.metadata[key] = clean_match
except OSError as error:
print("Error:", error)
sys.exit(1)
# ------------------------------------------------------- #
class Countdown():
def __init__(self,seconds):
self.timeout = seconds
self.timer = time.time() + self.timeout
def reset_countdown(self):
self.timer = time.time() + self.timeout
def is_elapsed(self):
if time.time() > self.timer:
return True
else:
return False
def remaining(self):
return math.floor(self.timer - time.time())
# ------------------------------------------------------- #
def initialise_dothat():
# define our special characters (level0 is the ' ' character)
# (each character is a 5x8 led matrix)
level1 = [0b00000,0b00000,0b00000,0b00000,0b00000,0b00000,0b00000,0b11111]
level2 = [0b00000,0b00000,0b00000,0b00000,0b00000,0b00000,0b11111,0b11111]
level3 = [0b00000,0b00000,0b00000,0b00000,0b00000,0b11111,0b11111,0b11111]
level4 = [0b00000,0b00000,0b00000,0b00000,0b11111,0b11111,0b11111,0b11111]
level5 = [0b00000,0b00000,0b00000,0b11111,0b11111,0b11111,0b11111,0b11111]
level6 = [0b00000,0b00000,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111]
level7 = [0b00000,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111]
level8 = [0b11111,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111]
# write the custom characters to the dothat memory (enough room for 8x characters)
# http://docs.pimoroni.com/displayotron/_modules/dothat/lcd.html#create_char
dothat.lcd.create_char(0,level1)
dothat.lcd.create_char(1,level2)
dothat.lcd.create_char(2,level3)
dothat.lcd.create_char(3,level4)
dothat.lcd.create_char(4,level5)
dothat.lcd.create_char(5,level6)
dothat.lcd.create_char(6,level7)
dothat.lcd.create_char(7,level8)
# set a decent level of contrast
# http://docs.pimoroni.com/displayotron/_modules/dothat/lcd.html#set_contrast
dothat.lcd.set_contrast(50)
# ------------------------------------------------------- #
def lcd_colour(f, index = None):
# there's a good colour change between these hues on a dothat *RBG* display
# I have an early dothat with reversed B/G channels, you may need to fiddle
# to get a nice range of colour
start = 160 # in the blue range
end = 350 # just touching the orange
step = end - start
hue = (start + (f * step)) / 360
col_rgb = dothat.backlight.hue_to_rgb(hue)
if index:
# http://docs.pimoroni.com/displayotron/_modules/dothat/backlight.html#hue
dothat.backlight.single_rgb(index, col_rgb[0], col_rgb[1], col_rgb[2], False)
else:
dothat.backlight.rgb(col_rgb[0], col_rgb[1], col_rgb[2])
# ------------------------------------------------------- #
def lcd_off():
time.sleep(0.5) # get's called a lot when sleeping, add a delay to save CPU cycles
dothat.lcd.clear() # http://docs.pimoroni.com/displayotron/_modules/dothat/lcd.html#clear
dothat.backlight.off() # http://docs.pimoroni.com/displayotron/_modules/dothat/backlight.html#off
dothat.backlight.set_graph(0) # http://docs.pimoroni.com/displayotron/_modules/dothat/backlight.html#set_graph
# ------------------------------------------------------- #
def main():
initialise_dothat() # do the initial LCD setup
lcd_timer = Countdown(DOTHAT_SLEEP_TIMER) # set a display timeout, in seconds
v = Visualiser() # setup cava visualiser process
v.start() # start the process
m = Metadata() # setup metadata reader process
m.start() # start the process
metadata_timer = Countdown(METADATA_DISPLAY_TIME) # time to display the track info for
display_text = {'Title': '','Artist': ''} # the metadata we are interested in
scroll_timer = Countdown(1 / METADATA_SCROLL_SPEED) # metadata scroll speed in seconds
scroll_string = '' # use this to store the original metadata string outside of the while loop
copy_scroll_text = False # flag used to copy the string only once
# main loop, catch Ctrl+C to exit gracefully
try:
while True:
time.sleep(SLEEP)
visualiser = v.get_output()
if visualiser: # we are capturing data through the visualiser
lcd_timer.reset_countdown() # reset the display timeout
# change the LCD backlight depending on the levels
levels_total = 0
levels = [0,0,0,0,0,0] #{'total':0, 'low':0, 'mid':0, 'high':0}
# process the output from CAVA
for column in range(DOTHAT_COLUMNS):
levels[math.floor(column/3)] += visualiser[column] /300
# able to change the colour for the 3 LCDs and the bar graph
#if column in range(0,5): levels['low'] += visualiser[column] # 5 columns
#if column in range(5,11): levels['mid'] += visualiser[column] # 6 columns
#if column in range(11,16): levels['high'] += visualiser[column] # 5 columns
# calculate the levels as a float between 0 and 1
levels_total = sum(visualiser) / (MAX_CAVA_LEVEL * DOTHAT_COLUMNS)
#levels['low'] = levels['low'] / (MAX_CAVA_LEVEL * 5) # as defined by the range, above
#levels['mid'] = levels['mid'] / (MAX_CAVA_LEVEL * 6)
#levels['high'] = levels['high'] / (MAX_CAVA_LEVEL * 5)
# update the dothat backlight
for column in range(DOTHAT_LEDS):
lcd_colour(levels[column],column)
dothat.backlight.update()
# update the bar graph (still a bit *too* bright for my taste!)
# http://docs.pimoroni.com/displayotron/_modules/dothat/backlight.html#set_graph
# dothat.backlight.set_graph(levels['total'])
# position cursor in the top left of the LCD, the rows are named 2 for the top one, and 0 for the bottom
dothat.lcd.set_cursor_position(0,0) # http://docs.pimoroni.com/displayotron/_modules/dothat/lcd.html#set_cursor_position
metadata = m.get_metadata() # get the current track info
# check for new metadata
for key in display_text.keys():
if display_text[key] != metadata[key]:
display_text[key] = metadata[key] # copy the new metadata
metadata_timer.reset_countdown() # reset the countdown to display metadata
copy_scroll_text = True # new metadata, so flag this as true
# uncomment to test the track info
if display_text['Title'] != '' or display_text['Artist'] != '':
print("Now Playing: " + display_text['Title'] + " - " + display_text['Artist'])
full_screen = None # will be set to false if displaying metadata, otherwise true to display over 3 lines
divisor_rows = None # this will be the number of rows to display over
# check to see if the timer has elapsed (or no metadata)
if metadata_timer.is_elapsed() or display_text['Title'] == '' or display_text['Artist']== '': # display full screen
full_screen = True
divisor_rows = 3
else:
full_screen = False
divisor_rows = 2
# create the LCD display output, reverse the order, i.e. 2,1,0 (2 = top line, 0 = bottom line)
for row in reversed(range(DOTHAT_ROWS)):
for column in range(DOTHAT_COLUMNS): # columns of data from 0 .. 15
divisor = MAX_CAVA_LEVEL / (DOTHAT_ROW_LEVEL * divisor_rows)
level = math.floor(visualiser[column] / divisor)
if row == 2 and not full_screen: # n.b. row '2' is the top row
display_string = display_text['Title']
if display_text['Artist']: display_string += " - " + display_text['Artist']
if len(display_string) < DOTHAT_ROWS: # pad to the length of the screen
display_string += ' ' * (DOTHAT_ROWS - (DOTHAT_ROWS - len(display_string)))
# http://docs.pimoroni.com/displayotron/_modules/dothat/lcd.html#write
# this *will* overspill onto the next line, so we need to cap to the width of the display
dothat.lcd.write( display_string[column] ) # slice to the column index
elif len(display_string) >= DOTHAT_ROWS: # scrolling display
display_string = (' ' * 3) + display_string # add blanks at the start
if copy_scroll_text == True: # flag to do this once (reset on new metadata)
scroll_string = display_string
copy_scroll_text = False
if scroll_timer.is_elapsed(): # we need this as we're in a fast while loop
# slice from the 2nd character, then add the first character to the end
scroll_string = scroll_string[1:len(scroll_string)] + scroll_string[0]
scroll_timer.reset_countdown() # reset the countdown
dothat.lcd.write( scroll_string[column] ) # slice to the column index
else: # full screen, or 2nd and 3rd lines (we scale the visualiser down with the divisor_rows variable)
if level <= (row * DOTHAT_ROW_LEVEL): # if the level is less than 0, 8 or 16 (depends on row)
character = ' ' # we don't display anything, i.e. a space character
dothat.lcd.write(character)
elif level >= (DOTHAT_ROW_LEVEL + (row * DOTHAT_ROW_LEVEL)): # if the level is above 8, 16, or 24
character = DOTHAT_ROW_LEVEL # display the maximum 'on' value
dothat.lcd.write( chr( character - 1 )) # shift to the left as zero (0) is represented with space
else:
character = level - (DOTHAT_ROW_LEVEL * row) # otherwise we display something in between...
dothat.lcd.write( chr( character - 1 )) # again shifted left by 1
else: # end of 'if visualiser:' statement
if lcd_timer.is_elapsed(): lcd_off() # switch the display off after a timeout
except KeyboardInterrupt: # catch Ctrl+C
lcd_off()
sys.exit(0)
# ------------------------------------------------------- #
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment