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 # 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", read))[0] voltage = swapped * 78.125 / 1000000 read = bus.read_word_data(address, 4) swapped = struct.unpack("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 im_menu_overlay = Image.new("RGBA", self.screen_size, (0, 0, 0, 0)) menu_overlay = ImageDraw.Draw(im_menu_overlay) if self.show_menu and menu_data is not None: # 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 menu_overlay.rectangle([(21,21), (self.screen_size[0] - 21,self.screen_size[1] - 21)], (200, 0, 0, 200)) offset_from_top = 21 # start at the 21st row of pixels to draw inside the outer overlay font = ImageFont.truetype(font='/usr/share/fonts/truetype/hack/Hack-Regular.ttf', size=14) for row in menu_data: if row["selected"]: menu_overlay.rectangle([(21, offset_from_top),(self.screen_size[0] - 21, offset_from_top + 40)], self.bg_color) # highlight background for selected menu item 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 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])) + ")") menu_overlay.text(((self.screen_size[0] / 2) - floor(output_size[0] / 2), offset_from_top + (20 - floor(output_size[1]))), row["output"], font=font, fill=self.fg_color if row["selected"] else self.bg_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))