Graphical front panel display

Taking everything as it currently exists on the RPi and stuffing it behind glass in the media center, readability is definitely out of the question. I think going back to the elapsed time may be the only choice that provides much value at that viewing distance. I do still like the info “tags” at the top, though.

I kept wondering whether anyone had tried using the latest luma.core, luma.oled or Adafruit CircuitPython stuff in a Kodi addon… That’s when I was reminded about the whole Python 2 to 3 transition that’s occurring for Kodi 19. Perhaps they’ll be greater experimentation once 19 is stable. (So I forgot about the Python transition – hey, I’m relatively new at this!)

I did order a full-color SPI-driven LCD display. Looking around for how to drive it usefully with Kodi, I came across KodiDisplayInfo. I’ll add it to the summary above.

Unfortunately, the page / view that most has my interest looks to be a work in progress, and the last commit was 3 years ago.

Music thumbnail:

I’ve been looking for similar solutions for years.

There was also Kodisplay https://github.com/vitalogy/script.kodisplay which looks a bit similar to Kodi Display Info but is implemented differently.

I’ve been interested in implementing something like this for ages - particularly for PVR Live TV (so you can see channel logo, number, current show etc.) as well as for music and video watching.

It has struck me that a Pi Zero W with a small matrix I2C or SPI connected OLED, TFT etc. display accessing your Kodi server over WiFi using a JSON RPC (?) over the network would be a neat solution - as you’d remove the need for your Kodi platform to be running OLED or TFT drivers and have I2C or SPI connectivity - and would be able to use the display as a kind of ‘add on’ device. It would work with Kodi on CoreElec, LibreElec, Windows, Macs, Android TV boxes, even on Android TV Smart TVs as it would be connected via the network rather than directly. This would be a really neat ‘universal display’ device for any Kodi instance?

The other thought I had was whether you could get a ‘status’ web page output from Kodi that any device capable of running a web browser would be able to connect to and use (Like a Pi 3A+ over WiFi with a DSI or MIPI DPI connected display). This would work well for larger TFTs, and also be a use for redundant obsolete iPads, Android tablets etc. There is a system called Frontview+ https://github.com/Ghawken/FrontView which does something similar but I didn’t find it really worked for me when I tried it.

