• WANTED: Happy members who like to discuss audio and other topics related to our interest. Desire to learn and share knowledge of science required. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

HDMI Display For WiiM Mini (via Inovato Quadra)

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Preliminary version of Python script for displaying WiiM Mini (and Pro?) currently playing data on an HDMI display. Fully tested on the $29 Inovato Quadra at various 16:9 screen resolutions, scales nicely on my 24" ASUS monitor, should work fine on 7" HDMI monitors. Screen blanking / HDMI display standby when no music playing. Also tested on Chromebook, but screen blanking won't work there. Requires installing a few Python modules, which I'll list later.


Python:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import time
import os
from datetime import datetime
import json
import requests
import xmltodict
import upnpclient
from tkinter import *
from PIL import ImageTk,Image,ImageFont,ImageDraw
from random import randrange
from threading import Thread

####################################################################
#### Change the ip address to that of your WiiM Mini
dev = upnpclient.Device("http://192.168.68.112:49152/description.xml")
####################################################################

ws = Tk()
ws.title('WiiM Office')
ws.attributes("-fullscreen",True)
ws.config(bg='#000000')
ws.resizable(0,0)

screenwidth = ws.winfo_screenwidth()
screenheight = ws.winfo_screenheight()
fontsize = screenheight // 20
print(screenheight,fontsize)
bgcolor = "#451f0c"
ON = 1
OFF = 0

time_lbl = Label(
    ws,
    text=time.strftime( "%d/%m/%Y %A %H:%M"),
    anchor="w",
    font=("Helvetica",fontsize),
    padx=10,
    pady=5,
    bg=bgcolor,
    fg='#ffffff'
    )

time_lbl.grid(row=4,column=0,sticky="nwes")

progress_lbl = Label(
    ws,
    text="",
    anchor="w",
    font=("Helvetica",fontsize),
    padx=10,
    pady=5,
    bg=bgcolor,
    fg='#ffffff'
    )

progress_lbl.grid(row=4,column=1,sticky="nwes")

title_lbl = Label(
    ws,
    text="",
    anchor="w",
    width=100,
    padx=10,
    pady=5,
    font=("Helvetica",fontsize),
    bg=bgcolor,
    fg='#ffffff'
    )

title_lbl.grid(row=0,column=0,columnspan=2,sticky=N+W)

artist_lbl = Label(
    ws,
    text="",
    anchor="nw",
    width=80,
    wraplength=400,
    justify="left",
    padx=10,
    pady=10,
    font=("Helvetica",fontsize),
    bg='#000000',
    fg='#ffffff'
    )

artist_lbl.grid(row=1,column=1,sticky=W+N)

album_lbl = Label(
    ws,
    text="",
    anchor="nw",
    width=80,
    wraplength=400,
    justify="left",
    padx=10,
    pady=10,
    font=("Helvetica",fontsize),
    bg='#000000',
    fg='#ffffff'
    )

album_lbl.grid(row=2,column=1,sticky=W+N)


meta_lbl = Label(
    ws,
    text="",
    anchor="sw",
    width=80,
    padx=10,
    pady=10,
    font=("Helvetica",fontsize),
    bg='#000000',
    fg='#ffffff'
    )

meta_lbl.grid(row=3,column=1,sticky=W+S)

art_lbl = Label(
    ws,
    image=None
    )

art_lbl.grid(row=1,column=0,rowspan=3,sticky=W+N,padx=0,pady=0)

def screen(flag):
    if flag == ON:
        os.system("xset dpms force on")
    else:
        os.system("xset dpms force standby")

