Using the Pimoroni Presto to display what's playing

cc_rider

Major Contributor
Joined
Oct 20, 2022
Messages
1,344
This is a slick little device, very much ups the game for displays with embedded controllers. Out of stock at the moment, I'm guessing it was much more popular than they anticipated.


Here's my first attempt at using Micropython code for this device, with the WiiM http API, to monitor the current track and album cover. Nothing fancy, I'll try using asyncio to make it update faster, but for now, "it just works", as has done for at least a week, even though I have my routers reboot every morning. If nothing's playing, it'll display the time and date after a few seconds.

Note that it doesn't have the processing power to resize JPGs or PNGs, so those are being done using an external web service (thanks, Paul Webster!).

I haven't attempted to do anything but display at this point, though it's a touchscreen, so has the possibility to be used as a remote control for the WiiM, as well.

The Presto comes pre-loaded with the necessary binaries, so you just need to load two files (via Thonny or another IDE) to get this working:

main.py
Python:
from picovector import ANTIALIAS_FAST, PicoVector, Polygon, Transform
import machine
from presto import Presto
import time
import utime
import math
import urequests as requests
import gc
import socket
import jpegdec
import pngdec
import json
import secrets
import ntptime

# Constants
WIIM_IP = secrets.WIIM_IP
TIMEOUT = 15
TIMEZONE_OFFSET = secrets.TIMEZONE_OFFSET * 3600

# Initialize Presto and Display
presto = Presto(ambient_light=False, full_res=True)
presto.set_backlight(0.2)
display = presto.display
 

WIDTH, HEIGHT = display.get_bounds()
CX, CY = WIDTH // 2, HEIGHT // 2


# Colors
BLACK = display.create_pen(0, 0, 0)
WHITE = display.create_pen(255, 255, 255)
GRAY = display.create_pen(60, 60, 60)

months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')

display.set_pen(BLACK)
display.clear()

# Initialize PicoVector
vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_FAST)
vector.set_font("Roboto-Medium.af", 14)

vector.set_font_letter_spacing(100)
vector.set_font_word_spacing(100)

transform = Transform()
#transform.scale(512,512)
vector.set_transform(transform)

# Initialize JPEG Decoder
jpd = jpegdec.JPEG(display)
pnd = pngdec.PNG(display)

def connect_to_wifi():
    print("Connecting to WiFi")
    status = str(presto.connect())
    print("Connection status: "+ status)
 
    # Get current time from NTP server
    try:
        ntptime.settime()
    except:
        print('Error updating time from NTP server')

def show_clock():
    display.set_pen(BLACK)
    display.clear()

    timestamp = utime.mktime(utime.localtime()) + TIMEZONE_OFFSET
    tm = utime.localtime(timestamp)

    # Format date and time
    date_str = "{:s} {:02d} {:s} {:d}".format(days[tm[6]], tm[2], months[tm[1]-1], tm[0])
    time_str = "{:02d}:{:02d}".format(tm[3], tm[4])
    display.set_pen(WHITE)
    vector.set_font_size(200)
    vector.text(time_str, 20, 240)
    vector.set_font_size(60)
    vector.text(date_str, 40, 360)
    presto.update()
 
def printtext(x,y,txt):
    display.set_pen(WHITE)
    vector.set_font_size(20)
    vector.text(txt, x, y)

def fetch_album_art(art_url):
 
    gc.collect()
    if "size=0" in art_url:
        proxy_url = str(art_url[:-1]) + "420X420"
    else:
        proxy_url = "https://wsrv.nl/?url="+art_url+"&w=420&h=420"

    response = requests.get(proxy_url, timeout=TIMEOUT)
 
    if response.status_code == 200:
        display.set_pen(BLACK)
        display.clear()
        album_art = response.content
        try:
            if ".png" in proxy_url:
                pnd.open_RAM(memoryview(album_art))
                pnd.decode(30,0)
            else:
                jpd.open_RAM(memoryview(album_art))
                ret = jpd.decode(30, 0, jpegdec.JPEG_SCALE_FULL)
             
        except Exception as e:
            print("Fetch Art Error: ",e)
        finally:
            response.close()
            gc.collect()

        ### Round corners of album cover
        try:
            rect = Polygon()
            display.set_pen(BLACK)
            rect.rectangle(20, -10, 440, 440, corners=(15,15,15,15), stroke=10)
            vector.draw(rect)
        except Exception as e:
            print("rect error:",e)
    else:
        print("Failed to fetch album art")
        display.set_pen(BLACK)
        display.clear()