There’s a lovely, simple, ‘Now Playing’ implementation for Sonos that runs on a Pi with the Pimoroni 720x720 Hyperpixel 4.0 display (perfect for square artwork because - it’s square) that I’ve built with a Pi 3A+ in a 3D printed case - and it works beautifully. Not cheap - but a very nice looking thing.
( https://www.hackster.io/mark-hank/sonos-album-art-on-raspberry-pi-screen-5b0012 ) which would also be a nice model to copy - if a bit expensive… I based mine on a 3A+ running via WiFi and had a 3D printing company print me this case https://www.thingiverse.com/thing:4128336 (Which is designed for the Touch not the non-Touch version annoyingly…) I had it printed in black and was really impressed with the quality of the print I received.

1 Like

Thanks for the new information, @noggin! I’ve updated my ever-growing summary above.

I like your notion of a separate, network-connected display (whether using a Kodi-generated web page or pulling info via JSON-RPC). The KodiDisplayInfo approach seems like it could have gone done that route.

I’ve also long thought it would be cool to have a smallish tablet (in the 5" range) with wireless charging propped up on a nice walnut stand (hiding the charger). The device could serve as both remote control and a “Now Playing” display. (JRemote has a decent screen for that, and JRemote2 is a recent re-write for any Android fans.) The small tablet market seems to have dried up, though, and Apple never added Qi to the iPod Touch.

Pimoroni’s Hyperpixel is a great looking display! If I was confident I could get something working, the 800x480 version would be fun to play with. At this point, though, I don’t know much about the DPI interface. Is it an RPi-only thing?

Also, in this description of the Adafruit DPI TFT display, there is this slight downer:

The other catch is that this display replaces the HDMI/NTSC output, so you can’t have the DPI HAT and HDMI working at once, nor can you ‘flip’ between the two.

Cheers,
Matt

Some info about artwork retrieval in Kodi…

  • The Images Available in Kodi section of their wiki lists the InfoLabels that are available to Python-based Kodi addons. That list includes MusicPlayer.Cover and Player.Art(type). Presumably these return a “Kodi-internal URL” rather than the artwork itself? I’ll play with logging what a service addon gets back to see.

  • The Artwork page in the wiki has a section on obtaining art via JSON-RPC. That documentation explicitly notes that one gets back Kodi-internal image paths, which can then be converted (in Python) to an externally accessible URL via urllib.parse.quote(). (The urllib.parse library is just part of Python 3.)

At this point, it beats me whether a Python addon can make use of the Kodi-internal image:// path in some other fashion. You wouldn’t think an external URL would be needed if the code is running as part of Kodi.

The JSON-RPC route seems like it would certainly work for a network-attached “Now Playing” widget.

Based on this July 2020 thread on the Odroid forum:

Would or could the C4(or c2) work with a DPI LCD?

it seems like

  • a parallel interface for displays has to be bit-banged on Odroid boards and
  • HardKernel’s own 3.5" display (480 x 320) thus achieves only 15 fps max.

That framerate would still be sufficient for fairly static info like time, progress bars, and artwork. It probably wouldn’t be great for a spectrum display.

@noggin, have you seen this?

MovieNow
https://www.movienowapp.de/index.php

It appears to be an RPi Zero-driven Movie Poster display, with both Kodi and Plex integration. Not exactly what you were describing, but certainly related. I don’t see the source code anywhere, though, and (according to a Sep 25 post on its forum) the last public (i.e., non-donation) version was 3.0.

FWIW, the following python script serves to get fetch-able full-sized image URLs. Using Python 3.8.3 with the json-rpc package added, here’s the script. More afterward.

import requests
import json
import urllib.parse


def main():
    base_url  = "http://10.0.0.188:8080"
    rpc_url   = base_url + "/jsonrpc"
    image_url = base_url + "/images"    
    headers = {'content-type': 'application/json'}

    print("Hello, world!")

    # Assemble a simple "ping" payload as a test
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "JSONRPC.Ping",
        "id"      : 2,
    }

    print("\nPing request:")
    print(json.dumps(payload))

    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
    print("Response: ", json.dumps(response))

    # See if simple InfoLabels can be retrieved
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "XBMC.GetInfoLabels",
        "params"  : {"labels": ["MusicPlayer.Title",
                                "MusicPlayer.Album",
                                "MusicPlayer.Artist",
                                "MusicPlayer.Time"]},
        "id"      : 3,
    }

    print("\nNext request:")
    print(json.dumps(payload))    

    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
    print("Response: ", json.dumps(response))


    # How about artwork details?
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "XBMC.GetInfoLabels",
        "params"  : {"labels": ["MusicPlayer.offset(0).Cover"]},
        "id"      : 4,
    }    

    print("\nNext request:")
    print(json.dumps(payload))    

    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
    print("Response: ", json.dumps(response))

    print("\nIsolating returned image:// path")
    image_path = response["result"]["MusicPlayer.offset(0).Cover"]
    print(image_path)

    print("\nExternal URL is nominally:")
    cover_url = image_url + "/" + urllib.parse.quote(image_path,safe='')
    print(cover_url)

    print("\n\n... However, files evidently need to be prepared for download!\n")
    
    # Prepare download
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "Files.PrepareDownload",
        "params"  : {"path": image_path},
        "id"      : 5,
    }        
    print("\nNext request:")
    print(json.dumps(payload))    

    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
    print("Response: ", json.dumps(response))    

    print("\nTry retrieving the following:")
    print(base_url + "/" + response["result"]["details"]["path"])
    
if __name__ == "__main__":
    main()

Saving the file under the name client.py and invoking it via python client.ph, here’s an example run from my laptop, with Kodi running separately on an Odroid C4.

Hello, world!

Ping request:
{"jsonrpc": "2.0", "method": "JSONRPC.Ping", "id": 2}
Response:  {"id": 2, "jsonrpc": "2.0", "result": "pong"}

Next request:
{"jsonrpc": "2.0", "method": "XBMC.GetInfoLabels", "params": {"labels": ["MusicPlayer.Title", "MusicPlayer.Album", "MusicPlayer.Artist", "MusicPlayer.Time"]}, "id": 3}
Response:  {"id": 3, "jsonrpc": "2.0", "result": {"MusicPlayer.Album": "Abbey Road", "MusicPlayer.Artist": "The Beatles", "MusicPlayer.Time": "00:40", "MusicPlayer.Title": "Maxwell's Silver Hammer"}}