def get_nowplaying():

    w = ws.winfo_width()
    h = ws.winfo_height()
    lh = title_lbl.winfo_height()
    old_height = lh
    old_title = ""
    cleared = False

    while True:
        time.sleep(1)
        try:

            time_text=time.strftime("%a, %d %b %Y    %H:%M")
            obj = dev.AVTransport.GetInfoEx(InstanceID='0')
           
            if obj['CurrentTransportState'] != 'PLAYING':
                if not cleared:
                    screen(OFF)
                    cleared = True
                   
                continue

            if cleared:
                screen(ON)
                cleared = False


            time_lbl.config(text=time_text)
            try:
                duration = obj['TrackDuration'][3:]
                reltime = obj['RelTime'][3:]
                progress = f"{reltime}/{duration}"
            except:
                progress = ""

            progress_lbl.config(text=progress)

            meta = obj['TrackMetaData']
            data = xmltodict.parse(meta)["DIDL-Lite"]["item"]
            title = data['dc:title']
            if title != old_title or h != old_height:
                screen(ON)
                try:
                    artist = data['upnp:artist'][:132]
                except:
                    artist = ""

                try:
                    quality = int(data['song:quality'])
                except:
                    quality = 0

                try:
                    actualQuality = data['song:actualQuality']
                    mediatype = "FLAC"
                except:
                    actualQuality = ""

                try:
                    rate = int(data['song:rate_hz'])/1000.0
                except:
                    rate = 0

                try:
                    depth = int(data['song:format_s'])
                    if actualQuality == "HD":
                      depth = 16
                    if depth > 24:
                      depth = 24
                except:
                    depth = 0


                try:
                    title = data['dc:title']
                except:
                    title = ""

                try:
                    album = data['upnp:album'][:132]
                except:
                    album = ""

                if album == "":
                    try:
                      album = data['dc:subtitle'][:132]
                    except:
                      pass

                try:
                    mediatype = data['upnp:mediatype'].upper()
                except:
                    if quality > 1 or len(actualQuality) > 0 :
                      mediatype = "FLAC"
                    else:
                      mediatype = ""

                try:
                    br= int(data['song:bitrate'])
                    bitrate = f"{br} kbps"
                except Exception as e:
                    print(e)
                    bitrate = ""

                try:
                  arttmp = data["upnp:albumArtURI"]
                  if isinstance(arttmp, dict):
                    arturl = arttmp["#text"]
                  else:
                    arturl = arttmp
                except:
                    arturl = ""
                   

                old_title = title
                old_height = h
                metatext = f"{depth} bits / {rate} kHz {bitrate}"
                title_lbl.config(text=title)
                artist_lbl.config(text=artist,wraplength=w/2)
                album_lbl.config(text=album,wraplength=w/2)
                meta_lbl.config(text=metatext)
               
                try:
                    image = Image.open(requests.get(arturl,stream=True).raw)
                    image = image.resize((h-(lh*2),h-(lh*2)))
                    img = ImageTk.PhotoImage(image)
                    art_lbl.config(image=img)
                except Exception as e:
                    print(e)
                    pass
               
        except Exception as e:
            print(e)

        ws.update()



thread = Thread(target=get_nowplaying)
thread.daemon = True            
thread.start()


ws.mainloop()


Screenshot 2022-10-02 5.49.05 PM.png
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
After learning a bit more about Armbian and tkinter, the app is now auto starting at boot up with auto login, and driving my 65” TV nicely on the tiny Inovato Quadra. I’ll push to GitHub soon.

FAB48D2B-CDA4-4798-9941-5C933B984995.jpeg
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Last edited:
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Minor mods to the layout.

5F20DDF4-F8D5-4CC2-AF1B-1A28F1361E6A.jpeg
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
…but will it work on a Raspberry Pi? Yes…

I used the stock lite version of Raspberry Pi OS on an old Pi Zero W, and added a minimal version of X to keep things small (no desktop required for this). Waveshare display attached via the 40-pin header, not HDMI. Had to make the font slightly smaller, the only mod required. A PITA compared to the simplicity of the Inovato, but it does work.

X installation similar to this, without the need for the bloated Chromium browser.

6BC4BE12-7323-4107-934E-8E53E53E8889.jpeg
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
This 7” HDMI display works nicely, driven by the Inovato Quadra, on the RME. Its fully enclosed metal case matches the RME perfectly. Although the Amazon spec says it’s touchscreen, it isn’t. Doesn’t matter in this application, tho.

B4CC25A4-731E-401F-BB91-A738F3F3B5F0.jpeg
 

direwolf08

Member
Joined
Aug 31, 2020
Messages
63
Likes
53
Thank you so much for creating and sharing this app! I followed the directions you have on GitHub and it works really well, I am using a Pi 4B with the Official Touchscreen (had them lying around) and Raspbian. I haven’t quite figured out how to get it to auto start at login, but that seems like a Raspbian problem. Out of curiosity, do you know of any good tkinter UI design guides? I think I am figuring it out from your python code, but I was going to see if tkinter is capable of more complex UI elements (e.g. blurred album art as the background behind the text).
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Sorry, this was my first ever tkinter app. If you figure out some kewl stuff, do share!