def update_display(title, artist):

    display.set_pen(BLACK)
    display.rectangle(0,420,WIDTH,60)
    display.set_pen(WHITE)
    vector.set_font_size(30)
    vector.text(artist, 30, CY + 205)
    vector.set_font_size(20)
    vector.text(title, 30, CY + 225)
 
    presto.update()

def main():
    # Give enough time to breakout before wdt
    #time.sleep(10)
    # Reboot when non-responsive
    #wdt = WDT(timeout=8388)
    #wdt.feed()

    connect_to_wifi()

    show_clock()
 
    old_album = ""
    track = -1
    queue = None
    trackId = ""
    title = ""
    Title = ""
    artist = ""
    art_url = ""
    state = ""
    count = 0
 
    gc.collect()
    print("Free:", gc.mem_free())

    while True:
        try:
            response = requests.get("https://"+WIIM_IP+"/httpapi.asp?command=getPlayerStatus", timeout=TIMEOUT)
            if response.status_code == 200:
                data = response.json()
             
                if data["status"] == "play":
                    count = 0
                    try:
                        if Title != data["Title"]:
                            Title = data["Title"]
                            try:
                                response = requests.get("https://"+WIIM_IP+"/httpapi.asp?command=getMetaInfo", timeout=TIMEOUT)
                                data = response.json()["metaData"]
                                if art_url != data["albumArtURI"]:
                                    art_url = data["albumArtURI"]
                                    fetch_album_art(art_url)

                                artist = data["artist"]
                                if artist == "unknow":
                                    artist = data["subtitle"]
                                 
                                title = data["title"]
                                update_display(artist, title)
                            except Exception as e:
                                print("Error getting metadata:",e)
                    except Exception as e:
                        print(e)
                else:
                    count +=1
                    if count > 10:
                        count = 0
                        Title = ""
                        show_clock()
         
        except Exception as e:
            print("Connection error:", e)
            time.sleep(1)
            count += 1
            if count > 5:
                count = 0
                connect_to_wifi()
         
        time.sleep(1)
 
if __name__ == "__main__":
    main()

secrets.py:
Code:
WIFI_SSID = ""
WIFI_PASSWORD = ""
WIIM_IP = "192.168.68.xxx"
TIMEZONE_OFFSET = -8

The secrets.py entries are obvious; in my case, right now in California, time is -8 hours compared to GMT.


IMG_3516.jpg
IMG_3514.jpg
 

Attachments

  • IMG_3595.jpg
    IMG_3595.jpg
    279 KB · Views: 11
Last edited:
I haven't attempted to do anything but display at this point, though it's a touchscreen, so has the possibility to be used as a remote control for the WiiM, as well.

First of all, thank you @cc_rider!

I would say having only album art, title and artist has a real strength to this design and obviously works great for a near-square design.

With bringing in more info, controls etc you obviously have the functional work but also a graphical design challenge that isn't so easy to resolve.

Personally would be looking at a slightly bigger screen. But other priorities for now...
 
Looks amazing, i just did a similar thing using an esp32 display called "GUITION 4” 480x480 ESP32-S3-4848S040"
It's cheap and works great but it's my first dabble with anything simmilar to this. I only had experiance with some JS and react stuff.

I dont really know what i'm doing so i chatGPTd my way to making it fetch json and displaying the title, artist and album but never could i make it display the album art. I need to learn more and make new code from the ground up because currently it's an AI mess :D
 
This is a slick little device, very much ups the game for displays with embedded controllers. Out of stock at the moment, I'm guessing it was much more popular than they anticipated.


