Hero Image

Perfecting a Pimoroni Presto Project

Since my last post about my now Playing Display code for the Pimoroni Presto, I've made some improvements to the code.

Whats changed

  • Pressing the buttons is now more responsive
  • Skip Button now skips
  • Pressing the screen when you have paused the media with resume playing
  • Album/Now Playing art now handled better
  • Text Centers better
  • Server app on the PC now running in non-debug mode.

The new code is shown below along with instructions for getting this setup.

Setting up

Getting the code to the Presto: Save the presto code to your Presto using a tool like Thonny. Save the file as main.py if you would like to to run on startup of the device.

Running the PC server code:

Clone the following repository and follow the instructions:

git clone https://github.com/vwillcox/PrestoMediaPlaying.git
cd PrestoMediaPlaying
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Now if you just want to test this you can run in debug mode:

python3 media-server.py

Now install the Presto Code to your Presto using Thonny and run Play some media on your PC (Youtube, Spotify etc) and the item should show on your Presto. If it does not you need to install one more component

ARCH Based Linux

sudo pacman -S playerctl

Debian Based Linux

sudo apt install -y playerctl

and now ensure it is running:

playerctld daemon

If all is working and you want to run the server code in a production environment, there is one more step

playerctld daemon
export FLASK_ENV=production
export FLASK_APP=media-server.py
gunicorn media-server:app --bind 0.0.0.0:5000 --daemon

Presto Code

  from presto import Presto
from picovector import ANTIALIAS_BEST, PicoVector, Polygon, Transform
from touch import Button
import urequests
import jpegdec
import gc
import time

machine.freq(200000000)
# --- Configuration ---
BACKLIGHT_RANGE = 20
BASE_URL = "http://192.168.50.173:5000"
ENDPOINTS = {
    "media": f"{BASE_URL}/now_playing",
    "art": f"{BASE_URL}/album_art",
    "toggle": f"{BASE_URL}/toggle_play",
    "skip": f"{BASE_URL}/skip",
}

# --- Hardware Setup ---
presto = Presto(ambient_light=False, full_res=True)
display = presto.display
WIDTH, HEIGHT = display.get_bounds()
CX, CY = WIDTH // 2, HEIGHT // 2
touch = presto.touch

# --- Graphics & UI Setup ---
WHITE, RED, GREEN, BLACK = (display.create_pen(r, g, b) for r, g, b in [(255, 255, 255), (230, 60, 45), (9, 185, 120), (0, 0, 0)])
vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_BEST)
vector.set_font("Roboto-Medium.af", 32)

# Define UI elements
pause_button = Button(1, HEIGHT - 50, CX - 2, 49)
skip_button = Button(WIDTH - CX, HEIGHT - 50, CX - 2, 49)
ui_elements = {
    "pause_rect": Polygon().rectangle(*pause_button.bounds, (5,)*4),
    "skip_rect": Polygon().rectangle(*skip_button.bounds, (5,)*4),
    "outline": Polygon().rectangle(5, 20, WIDTH - 10, HEIGHT - 80, (5,)*4, 2)
}

# --- Helper Functions ---
def set_backlight(level):
    """Sets the screen backlight level (0-20)."""
    presto.set_backlight(level / BACKLIGHT_RANGE)

def make_request(endpoint_key, method='get', log_errors=True):
    """Makes a request to a predefined endpoint."""
    try:
        if method == 'post':
            response = urequests.post(ENDPOINTS[endpoint_key])
        else:
            response = urequests.get(ENDPOINTS[endpoint_key])

        content = response.json() if 'json' in response.headers.get('Content-Type', '') else response.content
        response.close()
        return content
    except Exception as e:
        if log_errors:
            print(f"Request failed for '{endpoint_key}': {e}")
        return None

def draw_ui(data):
    """Draws the main user interface when media is playing."""
    display.set_pen(BLACK)
    display.clear()

    # Draw buttons and outline
    display.set_pen(GREEN)
    vector.draw(ui_elements["pause_rect"])
    display.set_pen(RED)
    vector.draw(ui_elements["skip_rect"])
    display.set_pen(WHITE)
    vector.draw(ui_elements["outline"])

    # Draw text
    def draw_centered_text(text, y):
        text_to_draw = text[:35] if len(text) > 35 else text
        _, _, w, _ = vector.measure_text(text_to_draw)
        vector.text(text_to_draw, int(CX - w / 2), y)

    draw_centered_text(f"{data.get('title', '')}", 45)
    draw_centered_text(data.get('album', ''), 80)

    # Draw button labels
    vector.text("Pause", pause_button.bounds[0] + 83, pause_button.bounds[1] + 33)
    vector.text("Skip", skip_button.bounds[0] + 83, skip_button.bounds[1] + 33)

    # Draw album art
    art_content = make_request('art', log_errors=False)
    if art_content:
        try:
            j = jpegdec.JPEG(display)
            j.open_RAM(art_content)

            # Set scale and dimensions based on the art_source flag
            if data.get("art_source") == "url":
                scale = jpegdec.JPEG_SCALE_HALF
                # If original is 640x640, scaled is 320x320
                img_width, img_height = 320, 320
            else:
                scale = jpegdec.JPEG_SCALE_FULL
                # FIX: Assume a default size for non-URL sources since we can't get dimensions
                img_width, img_height = 250, 250

            # Calculate position to center the art in the main outline area
            art_area_width = WIDTH - 10
            art_area_height = HEIGHT - 100 # This is the area between y=20 and y=HEIGHT-80
            img_x = 5 + (art_area_width - img_width) // 2
            if data.get("art_source") == "url":
                img_y = 60 + (art_area_height - img_height) // 2
            else:
                img_y = 20 + (art_area_height - img_height) // 2
            j.decode(img_x, img_y, scale, dither=True)
            del j
        except Exception as e:
            print(f"Image decode error: {e}")

    presto.update()