BTW I’ve updated this locally to fix a few things, will synch it up after the holiday.
 

ErVikingo

Active Member
Joined
Dec 16, 2022
Messages
267
Likes
289
Location
FL USA
Hello Ralph!

I have a WIIM Pro on order and scheduled for delivery this weekend and I came across your script for a display. Very impressive!

I am not a techy and dont want to screw it up. As I read your posts, I need to get:

- Inovato Quadra (comes with Linux)
- Waveshare screen

???

I appreciate your feedback. Cheers!
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Hello Ralph!

I have a WIIM Pro on order and scheduled for delivery this weekend and I came across your script for a display. Very impressive!

I am not a techy and dont want to screw it up. As I read your posts, I need to get:

- Inovato Quadra (comes with Linux)
- Waveshare screen

???

I appreciate your feedback. Cheers!
I like this screen better. It's cheaper, fully enclosed in a metal shell, and it has no reflections. Also has buttons for brightness and setup. Doesn't appear to be available at the moment, it comes and goes. I paid about $47. Note that it's NOT a touchscreen, which is fine for this project.

 

ErVikingo

Active Member
Joined
Dec 16, 2022
Messages
267
Likes
289
Location
FL USA
Thanks! Reading on Inovato there is a warning regarding potential overheating when using displays. I don't want to have a fan, have you experienced any issues with heat?
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Thanks! Reading on Inovato there is a warning regarding potential overheating when using displays. I don't want to have a fan, have you experienced any issues with heat?
No. I think the heat issue had to do with trying to play videos. This application puts very little stress on the CPU, just static images that update once per song.
 

direwolf08

Member
Joined
Aug 31, 2020
Messages
63
Likes
53
HI @Ralph_Cramden , I have noticed three things happening lately that I can’t figure out and would love to get your input on:
  1. I am running this on RaspberryPiOS and I cannot for the life of me figure out how to keep the screen on while music is playing. It always blanks out after a few songs. Pressing pause then play brings it back. I tried turning off timed screen blanking by changing xset s, but no luck.
  2. This only recently started, but when I try to play a station from TuneIn within the Wiim app, the display turns on but does not update from the previous data. It seems like the station metadata is missing some fields (not sure if it is missing artist, album or what), but the terminal window shows a warning ‘object of type ‘NoneType’ has no len()’. It works fine when I go back to playing stuff from Spotify or my server music library. It used to work fine when playing TuneIn stations until very recently. This is where I start getting in over my head with Python (and try except blocks)!
  3. similar behavior when I play podcasts, except only album art is affected (all metadata but album art update from the previous thing played). The error I get in this case is ssl related: HTTPSConnectionPool(host='192.168.0.145', port=443): Max retries exceeded with url: /data/AirplayArtWorkData.jpeg (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1123)')))

Again,thank you for creating this script!
 
Last edited:
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Yeah, sorry, I haven't tested this on much of anything except Tidal, which is what I use pretty much exclusively. I don't think WiiM provides complete metadata for TuneIn, or for any radio stations. The SSL error appears to be when the art URL is pointing, for some odd reason, to the WiiM itself, rather than to the cloud source. The WiiM's web server does indeed have a self-signed cert, which will definitely cause an error. Not sure why it's blanking out on you, likely the X screensaver kicking in.
 

ErVikingo

Active Member
Joined
Dec 16, 2022
Messages
267
Likes
289
Location
FL USA
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468

Jolly Joker

Member
Joined
Dec 27, 2022
Messages
13
Likes
2
Yes and no. While it would display, it likely wouldn't be usable. It's designed for a standard 16:9 landscape screen.
Can't be too hard to modify the layout. Looks like redoing the row, column, columnspan assignments in wiim.py?
 
Last edited:
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,574
Likes
3,468
Can't be too hard to modify the layout. Looks like redoing the row, column, columnspan assignments in wiim.py?
Of course, should anyone want to go to this trouble for this oddball screen.
 

Jolly Joker

Member
Joined
Dec 27, 2022
Messages
13
Likes
2
Of course, should anyone want to go to this trouble for this oddball screen.
Maybe I'm overestimating how much work there is in putting together hardware for something like this. Just seemed like a simple change if someone wants a non-16:9 screen. I can see how it would look nice covering the front of a suitably sized box.
 
Top Bottom