Implement playlist selection

This commit is contained in:
Markus Koch 2020-08-21 11:38:38 +02:00
parent 82614c65d7
commit 12a4f1c671

216
software/webradio.py Normal file → Executable file
View File

@ -4,10 +4,16 @@ from PIL import Image, ImageDraw, ImageFont
import asyncio import asyncio
from mpd import MPDClient from mpd import MPDClient
import spidev import spidev
import math
import traceback
IO_DELAY = (1.0 / 18.0) IO_DELAY = (1.0 / 18.0)
MPD_DELAY = 0.5 MPD_DELAY = 0.5
SPI_SPEED = 500000 SPI_SPEED = 500000
IO_SPEED = 50000
LIVE_SELECT = 160
LIVE_VOLUME = 40
class display(object): class display(object):
def __init__(self): def __init__(self):
@ -45,9 +51,11 @@ class display_playback(display):
font_shift = 5 font_shift = 5
w,h = self.d.textsize(self.title, font=self.font_title) 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 if (w > self.width): # Check the text is short enough to be static
self.title += "." self.title += "."
w,h = self.d.textsize(self.title, font=self.font_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 w += 10 # Spacing between repetitions
self.title_scroll_max = w self.title_scroll_max = w
self.title_scroll = self.title_scroll_max - 10 self.title_scroll = self.title_scroll_max - 10
@ -107,6 +115,55 @@ class display_playback(display):
# TODO: Maybe a progress bar? # TODO: Maybe a progress bar?
#self.d.line((0,15, 127,15), fill=self.fg) #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): class display_manager(object):
def __init__(self): def __init__(self):
self.tick = 0 self.tick = 0
@ -116,6 +173,12 @@ class display_manager(object):
self.current_display = disp self.current_display = disp
self.current_display.live = live 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): def display_is_dead(self):
if (self.current_display): if (self.current_display):
return (self.current_display.live == 0) 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) # 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: class radioserv:
def handle_rx(self, rdata): def handle_volume(self, rotary_delta, bat):
# 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
if (rotary_delta != 0): if (rotary_delta != 0):
print("Rotary: {}".format(rotary_delta)) nvol = int(self.status['volume']) + (rotary_delta * 4)
nvol = int(self.mpd.status()['volume']) + (rotary_delta * 4)
if (nvol < 0): if (nvol < 0):
nvol = 0; nvol = 0;
elif (nvol > 100): elif (nvol > 100):
nvol = 100 nvol = 100
self.disp_info.update_text("{}%".format(nvol), True) self.disp_info.update_text("{}%".format(nvol), True)
self.disp_info.update_info("Volume") self.disp_info.update_info("Battery: {}%".format(bat))
self.disp_mgr.set_display(self.disp_info, 40) self.disp_mgr.set_display(self.disp_info, LIVE_VOLUME)
self.mpd.setvol(nvol) 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): async def io_main(self):
while True: while True:
try:
if self.disp_mgr.display_is_dead(): if self.disp_mgr.display_is_dead():
self.disp_mgr.set_display(self.disp_playback) self.disp_mgr.set_display(self.disp_playback)
self.disp_mgr.animate(1) self.disp_mgr.animate(1)
self.disp_mgr.render() self.disp_mgr.render()
rdata = self.spi.xfer([100, 101, 102], 50000, 1000) rdata = self.spi.xfer([100, 101, 102], IO_SPEED, 1000)
self.handle_rx(rdata) self.handle_rx(rdata)
data = list(self.disp_mgr.get_image()) data = list(self.disp_mgr.get_image())
rdata = self.spi.xfer(data, SPI_SPEED, 10000) rdata = self.spi.xfer(data, SPI_SPEED, 10000)
await self.io_event.wait() await self.io_event.wait()
self.io_event.clear() self.io_event.clear()
except:
print("Error in io_main")
traceback.print_exc()
async def io_timer_main(self): async def io_timer_main(self):
while True: while True:
@ -203,12 +331,16 @@ class radioserv:
last_title = "Loading..." last_title = "Loading..."
last_state = "" last_state = ""
last_info = "" last_info = ""
last_random = -1
play_icon_map = {"play" : "▶️", "pause" : "ll", "stop" : ""} play_icon_map = {"play" : "▶️", "pause" : "ll", "stop" : ""}
while True: while True:
# Query MPD # Query MPD
status = self.mpd.status() status = self.mpd.status()
currentsong = self.mpd.currentsong() currentsong = self.mpd.currentsong()
self.status = status
self.currentsong = currentsong
# Song title # Song title
try: try:
title = currentsong['title'] title = currentsong['title']
@ -226,11 +358,14 @@ class radioserv:
last_title = title last_title = title
# Playback state (icons) # Playback state (icons)
if (status['state'] != last_state): if (status['state'] != last_state or status['random'] != last_random):
icons = [] icons = []
icons.append(play_icon_map.get(status["state"], "?")) icons.append(play_icon_map.get(status["state"], "?"))
if (status['random'] == '1'):
icons.append("Z")
self.disp_playback.update_syms(icons) self.disp_playback.update_syms(icons)
last_state = status["state"] last_state = status["state"]
last_random = status["random"]
# Playback info # Playback info
try: try:
@ -257,7 +392,21 @@ class radioserv:
# Connect to MPD # Connect to MPD
self.mpd = MPDClient() self.mpd = MPDClient()
retry = 15
while retry > 0:
retry -= 1
try:
self.mpd.connect("localhost", 6600) 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 # Set up displays
self.disp_playback = display_playback() self.disp_playback = display_playback()
@ -266,9 +415,14 @@ class radioserv:
self.disp_info = display_playback() self.disp_info = display_playback()
self.disp_info.update_text("---"); self.disp_info.update_text("---");
self.disp_select = display_selection()
self.disp_mgr = display_manager() self.disp_mgr = display_manager()
self.disp_mgr.set_display(self.disp_playback) 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 # Create main tasks
self.io_event = asyncio.Event() self.io_event = asyncio.Event()
self.kill_event = asyncio.Event() self.kill_event = asyncio.Event()