Here's my first attempt at using Micropython code for this device, with the WiiM http API, to monitor the current track and album cover. Nothing fancy, I'll try using asyncio to make it update faster, but for now, "it just works", as has done for at least a week, even though I have my routers reboot every morning. If nothing's playing, it'll display the time and date after a few seconds.

Note that it doesn't have the processing power to resize JPGs or PNGs, so those are being done using an external web service (thanks, Paul Webster!).

I haven't attempted to do anything but display at this point, though it's a touchscreen, so has the possibility to be used as a remote control for the WiiM, as well.

The Presto comes pre-loaded with the necessary binaries, so you just need to load two files (via Thonny or another IDE) to get this working:

main.py
Python:
from picovector import ANTIALIAS_FAST, PicoVector, Polygon, Transform
import machine
from presto import Presto
import time
import utime
import math
import urequests as requests
import gc
import socket
import jpegdec
import pngdec
import json
import secrets
import ntptime

# Constants
WIIM_IP = secrets.WIIM_IP
TIMEOUT = 15
TIMEZONE_OFFSET = secrets.TIMEZONE_OFFSET * 3600

# Initialize Presto and Display
presto = Presto(ambient_light=False, full_res=True)
presto.set_backlight(0.2)
display = presto.display
 

WIDTH, HEIGHT = display.get_bounds()
CX, CY = WIDTH // 2, HEIGHT // 2


# Colors
BLACK = display.create_pen(0, 0, 0)
WHITE = display.create_pen(255, 255, 255)
GRAY = display.create_pen(60, 60, 60)

months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')

display.set_pen(BLACK)
display.clear()

# Initialize PicoVector
vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_FAST)
vector.set_font("Roboto-Medium.af", 14)

vector.set_font_letter_spacing(100)
vector.set_font_word_spacing(100)

transform = Transform()
#transform.scale(512,512)
vector.set_transform(transform)

# Initialize JPEG Decoder
jpd = jpegdec.JPEG(display)
pnd = pngdec.PNG(display)

def connect_to_wifi():
    print("Connecting to WiFi")
    status = str(presto.connect())
    print("Connection status: "+ status)
 
    # Get current time from NTP server
    try:
        ntptime.settime()
    except:
        print('Error updating time from NTP server')

def show_clock():
    display.set_pen(BLACK)
    display.clear()

    timestamp = utime.mktime(utime.localtime()) + TIMEZONE_OFFSET
    tm = utime.localtime(timestamp)

    # Format date and time
    date_str = "{:s} {:02d} {:s} {:d}".format(days[tm[6]], tm[2], months[tm[1]-1], tm[0])
    time_str = "{:02d}:{:02d}".format(tm[3], tm[4])
    display.set_pen(WHITE)
    vector.set_font_size(200)
    vector.text(time_str, 20, 240)
    vector.set_font_size(60)
    vector.text(date_str, 40, 360)
    presto.update()
 
def printtext(x,y,txt):
    display.set_pen(WHITE)
    vector.set_font_size(20)
    vector.text(txt, x, y)

def fetch_album_art(art_url):
 
    gc.collect()
    if "size=0" in art_url:
        proxy_url = str(art_url[:-1]) + "420X420"
    else:
        proxy_url = "https://wsrv.nl/?url="+art_url+"&w=420&h=420"

    response = requests.get(proxy_url, timeout=TIMEOUT)
 
    if response.status_code == 200:
        display.set_pen(BLACK)
        display.clear()
        album_art = response.content
        try:
            if ".png" in proxy_url:
                pnd.open_RAM(memoryview(album_art))
                pnd.decode(30,0)
            else:
                jpd.open_RAM(memoryview(album_art))
                ret = jpd.decode(30, 0, jpegdec.JPEG_SCALE_FULL)
            
        except Exception as e:
            print("Fetch Art Error: ",e)
        finally:
            response.close()
            gc.collect()

        ### Round corners of album cover
        try:
            rect = Polygon()
            display.set_pen(BLACK)
            rect.rectangle(20, -10, 440, 440, corners=(15,15,15,15), stroke=10)
            vector.draw(rect)
        except Exception as e:
            print("rect error:",e)
    else:
        print("Failed to fetch album art")
        display.set_pen(BLACK)
        display.clear()

