2020-07-19 20:33:41 -04:00
import time
from PIL import Image , ImageFont , ImageDraw
from ST7789 import ST7789
from glob import glob
import struct
import smbus
import sys
2020-07-20 21:42:57 -04:00
import musicpd
2020-07-21 13:00:02 -04:00
from math import floor
2020-07-19 20:33:41 -04:00
class SoundslabDisplay :
2020-08-02 21:26:50 -04:00
def __init__ ( self , player_client , input_handler ) :
2020-07-19 20:33:41 -04:00
self . screen_size = 240 , 240
self . spi_speed_mhz = 80
self . st7789 = ST7789 (
rotation = 90 ,
port = 0 ,
cs = 1 ,
dc = 9 ,
backlight = 13 ,
spi_speed_hz = self . spi_speed_mhz * 1000 * 1000
)
2020-08-02 12:20:39 -04:00
self . displayOn = True
2020-07-19 20:33:41 -04:00
self . current_background = Image . new ( " RGBA " , self . screen_size , ( 0 , 0 , 255 , 255 ) )
self . current_overlay = Image . new ( " RGBA " , self . screen_size , ( 0 , 0 , 0 , 0 ) )
self . current_menu = Image . new ( " RGBA " , self . screen_size , ( 0 , 0 , 0 , 0 ) )
self . fg_color = ( 255 , 255 , 255 , 211 )
2020-07-21 15:52:00 -04:00
self . bg_color = ( 0 , 0 , 0 , 127 )
2020-07-21 21:26:21 -04:00
# the connection to mpd passed in
self . player_client = player_client
2020-08-02 21:26:50 -04:00
# the connection to the input handler passed in
self . input_handler = input_handler
2020-07-21 15:52:00 -04:00
# track battery voltage over time so we can figure out if we're charging the battery or not
self . previous_voltage = 0.0
# track currently playing song's art path to eliminate unnecessary reloads of the same image file
self . current_art_path = None
2020-07-20 21:42:57 -04:00
2020-08-02 12:20:39 -04:00
def _displayOff ( self ) :
self . st7789 . set_backlight ( 0 ) # turn off the backlight
self . st7789 . command ( 0x28 ) # turn off the display itself
self . displayOn = False
def _displayOn ( self ) :
self . st7789 . command ( 0x29 ) # turn on the display itself
self . st7789 . set_backlight ( 1 ) # turn on the backlight
self . displayOn = True
def toggleDisplayOnOff ( self ) :
if self . displayOn :
self . _displayOff ( )
else :
self . _displayOn ( )
2020-07-21 13:00:02 -04:00
def getBatteryState ( self ) :
bus = smbus . SMBus ( 1 )
address = 0x36
read = bus . read_word_data ( address , 2 )
swapped = struct . unpack ( " <H " , struct . pack ( " >H " , read ) ) [ 0 ]
voltage = swapped * 78.125 / 1000000
read = bus . read_word_data ( address , 4 )
swapped = struct . unpack ( " <H " , struct . pack ( " >H " , read ) ) [ 0 ]
capacity = swapped / 256
2020-07-21 15:52:00 -04:00
self . previous_voltage = voltage
2020-07-21 13:00:02 -04:00
return ( voltage , capacity )
2020-08-02 21:26:50 -04:00
def updateMenu ( self ) :
menu_data = self . input_handler . getMenuOptions ( )
2020-08-02 15:24:48 -04:00
# always start with a transparent menu overlay image
2020-08-02 16:14:53 -04:00
bg_color = ( 200 , 0 , 0 , 255 )
fg_color = ( 0 , 0 , 0 , 255 )
2020-08-02 15:24:48 -04:00
im_menu_overlay = Image . new ( " RGBA " , self . screen_size , ( 0 , 0 , 0 , 0 ) )
menu_overlay = ImageDraw . Draw ( im_menu_overlay )
2020-08-02 21:26:50 -04:00
if self . input_handler . state == self . input_handler . MENU and menu_data is not None :
2020-08-02 15:24:48 -04:00
# we have a menu to display
#
2020-08-02 17:36:52 -04:00
# menu_data is structured as a dict describing the menu to be displayed:
# {
# "has_previous": False,
# "has_next": True,
# "selected": 0,
# "rows": [ "row zero str", "row one str", "row two str" ]
# }
2020-08-02 15:24:48 -04:00
# a translucent red background rectangle for the menu area itself
2020-08-02 16:14:53 -04:00
menu_overlay . rectangle ( [ ( 21 , 21 ) , ( self . screen_size [ 0 ] - 21 , self . screen_size [ 1 ] - 21 ) ] , bg_color )
2020-08-02 15:24:48 -04:00
2020-08-02 17:36:52 -04:00
# set up prev indicator row
menu_overlay . rectangle ( [ ( 21 , 21 ) , ( self . screen_size [ 0 ] - 21 , 61 ) ] , bg_color )
if menu_data [ " has_previous " ] :
2020-08-02 17:45:18 -04:00
# draw triangle pointing up
2020-08-02 17:48:18 -04:00
print ( " drawing has_previous triangle " )
2020-08-02 17:45:18 -04:00
menu_overlay . polygon ( [ ( floor ( self . screen_size [ 0 ] / 2 ) - 20 , 56 ) , ( floor ( self . screen_size [ 0 ] / 2 ) , 23 ) , ( floor ( self . screen_size [ 0 ] / 2 ) + 20 , 56 ) ] , fill = fg_color )
2020-08-02 17:36:52 -04:00
# set up next indicator row
2020-08-02 17:38:16 -04:00
menu_overlay . rectangle ( [ ( 21 , self . screen_size [ 1 ] - 21 ) , ( self . screen_size [ 0 ] - 21 , self . screen_size [ 1 ] - 61 ) ] , bg_color )
2020-08-02 17:36:52 -04:00
if menu_data [ " has_next " ] :
2020-08-02 17:45:18 -04:00
# draw triangle pointing down
2020-08-02 17:48:18 -04:00
print ( " drawing has_next triangle " )
2020-08-02 17:56:13 -04:00
menu_overlay . polygon ( [ ( floor ( self . screen_size [ 0 ] / 2 ) - 20 , self . screen_size [ 1 ] - 56 ) , ( floor ( self . screen_size [ 0 ] / 2 ) , self . screen_size [ 1 ] - 23 ) , ( floor ( self . screen_size [ 0 ] / 2 ) + 20 , self . screen_size [ 1 ] - 56 ) ] , fill = fg_color )
2020-08-02 17:36:52 -04:00
# draw three menu rows
offset_from_top = 61 # start at the 21st row of pixels to draw inside the outer overlay, add 40 to account for the "prev" indicator row
2020-08-02 17:56:13 -04:00
font = ImageFont . truetype ( font = ' /usr/share/fonts/truetype/hack/Hack-Bold.ttf ' , size = 15 )
2020-08-02 17:36:52 -04:00
2020-08-02 17:41:58 -04:00
for idx , row in enumerate ( menu_data [ " rows " ] , start = 0 ) :
2020-08-02 21:26:50 -04:00
if idx == 1 :
2020-08-02 16:14:53 -04:00
menu_overlay . rectangle ( [ ( 21 , offset_from_top ) , ( self . screen_size [ 0 ] - 21 , offset_from_top + 40 ) ] , fg_color ) # highlight background for selected menu item
2020-08-02 15:24:48 -04:00
2020-08-02 17:36:52 -04:00
output_size = menu_overlay . textsize ( row ) # get the size of the text to draw so we can center it in our rectangle for this row
2020-08-02 21:26:50 -04:00
menu_overlay . text ( ( ( self . screen_size [ 0 ] / 2 ) - floor ( output_size [ 0 ] / 2 ) - 14 , offset_from_top + ( 20 - floor ( output_size [ 1 ] ) + 1 ) ) , row , font = font , fill = bg_color if idx == 1 else fg_color ) # draw output text in appropriate color
2020-08-02 15:24:48 -04:00
offset_from_top + = 40
# finally, set the current_menu image
self . current_menu = im_menu_overlay . copy ( )
2020-07-21 15:52:00 -04:00
def updateOverlay ( self ) :
2020-07-19 20:33:41 -04:00
# initialize overlay
im_overlay = Image . new ( " RGBA " , self . screen_size , ( 0 , 0 , 0 , 0 ) )
font = ImageFont . truetype ( font = ' /usr/share/fonts/truetype/hack/Hack-Bold.ttf ' , size = 18 )
2020-07-21 15:52:00 -04:00
small_font = ImageFont . truetype ( font = ' /usr/share/fonts/truetype/hack/Hack-Regular.ttf ' , size = 14 )
2020-07-19 20:33:41 -04:00
overlay = ImageDraw . Draw ( im_overlay )
# draw four rects on edges of screen_size
overlay . rectangle ( [ ( 0 , 0 ) , ( self . screen_size [ 0 ] , 20 ) ] , self . bg_color ) # top bar
overlay . rectangle ( [ ( 0 , self . screen_size [ 1 ] - 20 ) , ( self . screen_size [ 0 ] , self . screen_size [ 1 ] ) ] , self . bg_color ) # bottom bar
overlay . rectangle ( [ ( 0 , 20 ) , ( 20 , self . screen_size [ 1 ] - 20 ) ] , self . bg_color ) # left bar
overlay . rectangle ( [ ( self . screen_size [ 0 ] - 20 , 20 ) , ( self . screen_size [ 0 ] , self . screen_size [ 1 ] - 20 ) ] , self . bg_color ) # right bar
2020-07-20 21:42:57 -04:00
# get status from mpd
current_status = self . player_client . status ( )
current_track_number = int ( current_status [ ' song ' ] ) + 1
total_track_number = current_status [ ' playlistlength ' ]
track_progress_percent = float ( current_status [ ' elapsed ' ] ) / float ( current_status [ ' duration ' ] )
print ( " track progress: " + str ( track_progress_percent ) )
2020-07-19 20:33:41 -04:00
# add track # / total #
track_and_queue = str ( current_track_number ) + " of " + str ( total_track_number )
overlay . text ( ( 10 , 0 ) , track_and_queue , font = font , fill = self . fg_color )
# add battery level
2020-07-21 15:52:00 -04:00
previous_voltage = self . previous_voltage
2020-07-21 13:00:02 -04:00
( voltage , capacity ) = self . getBatteryState ( )
battery_display = ' {:02.2f} ' . format ( capacity ) + ' % '
battery_display_offset = 80
2020-07-21 15:52:00 -04:00
print ( " Battery: voltage: " + str ( voltage ) + " prev voltage: " + str ( previous_voltage ) + " capacity: " + str ( capacity ) )
2020-08-02 12:03:09 -04:00
# below method doesn't work - very inaccurate!
# should probably track over a longer period of time, which will mean
# it's harder to come up with a valid "charging or not" decision
# if voltage > previous_voltage:
# # we're probably charging!
# battery_display = 'Chrg ' + battery_display
# battery_display_offset = 130
2020-07-21 13:00:02 -04:00
overlay . text ( ( self . screen_size [ 0 ] - battery_display_offset , 0 ) , battery_display , font = font , fill = self . fg_color )
2020-07-19 20:33:41 -04:00
# add progress meter
2020-07-21 13:00:02 -04:00
overlay . rectangle ( [ ( 70 , self . screen_size [ 1 ] - 15 ) , ( self . screen_size [ 0 ] - 70 , self . screen_size [ 1 ] - 5 ) ] , fill = ( 0 , 0 , 0 , 0 ) , outline = self . fg_color , width = 1 )
overlay . rectangle ( [ ( 70 , self . screen_size [ 1 ] - 15 ) , ( int ( ( self . screen_size [ 0 ] - 140 ) * track_progress_percent ) + 70 , self . screen_size [ 1 ] - 5 ) ] , fill = self . fg_color , outline = self . fg_color , width = 1 )
# add playhead position and song duration displays at beginning and end of progress meter
( time_width , time_height ) = overlay . textsize ( text = ' 00:00 ' , font = small_font )
playhead = str ( floor ( float ( current_status [ ' elapsed ' ] ) / 60 ) ) + " : " + " {:02d} " . format ( floor ( float ( current_status [ ' elapsed ' ] ) % 60 ) )
duration = str ( floor ( float ( current_status [ ' duration ' ] ) / 60 ) ) + " : " + " {:02d} " . format ( floor ( float ( current_status [ ' duration ' ] ) % 60 ) )
overlay . text ( ( 5 , self . screen_size [ 1 ] - 20 + floor ( time_height / 2 ) ) , playhead , font = small_font , fill = self . fg_color )
overlay . text ( ( self . screen_size [ 0 ] - 5 - time_width , self . screen_size [ 1 ] - 20 + floor ( time_height / 2 ) ) , duration , font = small_font , fill = self . fg_color )
2020-07-19 20:33:41 -04:00
self . current_overlay = im_overlay . copy ( )
2020-07-20 21:42:57 -04:00
def updateAlbumArt ( self ) :
current_status = self . player_client . status ( )
song_data = self . player_client . playlistid ( current_status [ ' songid ' ] )
path_info = song_data [ 0 ] [ ' file ' ] . split ( ' / ' )
cover_image_file = ' /media/usb0/ ' + path_info [ 0 ] + ' / ' + path_info [ 1 ] + ' /cover.jpg '
2020-07-21 15:52:00 -04:00
if cover_image_file != self . current_art_path :
self . current_art_path = cover_image_file
im = Image . open ( cover_image_file ) . convert ( " RGBA " )
image = im . resize ( ( 200 , 200 ) )
# get dominant color from album art
#test_image = im.convert("RGB")
#test_image.resize((1, 1), resample=0)
#dominant_color = test_image.getpixel((0, 0))
# get top left pixel color instead and use that!
dominant_color = image . getpixel ( ( 0 , 0 ) )
backing = Image . new ( " RGBA " , self . screen_size , dominant_color )
backing . paste ( image , ( 20 , 20 ) )
self . current_background = backing . copy ( )
2020-07-19 20:33:41 -04:00
def updateDisplay ( self ) :
tempoutput = Image . alpha_composite ( self . current_background , self . current_overlay )
self . st7789 . display ( Image . alpha_composite ( tempoutput , self . current_menu ) )