Next request:
{"jsonrpc": "2.0", "method": "XBMC.GetInfoLabels", "params": {"labels": ["MusicPlayer.offset(0).Cover"]}, "id": 4}
Response:  {"id": 4, "jsonrpc": "2.0", "result": {"MusicPlayer.offset(0).Cover": "/var/media/Music/iTunes/The Beatles/Abbey Road/Folder.jpg"}}

Isolating returned image:// path
/var/media/Music/iTunes/The Beatles/Abbey Road/Folder.jpg

External URL is nominally:
http://10.0.0.188:8080images/%2Fvar%2Fmedia%2FMusic%2FiTunes%2FThe%20Beatles%2FAbbey%20Road%2FFolder.jpg


... However, files evidently need to be prepared for download!


Next request:
{"jsonrpc": "2.0", "method": "Files.PrepareDownload", "params": {"path": "/var/media/Music/iTunes/The Beatles/Abbey Road/Folder.jpg"}, "id": 5}
Response:  {"id": 5, "jsonrpc": "2.0", "result": {"details": {"path": "vfs/%2fvar%2fmedia%2fMusic%2fiTunes%2fThe%20Beatles%2fAbbey%20Road%2fFolder.jpg"}, "mode": "redirect", "protocol": "http"}}

Try retrieving the following:
http://10.0.0.188:8080/vfs/%2fvar%2fmedia%2fMusic%2fiTunes%2fThe%20Beatles%2fAbbey%20Road%2fFolder.jpg

Cutting and pasting the URL at the bottom into a web browser gets me the fullsize cover! I’ve not played yet with getting just a thumbnail image.

Reading through this Kodi-encoded URL text from the wiki, I was at first expecting fetches to the image/ path to “just work”. Somewhere along the way, though, a Files.PrepareDownload step got added.

I hope the above is useful to someone. You would have to replace the IP address targeted, of course.

If anyone with more experience using Kodi’s JSON-RPC interface wants to suggest any improvements, I’m all ears.

UPDATE: One can just use the MusicPlayer.Cover InfoLabel, rather than what I used above. Parsing of the response would have to be adjusted to matched.

Cheers!

Asking for

{"jsonrpc": "2.0", "method": "XBMC.GetInfoLabels", "params": {"labels": ["MusicPlayer.Cover"]}, "id": 4}

and

{"jsonrpc": "2.0", "method": "XBMC.GetInfoLabels", "params": {"labels": ["Player.Art(album.thumb)"]}, "id": 4}

yields the same results (namely, the album cover as it exists either in the folder or embedded in the file). Seems like the retrieved artwork will have to be re-sized as needed by the client.

The MIPI DPI display can run simultaneously with the HDMI display in some circumstances (and ISTR that a dual display kernel was being developed to allow for DPI and HDMI to be used at the same time) - and with the Pi 4B you have more flexibility.

However for my proposed use case of a standalone display you wouldn’t need the HDMI display - as Kodi would be running on a separate device - so a Pi 3A+ or a Pi Zero W could be used potentially (and you’d just SSH in for any coding stuff)

The DPI interface is a standard low level LCD panel interface - but few other SBCs properly support it. That’s the big advantage of the Pi - they properly support the hardware they sell and have the time and effort to implement decent driver support for it. The DPI interface can also be used for VGA output with a simple resistor DAC (known as the Gert VGA 666) for instance. It’s not really a bit-bangable interface like SPI or I2C though - it’s a full video rate video output solution like HDMI or DisplayPort, just at a lower level for directly driving LCD panels.

The python script below, when invoked in the luma.examples examples directory (since I cheated and just made use of its demo_opts module) with a command such as

python kodi_demo.py --display pygame --width 320 --height 240 --scale 1

produces this image (when Kodi is playing, of course):

kodi_000001

Using PIL for text drawing takes care of the Unicode problem. Now I just need to figure out about the whole wrapping approach for long titles, etc. I should probably throw a progress bar on as well.

