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-07-21 21:26:21 -04:00
def __init__ ( self , player_client ) :
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-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-08-02 15:34:16 -04:00
# start with menus hidden
self . show_menu = False
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 15:24:48 -04:00
def updateMenu ( self , menu_data = None ) :
# 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 15:34:16 -04:00
if self . show_menu and menu_data is not None :
2020-08-02 15:24:48 -04:00
# we have a menu to display
#
# menu_data is structured as a list of dicts describing the rows to be displayed:
# [
# { output: "UP_ARROW" },
# { output: "Toggle repeat", selected: True },
# { output: "Toggle shuffle" },
# { output: "Show queue" },
# { output: "DOWN_ARROW" }
# ]
# 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
offset_from_top = 21 # start at the 21st row of pixels to draw inside the outer overlay
2020-08-02 17:04:24 -04:00
font = ImageFont . truetype ( font = ' /usr/share/fonts/truetype/hack/Hack-Bold.ttf ' , size = 14 )
2020-08-02 15:24:48 -04:00
for row in menu_data :
2020-08-02 15:57:03 -04:00
if row [ " selected " ] :
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 15:57:03 -04:00
output_size = menu_overlay . textsize ( row [ " output " ] ) # get the size of the text to draw so we can center it in our rectangle for this row
2020-08-02 16:09:43 -04:00
print ( " showing ' " + row [ " output " ] + " ' at ( " + str ( ( self . screen_size [ 0 ] / 2 ) - floor ( output_size [ 0 ] / 2 ) ) + " , " + str ( offset_from_top + 20 - floor ( output_size [ 1 ] ) ) + " ) " )
2020-08-02 16:14:53 -04:00
menu_overlay . text ( ( ( self . screen_size [ 0 ] / 2 ) - floor ( output_size [ 0 ] / 2 ) - 10 , offset_from_top + ( 20 - floor ( output_size [ 1 ] ) ) ) , row [ " output " ] , font = font , fill = bg_color if row [ " selected " ] 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 ) )