def update_display(title, artist):

    display.set_pen(BLACK)
    display.rectangle(0,420,WIDTH,60)
    display.set_pen(WHITE)
    vector.set_font_size(30)
    vector.text(artist, 30, CY + 205)
    vector.set_font_size(20)
    vector.text(title, 30, CY + 225)
 
    presto.update()

def main():
    # Give enough time to breakout before wdt
    #time.sleep(10)
    # Reboot when non-responsive
    #wdt = WDT(timeout=8388)
    #wdt.feed()

    connect_to_wifi()

    show_clock()
 
    old_album = ""
    track = -1
    queue = None
    trackId = ""
    title = ""
    Title = ""
    artist = ""
    art_url = ""
    state = ""
    count = 0
 
    gc.collect()
    print("Free:", gc.mem_free())

    while True:
        try:
            response = requests.get("https://"+WIIM_IP+"/httpapi.asp?command=getPlayerStatus", timeout=TIMEOUT)
            if response.status_code == 200:
                data = response.json()
            
                if data["status"] == "play":
                    count = 0
                    try:
                        if Title != data["Title"]:
                            Title = data["Title"]
                            try:
                                response = requests.get("https://"+WIIM_IP+"/httpapi.asp?command=getMetaInfo", timeout=TIMEOUT)
                                data = response.json()["metaData"]
                                if art_url != data["albumArtURI"]:
                                    art_url = data["albumArtURI"]
                                    fetch_album_art(art_url)

                                artist = data["artist"]
                                if artist == "unknow":
                                    artist = data["subtitle"]
                                
                                title = data["title"]
                                update_display(artist, title)
                            except Exception as e:
                                print("Error getting metadata:",e)
                    except Exception as e:
                        print(e)
                else:
                    count +=1
                    if count > 10:
                        count = 0
                        Title = ""
                        show_clock()
        
        except Exception as e:
            print("Connection error:", e)
            time.sleep(1)
            count += 1
            if count > 5:
                count = 0
                connect_to_wifi()
        
        time.sleep(1)
 
if __name__ == "__main__":
    main()

secrets.py:
Code:
WIFI_SSID = ""
WIFI_PASSWORD = ""
WIIM_IP = "192.168.68.xxx"
TIMEZONE_OFFSET = -8

The secrets.py entries are obvious; in my case, right now in California, time is -8 hours compared to GMT.


View attachment 16236
View attachment 16237
Note that if you have a modern modem with DNS proxy you can define WIIM_IP as your WiiM device name and the local domain name.

Example: "ultra.local" or "ultra.home" or ...

So even if the IP changes it will still work.
 
Note that if you have a modern modem with DNS proxy you can define WIIM_IP as your WiiM device name and the local domain name.

Example: "ultra.local" or "ultra.home" or ...

So even if the IP changes it will still work.
Of course. However, as I reboot my router every night, it very often won't work until after a few pings to the IP address, so it's not practical here (in my case). I always set my WiiMs to a fixed IP address in my router.
 
This looks great, thanks for sharing. I picked up a Presto this week, and thought i'd try this out. Unfortunately, it seems to error when i test in Thonny:

Traceback (most recent call last):
File "<stdin>", line 43, in <module>
OSError: [Errno 2] ENOENT

Line 43 is: vector.set_font("Roboto-Medium.af", 14)

I've only really dabbled in coding before, so not sure if I'm missing something obvious. The IP and wifi details in my secrets file are definitely correct. Any ideas what could be wrong?
 
Jumped the gun slightly the other day. I get album art for Spotify but not Plex (my other main source). Also no artist/title on screen. I can tell it is reading the metadata by printing to the Shell in Thonny, but not sure why it's not showing on screen.
 
Jumped the gun slightly the other day. I get album art for Spotify but not Plex (my other main source). Also no artist/title on screen. I can tell it is reading the metadata by printing to the Shell in Thonny, but not sure why it's not showing on screen.
Again, was the font file not preinstalled on your Presto? It should have been there with all the example code, as some of the examples use it. If you've installed it from some random site, it probably won't work on this device. Make sure to use the one from the official repository:

 
Back
Top