Now I just need the physical LCD to get here…

The script has a little bit more error checking going on. It does check, for instance, that it can actually communicate with Kodi before proceeding and that something is playing. This needs to be developed further, since it does just assume that it’s MusicPlayer that is active.

The program is also just a one-shot. After fetching information and drawing that image, it exits.


kodi_demo.py

from demo_opts import get_device     # from luma.examples
from luma.core.render import canvas

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

import time
import requests
import json
import io

frameSize = (320, 240)

def main():
    base_url = "http://10.0.0.188:8080"
    rpc_url   = base_url + "/jsonrpc"
    headers  = {'content-type': 'application/json'}    

    device = get_device()

    font   = ImageFont.truetype("FreeSans.ttf", 14, encoding='unic')
    fontB  = ImageFont.truetype("FreeSansBold.ttf", 14, encoding='unic')
    image  = Image.new('RGB', (frameSize), 'black')    
    draw   = ImageDraw.Draw(image)

    # First ensure Kodi is up and accessible
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "JSONRPC.Ping",
        "id"      : 2,
    }

    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
    if response['result'] != 'pong':
        print("Kodi not available via HTTP-transported JSON-RPC.  Exiting.")
        exit(1);
    
    draw.rectangle([(1,1), (frameSize[0]-2,frameSize[1]-2)], 'black', 'white')
    
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "Player.GetActivePlayers",
        "id"      : 3,
    }        
    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
    
    if len(response['result']) == 0:
        draw.text(( 5, 5), "Nothing playing",  fill='white', font=font)
    else:   
        payload = {
            "jsonrpc": "2.0",        
            "method"  : "XBMC.GetInfoLabels",
            "params"  : {"labels": ["MusicPlayer.Title",
                                    "MusicPlayer.Album",
                                    "MusicPlayer.Artist",
                                    "MusicPlayer.Time",
                                    "MusicPlayer.Cover",
            ]},
            "id"      : 4,
        }
        response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
        print("Response: ", json.dumps(response))

        info = response['result']

        # Retrieve cover image from Kodi
        if info['MusicPlayer.Cover'] != '':
            image_path = info['MusicPlayer.Cover']
            payload = {
                "jsonrpc": "2.0",        
                "method"  : "Files.PrepareDownload",
                "params"  : {"path": image_path},
                "id"      : 5,
            }
            response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
            if ('details' in response['result'].keys() and
                'path' in response['result']['details'].keys()) :
                image_url = base_url + "/" + response['result']['details']['path']
                print("image_url : ", image_url) # debug info

                r = requests.get(image_url, stream = True)
                # Check that the retrieval was successful
                if r.status_code == 200:
                    r.raw.decode_content = True
                    cover = Image.open(io.BytesIO(r.content))
                    thumb = cover.resize((128, 128), Image.ANTIALIAS)
                    image.paste(thumb, (5, 5))

        # Display track information
        draw.text(( 5, 140), info['MusicPlayer.Title'],  fill='white',  font=fontB)
        draw.text(( 5, 160), info['MusicPlayer.Album'],  fill='white',  font=font)
        draw.text(( 5, 180), info['MusicPlayer.Artist'], fill='yellow', font=font)                

        # Output to OLED/LCD display
        device.display(image)
            
    time.sleep(3)
    

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass

Some improvements:

  • truncate text that’s too long (adding ellipsis)
  • add progress bar
  • add time display

image

image

1 Like

The LCD display arrived…

spi_LCD_display

spi_LCD_display2

Thing of beauty, Sir.

Thanks!!

I made a few more tweaks. Screenshots from emulation and a new photo are below. I also tried to make the script more resilient to communication failures (e.g., Kodi exiting and restarting).

Driving a larger display would be tough for SPI, which is where DPI comes in I suppose. Also, should freqData ever become available from Kodi, it may be better to stick with a smaller, monochrome OLED display for its speed. Nevertheless, I’m pretty happy with this approach.

I’ll need to move on to the case next.

Source code is below, in case it’s useful to anyone else. The Python script is independent of Kodi. It requires luma.core and luma.lcd, which implies a fairly recent version of Python 3. Also note that I’m not including the default image files or font files below.

Cheeers,
Matt

kodi_panel_emulator1

