commit 1170c807ba42d79aa0ab2b5f390aaf2c3ef963ea Author: Edward Tirado Jr Date: Sun Feb 16 20:55:41 2025 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67c399d --- /dev/null +++ b/.gitignore @@ -0,0 +1,155 @@ +config.ini + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..34dc725 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Mopidy Display for Pimoni Pirate Audio + + +Shows playing track and album art from Mopidy on Pimoni Pirate Audio screen. Supports getting data from external mopidy server. + +![display example](https://i.imgur.com/F0jGexo.jpg) + +Uses the [Narifa](https://www.fontspace.com/narifah-font-f72202) font + +## Controls +A button: Volume down + +X button: Volume up + +B button: Previous Track + +Y button: Next Track diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..e3f277f --- /dev/null +++ b/config.ini.example @@ -0,0 +1,3 @@ +[pirate-display] +mopidy_host = 192.168.1.123 +mopidy_web_port = 6680 diff --git a/fonts/Narifah-EaBWz.otf b/fonts/Narifah-EaBWz.otf new file mode 100644 index 0000000..a25bed8 Binary files /dev/null and b/fonts/Narifah-EaBWz.otf differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..8730d0f --- /dev/null +++ b/main.py @@ -0,0 +1,132 @@ +import asyncio +import textwrap +from asyncio.tasks import create_task +import urllib.request +import configparser +import RPi.GPIO as GPIO + +from mopidy_asyncio_client import MopidyClient +from PIL import Image, ImageDraw, ImageFont, ImageFilter +from ST7789 import ST7789 + +class PirateDisplay(): + def __init__(self): + self.SPI_SPEED_MHZ = 80 + + self.st7789 = ST7789( + rotation=90, + port=0, + cs=1, + dc=9, + backlight=13, + spi_speed_hz=self.SPI_SPEED_MHZ * 1000 * 1000 + ) + self.buttons = [5,6,16,24] + + config = configparser.ConfigParser() + config.read("config.ini") + + self.mopidy_host = config["pirate-display"]["mopidy_host"] + self.mopidy_web_port = config["pirate-display"]["mopidy_web_port"] + + + async def connect(self): + """Create a connection to Mopidy""" + await self.setup_buttons() + self.loop = asyncio.get_running_loop() + + async with MopidyClient(host=self.mopidy_host) as mopidy: + self.running_client = mopidy + mopidy.bind('track_playback_started', self.playback_started_handler) + await self.display(mopidy) + + while True: + await asyncio.sleep(1) + + + async def setup_buttons(self): + """Add handlers to each button""" + GPIO.setmode(GPIO.BCM) + GPIO.setup(self.buttons, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + GPIO.add_event_detect(5, GPIO.FALLING, self.handle_A, bouncetime=100) + GPIO.add_event_detect(6, GPIO.FALLING, self.handle_B, bouncetime=100) + GPIO.add_event_detect(16, GPIO.FALLING,self.handle_X, bouncetime=100) + GPIO.add_event_detect(24, GPIO.FALLING, self.handle_Y, bouncetime=100) + + + def handle_A(self,pin): + """Decrease volume""" + self.loop.create_task(self.update_volume('down')) + + + def handle_B(self, pin): + """Go to previous track""" + self.loop.create_task(self.running_client.playback.previous()) + + + def handle_X(self,pin): + """Increase Volume""" + self.loop.create_task(self.update_volume('up')) + + + def handle_Y(self, pin): + """Play next track""" + self.loop.create_task(self.running_client.playback.next()) + + + async def update_volume(self, direction): + """Increase or decrease the Mopidy server volume""" + current_volume = await self.loop.create_task(self.running_client.mixer.get_volume()) + if direction == 'up': + await self.running_client.mixer.set_volume(current_volume + 15) + else: + await self.running_client.mixer.set_volume(current_volume - 15) + + + async def playback_started_handler(self, data): + """Update the diplay when the track changes""" + await self.display(self.running_client) + + + async def display(self, client): + """Generate the image to be displayed and send it to the device screen""" + current_track = await client.playback.get_current_tl_track() + + album_uri = current_track["track"]["album"]["uri"] + album_art = await client.library.get_images([album_uri]) + + # Use a black background if image isn't found + try: + image_url = f"http://{self.mopidy_host}:{self.mopidy_web_port}{album_art[album_uri][0]['uri']}" + + urllib.request.urlretrieve(image_url, "/tmp/album.jpeg") + image = Image.open("/tmp/album.jpeg") + image = image.filter(ImageFilter.GaussianBlur(5)) + image = image.resize((240,240)) + except: + image = Image.new("RGB", (240, 240), (0,0,0)) + + font = ImageFont.truetype("./fonts/Narifah-EaBWz.otf", 20) + draw = ImageDraw.Draw(image) + + # Track name + song_title = textwrap.wrap(current_track["track"]["name"], 20) + draw.multiline_text((10,20), align="left", text= "\n".join(song_title), font=font, stroke_fill=(1,103,181), stroke_width=2) + + # Artist name + artist = textwrap.wrap(current_track["track"]["artists"][0]["name"], 20) + draw.multiline_text((10,190), align="left", text="\n".join(artist), font=font, stroke_fill=(1,103,181), stroke_width=2) + + image.show() + + self.st7789.display(image) + + +if __name__ == '__main__': + pirate_display = PirateDisplay() + try: + asyncio.run(pirate_display.connect()) + except KeyboardInterrupt: + pirate_display.loop.stop() + print("Disconnected") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bb020fd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pillow +mopidy-asyncio-client diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..c9e12bd --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /home/pi/pirate-audio-display +exec python main.py diff --git a/systemd/mopidy-display.service b/systemd/mopidy-display.service new file mode 100644 index 0000000..2ae9dc2 --- /dev/null +++ b/systemd/mopidy-display.service @@ -0,0 +1,15 @@ +# Put this file in /usr/lib/systemd/system/ +# Run "systemctl enable mopidy-display" to launch on startup +[Unit] +Description=Mopidy Display +After=network-online.target time-sync.target sound.target avahi-daemon.service pulseaudio.service + +[Service] +Environment=DISPLAY=:0 +ExecStart=/home/pi/pirate-audio-display/start.sh +Restart=on-failure +User=pi +Group=pi + +[Install] +WantedBy=multi-user.target