Display.set_layer() for Display Packs

Greetings all,

I have both the Explorer kit, and a combination of the Pico Plus 2W with the Display Pack 2.

In the Explorer examples there is an Explorer boot menu/loader (main.py). In this file there is a call to display.set_layer(0) which is used to set a background layer png.

Is there a similar function for the Display Pack 2, and Display Pack 2.8 (on order) in the picographics library? I’ve looked with no success but may have missed the obvious in the Github documentation. Any help is appreciated!

Thank you,
David W.

I’m far from any kind of expert on this, JFYI. I’m not aware of an equivalent command. The Explorer is using Pico Vector, whereas the Display Packs use Pico Graphics. What you get depends on the custom uf2 used.

Thanks Alphanumeric.

For my Pico Plus 2W I’m using the Pimoroni uf2. I’ll read up on the two graphics libraries. I had assumed they both used the same base library with the customization being for the Explorer due to it being an all-in-one board.

Since the commands are so similar I have adapted the menu from Explorer to run on the PP2W minus the background png. And I’ve also been able to get the Rainbow Wheel from the Display Pack 2 to run on the Explorer.

Update: In PicoGraphics there is a “layer” setting that is undocumented at the moment per a statement here: https://github.com/pimoroni/pimoroni-pico/releases/tag/v1.24.0-beta2

I was able to display a png by modifying the following line in the Explorer main.py example:

display = PicoGraphics(display=DISPLAY_PICO_DISPLAY_2, layers=2)

The layer of choice can be selected with:

display.set_layer(0) # layer 1
display.set_layer(1) # layer 2

The background png can be had here: https://github.com/pimoroni/explorer/tree/main/examples/lib
The image is heavily compressed on the Display Pack 2, and does not look like it does on the Explorer. Probably an image with a limited palette will do better. I’m curious how it will look on the Display Pack 2.8 once it makes it accross the pond to Michigan.

Here is the modified code for the Display Pack 2, name it main.py to run at boot.

# From "Explorer" boot menu/loader
# Modded to run on Display Pack 2
# Note: png file must be 320 x 240 pixels

import gc		# Garbage collection library
import pngdec 	# 'Embedded-friendly' PNG image decoding library
import time
from os import listdir
from machine import Pin

from picographics import PicoGraphics, DISPLAY_PICO_DISPLAY_2

display = PicoGraphics(display=DISPLAY_PICO_DISPLAY_2, layers=2)

WHITE = display.create_pen(255, 255, 255)
BLACK = display.create_pen(0, 0, 0)

button_a = Pin(12, Pin.IN, Pin.PULL_UP)
button_b = Pin(13, Pin.IN, Pin.PULL_UP)
button_x = Pin(14, Pin.IN, Pin.PULL_UP)
button_y = Pin(15, Pin.IN, Pin.PULL_UP)

last_button_press = time.ticks_ms()


def hsv_to_rgb(h: float, s: float, v: float) -> tuple[float, float, float]:
    if s == 0.0:
        return v, v, v
    i = int(h * 6.0)
    f = (h * 6.0) - i
    p = v * (1.0 - s)
    q = v * (1.0 - s * f)
    t = v * (1.0 - s * (1.0 - f))
    v = int(v * 255)
    t = int(t * 255)
    p = int(p * 255)
    q = int(q * 255)
    i = i % 6
    if i == 0:
        return v, t, p
    if i == 1:
        return q, v, p
    if i == 2:
        return p, v, t
    if i == 3:
        return p, q, v
    if i == 4:
        return t, p, v
    if i == 5:
        return v, p, q


def debounce(button, debounce_ms=200):
    global last_button_press
    try:
        value = button.value() == 0
        if value and time.ticks_ms() - last_button_press > debounce_ms:
            last_button_press = time.ticks_ms()
            return True
    except NameError:
        last_button_press = time.ticks_ms()
    return False


def get_applications() -> list[dict[str, str]]:
    # fetch a list of the applications that are stored in the filesystem
    applications = []
    for file in listdir():
        if file.endswith(".py") and file != "main.py":
            # convert the filename from "something_or_other.py" to "Something Or Other"
            # via weird incantations and a sprinkling of voodoo
            title = " ".join([v[:1].upper() + v[1:] for v in file[:-3].split("_")])

            applications.append(
                {
                    "file": file,
                    "title": title
                }
            )

    # sort the application list alphabetically by title and return the list
    return sorted(applications, key=lambda x: x["title"])


def prepare_for_launch() -> None:
    for k in locals().keys():
        if k not in ("__name__",
                     "application_file_to_launch",
                     "gc"):
            del locals()[k]
    gc.collect()