kodi_panel_emulator2

kodi_panel_photo


kodi_panel.py

#
# MIT License
# 
# Copyright (c) 2020  Matthew Lovell
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from luma.core.interface.serial import spi
from luma.core.render import canvas
from luma.lcd.device import ili9341

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

import time
import logging
import requests
import json
import io

base_url = "http://localhost:8080"
rpc_url  = base_url + "/jsonrpc"
headers  = {'content-type': 'application/json'}    

frameSize    = (320, 240)
thumb_height = 140;

last_image_path = ""
last_thumb      = ""

# Audio/Video codec names
codec_name = {"ac3"      : "DD",
              "eac3"     : "DD",
              "dtshd_ma" : "DTS-MA",
              "dca"      : "DTS",
              "truehd"   : "DD-HD",
              "aac"      : "AAC",
              "wmapro"   : "WMA",
              "mp3float" : "MP3",
              "flac"     : "FLAC",
              "BXA"      : "BXA",
              "alac"     : "ALAC",
              "vorbis"   : "OggV",
              "dsd_lsbf_planar": "DSD",
              "aac"      : "AAC",
              "pcm_s16be": "PCM",
              "mp2"      : "MP2",
              "pcm_u8"   : "PCM"}

# Handle to ILI9341-driven SPI panel via luma
serial = spi(port=0, device=0, gpio_DC=24, gpio_RST=25)
device = ili9341(serial, active_low=False, width=320, height=240,
                 framebuffer="full_frame",
                 bus_speed_hz=32000000)

# Track info fonts
font      = ImageFont.truetype("FreeSans.ttf", 22, encoding='unic')
fontB     = ImageFont.truetype("FreeSansBold.ttf", 22, encoding='unic')
font_sm   = ImageFont.truetype("FreeSans.ttf", 18, encoding='unic')
font_tiny = ImageFont.truetype("FreeSans.ttf", 11)

# Font for time and track
font7S    = ImageFont.truetype("DSEG14Classic-Regular.ttf", 32)
font7S_sm = ImageFont.truetype("DSEG14Classic-Regular.ttf", 11)

image  = Image.new('RGB', (frameSize), 'black')    
draw   = ImageDraw.Draw(image)


def truncate_text(pil_draw, xy, text, fill, font):
    truncating = 0
    new_text = text
    t_width, t_height = pil_draw.textsize(new_text, font)
    while t_width > (frameSize[0] - 20):
        truncating = 1
        new_text = new_text[:-1]
        t_width, t_height = pil_draw.textsize(new_text, font)
    if truncating:
        new_text += "\u2026"
    pil_draw.text(xy, new_text, fill, font)


def progress_bar(pil_draw, bgcolor, color, x, y, w, h, progress):
    pil_draw.ellipse((x+w,y,x+h+w,y+h),fill=bgcolor)    
    pil_draw.ellipse((x,y,x+h,y+h),fill=bgcolor)    
    pil_draw.rectangle((x+(h/2),y, x+w+(h/2), y+h),fill=bgcolor)
   
    if(progress<=0):        
        progress = 0.01    
    if(progress>1):        
        progress=1    
    w = w*progress    
    pil_draw.ellipse((x+w,y,x+h+w,y+h),fill=color)    
    pil_draw.ellipse((x,y,x+h,y+h),fill=color)    
    pil_draw.rectangle((x+(h/2),y, x+w+(h/2), y+h),fill=color)
    

