From 12a4f1c67129ebab915bee797c977bf6bfc9cc33 Mon Sep 17 00:00:00 2001 From: Markus Koch Date: Fri, 21 Aug 2020 11:38:38 +0200 Subject: [PATCH] Implement playlist selection --- software/webradio.py | 238 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 196 insertions(+), 42 deletions(-) mode change 100644 => 100755 software/webradio.py diff --git a/software/webradio.py b/software/webradio.py old mode 100644 new mode 100755 index 31e2132..1081571 --- a/software/webradio.py +++ b/software/webradio.py @@ -4,10 +4,16 @@ from PIL import Image, ImageDraw, ImageFont import asyncio from mpd import MPDClient import spidev +import math +import traceback IO_DELAY = (1.0 / 18.0) MPD_DELAY = 0.5 SPI_SPEED = 500000 +IO_SPEED = 50000 + +LIVE_SELECT = 160 +LIVE_VOLUME = 40 class display(object): def __init__(self): @@ -45,9 +51,11 @@ class display_playback(display): font_shift = 5 w,h = self.d.textsize(self.title, font=self.font_title) + h = 20 # Force standard height to avoid artifacting if (w > self.width): # Check the text is short enough to be static self.title += "." w,h = self.d.textsize(self.title, font=self.font_title) + h = 26 # Force standard height to avoid artifacting w += 10 # Spacing between repetitions self.title_scroll_max = w self.title_scroll = self.title_scroll_max - 10 @@ -107,15 +115,70 @@ class display_playback(display): # TODO: Maybe a progress bar? #self.d.line((0,15, 127,15), fill=self.fg) +class display_selection(display): + def __init__(self): + super().__init__() + self.entries = [] + self.page = 0 + + def animate(self, ticks): + pass + + def render(self): + #self.img.paste(self.img_pre, (0,0)) + self.img = self.img_pre + + def set_entries(self, entries): + self.entries = entries + self.set_page(0) + + def set_page(self, page): + self.page = page + font_shift = 0 + x = 0 + y = 0 + self.img_pre = Image.new('1', (self.width, self.height), color = not self.fg) + for entry in self.entries[page*6:]: + ImageDraw.Draw(self.img_pre).text((x,y-font_shift), entry, font=self.font_info, fill=self.fg) + y += self.height / 3 + if (y >= self.height): + y = 0 + x += self.width / 2 + if (x >= self.width): + break + + def set_page_relative(self, delta): + self.page += delta + max_page = math.ceil(len(self.entries) / 6) - 1 + if (self.page < 0): + self.page = 0 + elif (self.page > max_page): + self.page = max_page + self.set_page(self.page) + + def get_entry(self, offset, page = -1): + if (page == -1): + page = self.page + try: + return self.entries[page * 6 + offset] + except: + return "" + class display_manager(object): def __init__(self): self.tick = 0 self.current_display = None - def set_display(self, disp, live=-1): + def set_display(self, disp, live = -1): self.current_display = disp self.current_display.live = live + def set_live(self, live = -1): + self.current_display.live = live + + def kill_display(self): + self.current_display.live = 0 + def display_is_dead(self): if (self.current_display): return (self.current_display.live == 0) @@ -144,55 +207,120 @@ class display_manager(object): # TODO: Consider also animating inactive screens... Might resolve some glitches when updating text while a screen is in the background (or pass `tick` to update funcs) class radioserv: - def handle_rx(self, rdata): - # Handle key presses - key_id = rdata[0] & 0x0F - key_press_short = ((rdata[0] & 0xC0) == 128) - if (key_press_short): - print("Press key: {}".format(key_id)) - if (key_id == 0): # Tune- - pass - elif (key_id == 1): # Tune+ - self.mpd.previous() - elif (key_id == 2): # Power - self.kill_event.set() - elif (key_id == 4): # Wheel - self.mpd.pause() - elif (key_id == 5): # Source - pass - elif (key_id == 6): # Info / Menu - self.mpd.next() - elif (key_id == 7): # PRESET - pass - # Handle rotary input - rotary_delta = int(rdata[1]) - if (rotary_delta > 127): - rotary_delta -= 256 + def handle_volume(self, rotary_delta, bat): if (rotary_delta != 0): - print("Rotary: {}".format(rotary_delta)) - nvol = int(self.mpd.status()['volume']) + (rotary_delta * 4) + nvol = int(self.status['volume']) + (rotary_delta * 4) if (nvol < 0): nvol = 0; elif (nvol > 100): nvol = 100 self.disp_info.update_text("{}%".format(nvol), True) - self.disp_info.update_info("Volume") - self.disp_mgr.set_display(self.disp_info, 40) + self.disp_info.update_info("Battery: {}%".format(bat)) + self.disp_mgr.set_display(self.disp_info, LIVE_VOLUME) self.mpd.setvol(nvol) - #print("Battery: {}".format(rdata[2])) + + def open_playlist_screen(self): + playlists = self.mpd.listplaylists() + entries = [] + for entry in playlists: + entries.append(entry['playlist']) + self.disp_select.set_entries(entries) + #self.disp_select.set_entries(["Test", "Foo", "Bar", "Meow", "Zzzzzz", "Yehaw!", "Trees", "Bees", "Flowers"]) + self.disp_mgr.set_display(self.disp_select, LIVE_SELECT) + + def handle_rx(self, rdata): + # Handle battery voltage + bat = int((int(rdata[2]) - 175) / 45 * 100) + if (bat < 0): + bat = 0 + elif (bat > 100): + bat = 100 + if (bat == 0): + print("Battery depleted. Shutting down.") + self.kill_event.set() + # Handle key presses + key_id = rdata[0] & 0x0F + key_press_short = ((rdata[0] & 0xC0) == 128) + # Handle rotary input + rotary_delta = int(rdata[1] & 0x1F) - 16 + if (self.disp_mgr.current_display == self.disp_playback): + if (key_press_short): + print("Press key: {}".format(key_id)) + if (key_id == 0): # Tune- + print(self.status) # TODO: Only for testing purposes. This key is still free! + print(self.currentsong) + elif (key_id == 1): # Tune+ + self.mpd.previous() + elif (key_id == 2): # Power + self.kill_event.set() + elif (key_id == 4): # Wheel + if (self.status['state'] == "stop"): + self.mpd.play(0) + else: + self.mpd.pause() + elif (key_id == 5): # Source + self.open_playlist_screen() + elif (key_id == 6): # Info / Menu + self.mpd.next() + elif (key_id == 7): # PRESET + self.mpd.random(int(not int(self.status['random']))) + self.handle_volume(rotary_delta, bat) + + elif (self.disp_mgr.current_display == self.disp_info): + self.handle_volume(rotary_delta, bat) + if (key_press_short): + self.disp_mgr.kill_display() + + elif (self.disp_mgr.current_display == self.disp_select): + if (key_press_short): + sel = -1 + + if (key_id == 0): # Tune- + sel = 2 + elif (key_id == 1): # Tune+ + sel = 1 + elif (key_id == 2): # Power + sel = 0 + elif (key_id == 4): # Wheel + sel = 100 + elif (key_id == 5): # Source + sel = 3 + elif (key_id == 6): # Info / Menu + sel = 4 + elif (key_id == 7): # PRESET + sel = 5 + + if (sel == -1): + pass + elif (sel == 100): + self.disp_mgr.kill_display() + else: + print("Load playlist {}.".format(self.disp_select.get_entry(sel))) + self.mpd.clear() + self.mpd.load(self.disp_select.get_entry(sel)) + self.mpd.play(0) + self.disp_mgr.kill_display() + + if (rotary_delta != 0): + self.disp_mgr.set_live(LIVE_SELECT) + self.disp_select.set_page_relative(rotary_delta) async def io_main(self): while True: - if self.disp_mgr.display_is_dead(): - self.disp_mgr.set_display(self.disp_playback) - self.disp_mgr.animate(1) - self.disp_mgr.render() - rdata = self.spi.xfer([100, 101, 102], 50000, 1000) - self.handle_rx(rdata) - data = list(self.disp_mgr.get_image()) - rdata = self.spi.xfer(data, SPI_SPEED, 10000) - await self.io_event.wait() - self.io_event.clear() + try: + if self.disp_mgr.display_is_dead(): + self.disp_mgr.set_display(self.disp_playback) + self.disp_mgr.animate(1) + self.disp_mgr.render() + rdata = self.spi.xfer([100, 101, 102], IO_SPEED, 1000) + self.handle_rx(rdata) + data = list(self.disp_mgr.get_image()) + rdata = self.spi.xfer(data, SPI_SPEED, 10000) + await self.io_event.wait() + self.io_event.clear() + except: + print("Error in io_main") + traceback.print_exc() async def io_timer_main(self): while True: @@ -203,12 +331,16 @@ class radioserv: last_title = "Loading..." last_state = "" last_info = "" + last_random = -1 play_icon_map = {"play" : "▶️", "pause" : "ll", "stop" : "□"} while True: # Query MPD status = self.mpd.status() currentsong = self.mpd.currentsong() + self.status = status + self.currentsong = currentsong + # Song title try: title = currentsong['title'] @@ -226,11 +358,14 @@ class radioserv: last_title = title # Playback state (icons) - if (status['state'] != last_state): + if (status['state'] != last_state or status['random'] != last_random): icons = [] icons.append(play_icon_map.get(status["state"], "?")) + if (status['random'] == '1'): + icons.append("Z") self.disp_playback.update_syms(icons) last_state = status["state"] + last_random = status["random"] # Playback info try: @@ -257,7 +392,21 @@ class radioserv: # Connect to MPD self.mpd = MPDClient() - self.mpd.connect("localhost", 6600) + retry = 15 + while retry > 0: + retry -= 1 + try: + self.mpd.connect("localhost", 6600) + break + except: + print("Error connecting to MPD. Retrying in 1s.") + await asyncio.sleep(1) + + try: + self.mpd.status() + except: + print("Could not connect to server. Exiting.") + return -1 # Set up displays self.disp_playback = display_playback() @@ -266,9 +415,14 @@ class radioserv: self.disp_info = display_playback() self.disp_info.update_text("---"); + self.disp_select = display_selection() + self.disp_mgr = display_manager() self.disp_mgr.set_display(self.disp_playback) + # Flush I/O messages + rdata = self.spi.xfer([0, 0, 0], IO_SPEED, 1000) + # Create main tasks self.io_event = asyncio.Event() self.kill_event = asyncio.Event()