def menu() -> str:
    applications = get_applications()

    display.set_backlight(1.0)

    selected_item = 2
    scroll_position = 2
    target_scroll_position = 2

    selected_pen = WHITE
    unselected_pen = WHITE
    shadow_pen = BLACK

    # ** For png display on multilayer 2.8" Explorer module **
    # Set layer to 0 and decode the background PNG.
    display.set_layer(0)
    p = pngdec.PNG(display)
    p.open_file("backgroundforscreen.png")
    p.decode(0, 0)
    
    while True:

        if debounce(button_x):
            target_scroll_position -= 1
            target_scroll_position = target_scroll_position if target_scroll_position >= 0 else len(applications) - 1

        if debounce(button_y):
            target_scroll_position += 1
            target_scroll_position = target_scroll_position if target_scroll_position < len(applications) else 0

        if debounce(button_a):
            return applications[selected_item]["file"]

        scroll_position += (target_scroll_position - scroll_position) / 5

        # work out which item is selected (closest to the current scroll position)
        selected_item = round(target_scroll_position)

        # Set the layer to 1. We'll make all of our changes on this layer.
        display.set_layer(1)
        display.set_pen(BLACK)
        display.clear()

        # Draw some markers on screen so you know which buttons to press :)
        display.set_pen(WHITE)
        display.triangle(303, 42, 293, 55, 313, 55)
        display.rectangle(295, 114, 16, 16)
        display.triangle(303, 205, 293, 191, 313, 191)

        for list_index, application in enumerate(applications):
            distance = list_index - scroll_position

            text_size = 4 if selected_item == list_index else 2

            # center text horizontally
            text_x = 10

            row_height = text_size * 5 + 20

            # center list items vertically
            text_y = int(90 + distance * row_height - (row_height / 2))

            # draw the text, selected item brightest and with shadow
            if selected_item == list_index:
                display.set_pen(shadow_pen)
                display.text(application["title"], text_x + 1, text_y + 1, -1, text_size)

            text_pen = selected_pen if selected_item == list_index else unselected_pen
            display.set_pen(text_pen)
            display.text(application["title"], text_x, text_y, -1, text_size)

        display.update()


# The application we will be launching. This should be our only global, so we can
# drop everything else.
application_file_to_launch = menu()

# Run whatever we've set up to.
# If this fails, we'll exit the script and drop to the REPL, which is
# fairly reasonable.
prepare_for_launch()
__import__(application_file_to_launch)

Nice sleuthing and nicely done. =) I have a Pico Plus 2 here, and a Display Pack 2.8. I’ll dig them out and have a go at running your code.

Thank you Alphanumeric! After further testing though I found that my success was premature. I tested the code on an RPi Pico, RPi Pico W, Pico 2W, and a Pimoroni Pico Plus 2W. All were running the latest Pimoroni uf2 images for the respective Pico’s.

Here were the issues I ran into:

– Pico: The code worked but image was very low res as mentioned above.

– Pico W: The code did not work and would lock up the Pico W. I tried this on 2 modules thinking I had a bad module at first. The only way to break the Pico W’s out and back to working order was to install the official RPi Pico W uf2 and then delete the main.py files from the module. Thonny provided the following message during the issue:

", line 170, in <module>
  File "main.py", line 110, in menu
MemoryError: memory allocation failed, allocating 48156 bytes
MicroPython v1.24.0, picow v1.24.0-beta2 on 2024-12-11; Raspberry Pi Pico W with RP2040

– Pico 2W, and Pimonroni Pico Plus 2W: Neither would run the code due to not recogonizing the layers = 2 in the following statment:

display = PicoGraphics(display=DISPLAY_PICO_DISPLAY_2, layers=2)

and provided the following error:

MPY: soft reboot
Traceback (most recent call last):
  File "<stdin>", line 13, in <module>
TypeError: extra keyword arguments given

I think this is due to the code still being developed and that waiting for the code to be updated and released in future uf2 images is probably best for now. It does look promising though as we can see this works well in the Pimoroni Explorer and is something to look forward to. -David

AFAIK it is a work in progress. Poke around here and you will see what I mean.
pimoroni/pimoroni-pico: Libraries and examples to support Pimoroni Pico add-ons in C++ and MicroPython.
Just scroll down a bit. ;)

The Pico (1) being slow is because of the 264kB of internal RAM.
The Pico W, I’ve been told, gives up some of that for the WIFI. Likely why it just crashes with an out of memory error. I’ve hit that wall a few times tinkering around.

The Pico 2 has 520 KB on-chip SRAM (twice what the RP2040 Pico has), and faster processors. All that’s holding you back then is the software in the uf2 file.

1 Like

Thanks for the link, I will give that a go today.

I believe you are right about the memory limitations as a ram test script bore out the fact that the Pico W loses some memory over the Pico.