def update_display():
    global last_image_path
    global last_thumb
    draw.rectangle([(1,1), (frameSize[0]-2,frameSize[1]-2)], 'black', 'black')
   
    payload = {
        "jsonrpc": "2.0",        
        "method"  : "Player.GetActivePlayers",
        "id"      : 3,
    }        
    response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
   
    if len(response['result']) == 0:
        # Nothing playing
        device.backlight(False)
        # draw.text(( 5, 5), "Nothing playing",  fill='white', font=font)
        last_image_path = ""
        last_thumb = ""
    elif response['result'][0]['type'] != 'audio':
        # Not audio
        device.backlight(False)        
        #draw.text(( 5, 5), "Not audio playing",  fill='white', font=font)
        last_image_path = ""
        last_thumb = ""        
    else:
        # Something's playing!
        device.backlight(True)
        
        payload = {
            "jsonrpc": "2.0",        
            "method"  : "XBMC.GetInfoLabels",
            "params"  : {"labels": ["MusicPlayer.Title",
                                    "MusicPlayer.Album",
                                    "MusicPlayer.Artist",
                                    "MusicPlayer.Time",
                                    "MusicPlayer.Duration",                                    
                                    "MusicPlayer.TrackNumber",
                                    "MusicPlayer.Property(Role.Composer)",
                                    "MusicPlayer.Codec",
                                    "MusicPlayer.Year",
                                    "MusicPlayer.Genre",                                                                        
                                    "MusicPlayer.Cover",
            ]},
            "id"      : 4,
        }
        response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
        #print("Response: ", json.dumps(response))

        info = response['result']

        # progress bar
        payload = {
            "jsonrpc": "2.0",        
            "method"  : "Player.GetProperties",
            "params"  : {
                "playerid": 0,
                "properties" : ["percentage"],
            },
            "id"      : "prog",
        }        
        response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()        
        if 'percentage' in response['result'].keys():
            prog = float(response['result']['percentage']) / 100.0
        else:
            prog = -1;
           
        # retrieve cover image from Kodi, if it exists and needs a refresh
        image_set = False
        if (info['MusicPlayer.Cover'] != '' and
            info['MusicPlayer.Cover'] != 'DefaultAlbumCover.png' and
            info['MusicPlayer.Cover'] != 'special://temp/airtunes_album_thumb.jpg'):
            image_path = info['MusicPlayer.Cover']
            #print("image_path : ", image_path) # debug info

            if image_path == last_image_path:
                image_set = True
            else:
                last_image_path = image_path
                payload = {
                    "jsonrpc": "2.0",
                    "method"  : "Files.PrepareDownload",
                    "params"  : {"path": image_path},
                    "id"      : 5,
                }
                response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
                if ('details' in response['result'].keys() and
                    'path' in response['result']['details'].keys()) :
                    image_url = base_url + "/" + response['result']['details']['path']
                    #print("image_url : ", image_url) # debug info

                    r = requests.get(image_url, stream = True)
                    # check that the retrieval was successful
                    if r.status_code == 200:
                        try:
                            r.raw.decode_content = True
                            cover = Image.open(io.BytesIO(r.content))
                            # resize while maintaining aspect ratio
                            orig_w, orig_h = cover.size[0], cover.size[1]
                            shrink = (float(thumb_height)/orig_h)
                            new_width = int(float(orig_h)*float(shrink))
                            # just crop if the image turns out to be really wide
                            if new_width > 140:
                                thumb = cover.resize((new_width, thumb_height), Image.ANTIALIAS).crop((0,0,140,thumb_height))
                            else:
                                thumb = cover.resize((new_width, thumb_height), Image.ANTIALIAS)
                            last_thumb = thumb
                            image_set = True
                        except:
                            cover = Image.open("kodi_thumb.jpg")
                            last_thumb = cover
                            image_set = True


        if not image_set:
            # default image when no artwork is available
            last_image_path = "./kodi_thumb.jpg"
            # is Airplay active?
            if info['MusicPlayer.Cover'] == 'special://temp/airtunes_album_thumb.jpg':
                last_image_path = "./airplay_thumb.png"
            cover = Image.open(last_image_path)
            last_thumb = cover
            image_set = True

        if image_set:
            image.paste(last_thumb, (5, 5))
            
        # progress bar and elapsed time
        if prog != -1:
            if info['MusicPlayer.Time'].count(":") == 2:
                # longer bar for longer displayed time
                progress_bar(draw, 'grey', 'lightgreen', 148, 5, 162, 4, prog)                
            else:
                progress_bar(draw, 'grey', 'lightgreen', 148, 5, 102, 4, prog)
                
        draw.text(( 148, 14), info['MusicPlayer.Time'],  fill='lightgreen', font=font7S)  

        # track number
        if info['MusicPlayer.TrackNumber'] != "":
            draw.text(( 148, 60), "Track", fill='white', font=font_tiny)
            draw.text(( 148, 73), info['MusicPlayer.TrackNumber'],  fill='white', font=font7S)

        # track title
        truncate_text(draw, (5, 150), info['MusicPlayer.Title'],  fill='white',  font=font)

        # other track information
        truncate_text(draw, (5, 180), info['MusicPlayer.Album'],  fill='white',  font=font_sm)
        if info['MusicPlayer.Artist'] != "":
            truncate_text(draw, (5, 205), info['MusicPlayer.Artist'], fill='yellow', font=font_sm)
        elif info['MusicPlayer.Property(Role.Composer)'] != "":
            truncate_text(draw, (5, 205), "(" + info['MusicPlayer.Property(Role.Composer)'] + ")", fill='yellow', font=font_sm)

        # audio info
        codec = info['MusicPlayer.Codec']
        if info['MusicPlayer.Duration'] != "":
            draw.text(( 230, 60), info['MusicPlayer.Duration'], font=font_tiny)        
        if codec in codec_name.keys():
            draw.text(( 230, 74), codec_name[codec], font=font_tiny)
        if info['MusicPlayer.Genre'] != "":
            draw.text(( 230, 88), info['MusicPlayer.Genre'][:15], font=font_tiny)            
        if info['MusicPlayer.Year'] != "":
            draw.text(( 230, 102), info['MusicPlayer.Year'], font=font_tiny)
            
    # Output to OLED/LCD display
    device.display(image)

   
