Implement playlist selection
This commit is contained in:
parent
82614c65d7
commit
12a4f1c671
218
software/webradio.py
Normal file → Executable file
218
software/webradio.py
Normal file → Executable file
@ -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:
|
||||
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], 50000, 1000)
|
||||
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()
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user