From 34592b6060f33ff69099046486463aba5f2d6fc4 Mon Sep 17 00:00:00 2001 From: Markus Koch Date: Sun, 5 Jul 2020 17:45:45 +0200 Subject: [PATCH] Add initial display renderer --- software/webradio.py | 217 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 software/webradio.py diff --git a/software/webradio.py b/software/webradio.py new file mode 100644 index 0000000..a5bec78 --- /dev/null +++ b/software/webradio.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python + +from PIL import Image, ImageDraw, ImageFont +import asyncio +from mpd import MPDClient + +IO_DELAY = (1.0 / 20.0) +MPD_DELAY = 1 + +class display(object): + def __init__(self): + self.fg = 1 + self.width = 128 + self.height = 32 + self.font_title = ImageFont.truetype('/usr/share/fonts/noto-cjk/NotoSansCJK-Light.ttc', 18) + self.font_info = ImageFont.truetype('/usr/share/fonts/TTF/DejaVuSans.ttf', 10) + self.img = Image.new('1', (self.width, self.height), color = not self.fg) + self.d = ImageDraw.Draw(self.img) + self.dbg = 0 + + def render(self): + print("render NYI") + + def get_image(self): + print("get_image NYI") + + def animate(self, ticks): + print("animate NYI") + + def get_image(self): + #self.img.save('pil_text_font.png') + #self.img.save("/tmp/wr/frame%d.png" % self.dbg) + print(str(len(self.img.tobytes()))) + self.dbg += 1 + +class display_playback(display): + def __init__(self): + super().__init__() + self.title = "" + self.title_scroll = 0 + self.title_scroll_max = -1 + self.info = "" + self.syms = []; + + def update_text(self, title): + self.title = title + + w,h = self.d.textsize(self.title, font=self.font_title) + if (w > self.width): + self.title += ". " # Wraparound for scrolling (pt1) + self.title_scroll_max,h = self.d.textsize(self.title, font=self.font_title) + self.title_scroll = self.title_scroll_max - 10 + self.title += self.title # Wraparound for scrolling (pt2) + else: + self.title_scroll = 0 + self.title_scroll_max = -1 + + def update_info(self, info): + self.info = info + + def update_syms(self, syms): + self.syms = syms; + + def scroll_title(self, amount = 1): + if (self.title_scroll >=0): + self.title_scroll = (self.title_scroll + amount) % self.title_scroll_max + + def animate(self, ticks): + self.scroll_title(ticks) + + def render(self): + # Clear framebuffer + self.d.rectangle((0,0,self.width,self.height), fill = not self.fg) + + # Title + self.d.text((-self.title_scroll,-5), self.title, font=self.font_title, fill=self.fg) + + # Playback info + w,h = self.d.textsize(self.info, font=self.font_info) + self.d.text((self.width-w, self.height - 1 - 10 - 1), self.info, font=self.font_info, fill=self.fg) + + # Status icons + status = "" + status = status.join([" " + icon + " " for icon in self.syms]) + self.d.text((-1, self.height - 1 - 10 - 1), status, font=self.font_info, fill=self.fg) + + # TODO: Maybe a progress bar? + #self.d.line((0,15, 127,15), fill=self.fg) + + # Serialize + # TODO: Serialize + +class display_manager(object): + def __init__(self): + self.tick = 0 + self.current_display = None + + def set_display(self, disp): + self.current_display = disp + + def render(self): + if (self.current_display): + self.current_display.render() + + def get_image(self): + if (self.current_display): + self.current_display.get_image() + else: + print("TODO: return empty image") + + def animate(self, ticks): + old_tick = self.tick + self.tick += ticks + if (self.current_display): + if (not hasattr(self.current_display, 'ltick')): + self.current_display.ltick = old_tick + self.current_display.animate(self.tick - self.current_display.ltick) + self.current_display.ltick = self.tick + # 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: + async def io_main(self): + while True: + self.disp_mgr.animate(1) + self.disp_mgr.render() + self.disp_mgr.get_image() + print("x") + await self.io_event.wait() + self.io_event.clear() + + async def io_timer_main(self): + while True: + await asyncio.sleep(IO_DELAY) + self.io_event.set() + + async def mpd_main(self): + last_title = "" + last_state = "" + last_info = "" + play_icon_map = {"play" : "▶️", "pause" : "ll", "stop" : "□"} + while True: + # Query MPD + status = self.mpd.status() + currentsong = self.mpd.currentsong() + + # Song title + try: + title = currentsong['title'] + " by " + currentsong['artist'] + except: + try: + title = currentsong['file']; + except: + title = "" + if (title != last_title): + self.disp_playback.update_text(title) + last_title = title + + # Playback state (icons) + if (status['state'] != last_state): + icons = [] + icons.append(play_icon_map.get(status["state"], "?")) + self.disp_playback.update_syms(icons) + last_state = status["state"] + + # Playback info + try: + cm, cs = divmod(round(float(status['elapsed'])), 60) + dm, ds = divmod(round(float(status['duration'])), 60) + info = "#%d/%d %d:%02d/%d:%02d" % (int(status['song']) + 1, int(status['playlistlength']), cm, cs, dm, ds) + except: + info = "Stopped" + if (info != last_info): + self.disp_playback.update_info(info) + + # Sadly I can't use mpd.idle() here + await asyncio.sleep(MPD_DELAY) + + async def main(self): + # Connect to MPD + self.mpd = MPDClient() + self.mpd.connect("yuki", 6600) + + # Set up displays + self.disp_playback = display_playback() + self.disp_playback.update_text("Loading...") + + self.disp_mgr = display_manager() + self.disp_mgr.set_display(self.disp_playback) + + # Create main tasks + self.io_event = asyncio.Event() + + io_task = asyncio.create_task(self.io_main()) + io_timer_task = asyncio.create_task(self.io_timer_main()) + mpd_task = asyncio.create_task(self.mpd_main()) + + # TODO: Wait for exit condition (shutdown command) + await asyncio.sleep(10) + + io_timer_task.cancel() + io_task.cancel() + mpd_task.cancel() + + self.mpd.close() + self.mpd.disconnect() + + def __init__(self): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + loop.run_until_complete(self.main()) + finally: + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + +r = radioserv()