Implement playlist selection
This commit is contained in:
parent
82614c65d7
commit
12a4f1c671
238
software/webradio.py
Normal file → Executable file
238
software/webradio.py
Normal file → Executable 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,15 +115,70 @@ 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
|
||||||
self.current_display = None
|
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 = 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:
|
||||||
if self.disp_mgr.display_is_dead():
|
try:
|
||||||
self.disp_mgr.set_display(self.disp_playback)
|
if self.disp_mgr.display_is_dead():
|
||||||
self.disp_mgr.animate(1)
|
self.disp_mgr.set_display(self.disp_playback)
|
||||||
self.disp_mgr.render()
|
self.disp_mgr.animate(1)
|
||||||
rdata = self.spi.xfer([100, 101, 102], 50000, 1000)
|
self.disp_mgr.render()
|
||||||
self.handle_rx(rdata)
|
rdata = self.spi.xfer([100, 101, 102], IO_SPEED, 1000)
|
||||||
data = list(self.disp_mgr.get_image())
|
self.handle_rx(rdata)
|
||||||
rdata = self.spi.xfer(data, SPI_SPEED, 10000)
|
data = list(self.disp_mgr.get_image())
|
||||||
await self.io_event.wait()
|
rdata = self.spi.xfer(data, SPI_SPEED, 10000)
|
||||||
self.io_event.clear()
|
await self.io_event.wait()
|
||||||
|
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()
|
||||||
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
|
# 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()
|
||||||
|
Loading…
Reference in New Issue
Block a user