def handle_touch(status):
    """Handles all touch input based on player status."""
    touch.poll()
    if not touch.state:
        return

    if status == "Paused":
        #print("Screen touched while paused. Toggling play.")
        make_request('toggle', 'post')
        time.sleep(0.5) # Debounce
        return

    if pause_button.is_pressed():
        #print("Toggled play/pause via button")
        make_request('toggle', 'post')
        time.sleep(0.5) # Debounce
    elif skip_button.is_pressed():
        #print("Pressed Skip")
        make_request('skip', 'post')
        time.sleep(0.5) # Debounce

# --- Main Loop ---
set_backlight(0)
if not presto.connect():
    print("Connection failed. Halting.")
else:
    #print("Connection successful.")
    while True:
        try:
            media_data = make_request('media')
            status = media_data.get('status') if media_data else "Error"
            #print(f"Status: {status}")

            if status == "Playing":
                set_backlight(10)
                draw_ui(media_data) 
                handle_touch(status)
            else: # Paused, stopped, or error
                set_backlight(0)
                if status == "Paused":
                    handle_touch(status)
                time.sleep(0.1 if status == "Paused" else 1)

            gc.collect()

        except Exception as e:
            print(f"Main loop error: {e}")
            set_backlight(0)
            time.sleep(2)
            gc.collect()
  
  

Updated Server Code

  from flask import Flask, jsonify, send_file
import subprocess
import requests
import os
from urllib.parse import urlparse, unquote
from PIL import Image
import io

app = Flask(__name__)

def get_media_info():
    try:
        title = subprocess.check_output(['playerctl', 'metadata', 'xesam:title']).decode('utf-8').strip()
        artist = subprocess.check_output(['playerctl', 'metadata', 'xesam:artist']).decode('utf-8').strip()
        album = subprocess.check_output(['playerctl', 'metadata', 'xesam:album']).decode('utf-8').strip()
        art_url = subprocess.check_output(['playerctl', 'metadata', 'mpris:artUrl']).decode('utf-8').strip()
        status = subprocess.check_output(['playerctl','status']).decode('utf-8').strip()
        #print(art_url)
        art_path = None
        art_source = None

        if art_url.startswith("http://") or art_url.startswith("https://"):
            response = requests.get(art_url)
            if response.status_code == 200:
                with open("/tmp/album_art.jpg", "wb") as f:
                    f.write(response.content)
                art_path = "/tmp/album_art.jpg"
                art_source = "url"
        elif art_url.startswith("file:///"):
            parsed = urlparse(art_url)
            art_path = unquote(parsed.path)
            art_source = "file"

        return {
            "title": title,
            "artist": artist,
            "album": album,
            "art_path": art_path,
            "status": status,
            "art_source": art_source
        }

    except subprocess.CalledProcessError:
        return None

@app.route('/now_playing')
def now_playing():
    media_info = get_media_info()
    if media_info:
        return jsonify({
            "title": media_info["title"],
            "artist": media_info["artist"],
            "album": media_info["album"],
            "is_playing": True,
            "status": media_info["status"],
            "art_source": media_info["art_source"]
        })
    else:
        return jsonify({"error": "No media playing"}), 404

@app.route('/album_art')
def album_art():
    info = get_media_info()
    art_path = info.get("art_path") if info else None

    if art_path and os.path.exists(art_path):
        try:
            if art_path.lower().endswith((".jpg", ".jpeg")):
                return send_file(art_path, mimetype='image/jpeg')
            else:
                with Image.open(art_path) as img:
                    img = img.convert("RGB")
                    buffer = io.BytesIO()
                    img.save(buffer, format="JPEG")
                    buffer.seek(0)
                    return send_file(buffer, mimetype='image/jpeg')
        except Exception as e:
            return f"Image processing error: {e}", 500

    return "No album art", 404

@app.route('/toggle_play', methods=['POST'])
def toggle_play():
    os.system("playerctl play-pause")
    return jsonify({"status": "toggled"})

@app.route('/skip', methods=['POST'])
def skip():
    os.system("playerctl next")
    return jsonify({"Status": "skipped"})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)