def main():
    print("Hello, world")
    device.backlight(False)
    
    # Turn down verbosity from http connections
    logging.basicConfig()
    logging.getLogger("urllib3").setLevel(logging.WARNING)
   
    while True:
    
        while True:
            # first ensure Kodi is up and accessible
            payload = {
                "jsonrpc": "2.0",
                "method"  : "JSONRPC.Ping",
                "id"      : 2,
            }
            
            try:
                response = requests.post(rpc_url, data=json.dumps(payload), headers=headers).json()
                if response['result'] != 'pong':
                    print("Kodi not available via HTTP-transported JSON-RPC.  Waiting...")
                    time.sleep(5)
                else:
                    break
            except:
                time.sleep(5)
                pass
                
        print("Entering display loop.")

        while True:
            try:
                update_display()
            except (ConnectionRefusedError,
                    requests.exceptions.ConnectionError):
                print("Communication disrupted.")
                break
            time.sleep(0.9)
   

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass
1 Like

As it’s a standalone python app I’ll try to spin it up on a Pi Zero W to which I can connect an SPI 160x160 OLED or 320x240 TFT, and point to Kodi running on an S905X3 box I have running Kodi under CoreElec.

For DPI/DSI/small HDMI displays a version that outputs to a regular screen might also be useful - rather than to an SPI display?

I think that would be possible. One needs to do something with the constructed image other than

device.display(image)

The image object is straight PIL / Pillow.

Looking (briefly) at the WeatherPi_TFT python program, it seems to make use of pygame and displaying via a framebuffer device (either /dev/fb0 or /dev/fb1). I saw something in the documentation about dtoverlay as well. I didn’t examine things much further, though.

UPDATE: Further observations about the state of pygame and libSDL in 2020 in Post #83 below.

Getting back to Odroid and CoreELEC, does anyone know how to install the RPi.GPIO equivalent for Odroid?

The HardKernel wiki for C2:

https://wiki.odroid.com/odroid-c2/application_note/gpio/rpi.gpio

points to this github repository:

RPi.GPIO-Odroid
https://github.com/awesometic/RPi.GPIO-Odroid

The build instructions listed don’t seem that bad:

  1. Install build-essential, python, python-dev, git
  2. git clone https://github.com/jfath/RPi.GPIO-Odroid.git
  3. cd RPi.GPIO-Odroid
  4. sudo python setup.py clean --all
  5. sudo python setup.py build install

However, CoreELEC lacks git. I don’t know about python-dev, either.

This seems like the fastest route for getting luma.core, luma.lcd up and running on an Odroid board.

Yep - pygame is how a couple of other people have done this.

For anyone following along, see this forum thread:

Entware and the packages it makes available are a great resource to have available for CoreELEC.

kodi_panel running under CoreELEC on an Odroid C4:

kodi_panel_on_C4

No armbian or Ubuntu needed, fortunately!

3 Likes