soundslab/SoundslabDisplay.py

194 lines
9.9 KiB
Python
Raw Normal View History

import time
from PIL import Image, ImageFont, ImageDraw
from ST7789 import ST7789
from glob import glob
import struct
import smbus
import sys
import musicpd
from math import floor
class SoundslabDisplay:
def __init__(self, player_client):
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
)
self.displayOn = True
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)
self.bg_color = (0, 0, 0, 127)
# the connection to mpd passed in
self.player_client = player_client
# 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
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()
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
self.previous_voltage = voltage
return (voltage, capacity)
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)
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:
# 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" ]
# }
# 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 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 17:36:52 -04:00
if menu_data["selected"] == idx:
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 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 18:00:08 -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 menu_data["selected"] == idx else fg_color) # draw output text in appropriate color
offset_from_top += 40
# finally, set the current_menu image
self.current_menu = im_menu_overlay.copy()
def updateOverlay(self):
# 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)
small_font = ImageFont.truetype(font='/usr/share/fonts/truetype/hack/Hack-Regular.ttf', size=14)
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
# 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))
# 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
previous_voltage = self.previous_voltage
(voltage, capacity) = self.getBatteryState()
battery_display = '{:02.2f}'.format(capacity) + '%'
battery_display_offset = 80
print("Battery: voltage: " + str(voltage) + " prev voltage: " + str(previous_voltage) + " capacity: " + str(capacity))
# 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
overlay.text((self.screen_size[0] - battery_display_offset, 0), battery_display, font=font, fill=self.fg_color)
# add progress meter
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)
self.current_overlay = im_overlay.copy()
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'
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()
def updateDisplay(self):
tempoutput = Image.alpha_composite(self.current_background, self.current_overlay)
self.st7789.display(Image.alpha_composite(tempoutput, self.current_menu))