DEV Community

Cover image for Inky Frame menu items
djchadderton
djchadderton

Posted on

Inky Frame menu items

Pimoroni offers a range of its own products based around the Raspberry Pi Pico microcontroller, usually with example software that will at least allow you to use them without much work, although the software is often out-of-date and the documentation inadequate to show clearly how it could be modified to do anything other than the default actions.

Some of the products provide a menu system to allow different functions to be selected with buttons on the device, but they use different ways of programming this.

This article looks at the menu system on the Inky Frame series of e-ink displays in an attempt to see how it can be modified to include a new custom item.

Breaking down the code

main.py firstly imports the appropriate class for screen as DISPLAY (select the appropriate line for the screen you are using) and initiates it into the graphics variable, obtaining the WIDTH and HEIGHT of the display from it.

# from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY      # 5.7"
from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY  # 4.0"
# from picographics import PicoGraphics, DISPLAY_INKY_FRAME_7 as DISPLAY  # 7.3"

graphics = PicoGraphics(DISPLAY)
WIDTH, HEIGHT = graphics.get_bounds()
Enter fullscreen mode Exit fullscreen mode

After the launcher function is defined, all LEDs are turned off—button and warning LEDs—in case any are still on. ih is imported from inky-helper.

ih.clear_button_leds()
ih.led_warn.off()
Enter fullscreen mode Exit fullscreen mode

If A and E are being pressed together or there is no state.json file, launcher() is called to display a menu then loop until a button is pressed.

if ih.inky_frame.button_a.read() and ih.inky_frame.button_e.read():
    launcher()
Enter fullscreen mode Exit fullscreen mode

If a button is pressed in that loop, the update_state method in inky_helper is called to save the name of the selected option into the state.json file, then machine.reset() is called to do a hard reset, e.g.

if ih.inky_frame.button_a.read():
    ih.inky_frame.button_a.led_on()
    ih.update_state("nasa_apod")
    time.sleep(0.5)
    reset()
Enter fullscreen mode Exit fullscreen mode

If the state.json file does exist, the load_state method in inky_helper reads the contents of the file, converts it to JSON then into a dictionary and saves it as the global state variable. It only has one entry with the key "run" holding the name of the file (without the .py extension)

The launch_app method in inky_helper is then called from main, which imports the file for the selected menu item, under the "run" key in the state dictionary, into the global app variable and calls update state to save this back into state.json.

if ih.file_exists("state.json"):
    ih.load_state()
    ih.launch_app(ih.state['run'])
Enter fullscreen mode Exit fullscreen mode

Three values are then injected into the imported app file: graphics, WIDTH and HEIGHT.

ih.app.graphics = graphics
ih.app.WIDTH = WIDTH
ih.app.HEIGHT = HEIGHT
Enter fullscreen mode Exit fullscreen mode

For this to work, each app file must have somewhere in the variable initiation section:

graphics = None
WIDTH = None
HEIGHT = None
Enter fullscreen mode Exit fullscreen mode

An attempt is then made to create a WiFi network connection using the credentials in the secrets.py file, which should define just two variables: WIFI_SSID and WIFI_PASSWORD, which are self-explanatory.

There is a forced garbage collection with gc.collect() to clear some memory, then an infinite while loop begins that goes through the sequence:

  • call an update() method in the imported file
  • turn the warn LED on
  • call a draw() method in the imported file
  • turn the warn LED off
  • get the UPDATE_INTERVAL constant from the imported file then call the sleep method in inky_helper to set the RTC wake-up timer go into a sleep mode if on battery or do a regular sleep if on USB power. ## Creating a new menu item

To try this out, this section will create a new file called test.py that will just put up a test message on the screen with the current date and time.

First, import just the parts needed to save memory:

from ntptime import settime
from machine import RTC
Enter fullscreen mode Exit fullscreen mode

In order to create a new item to replace one in the default menu, the file to be imported will need to contain, apart from the new application code:

  • variables graphics, WIDTH and HEIGHT initialised to None.

    graphics = None
    WIDTH = None
    HEIGHT = None
    
  • a constant UPDATE_INTERVAL set to the time in minutes between updates of the screen.

    UPDATE_INTERVAL = 5 # set to 5 minutes so don't have to wait too long to see whether it's working
    
  • instantiate any other variables required for this file.

    time_string = None
    rtc = RTC()
    
  • a method update() that gets the new data (a network/Internet connection should be available).

    def update():
        global time_string
    
        try:
            settime()
        except OSError:
            print("Unable to contact NTP server")
    
        rtc_time = rtc.datetime()
        year, month, mday, weekday, hour, minute, second, subsecond = rtc_time
    
        if year < 2023:
            time_string = "Unable to update time"
        else:
            time_string = f"{mday:0=2}/{month:0=2}/{year:0=2} at {hour:0=2}:{minute:0=2}"
    
  • a method draw() that refreshes the screen with the new data.

    def draw():
        global time_string
    
        graphics.set_font("sans")
        graphics.set_pen(0)
        graphics.clear()
        graphics.set_pen(1)
        graphics.set_thickness(5)
        graphics.text("Test", int(WIDTH / 2) - 120, int(HEIGHT / 2) - 20, 600, 4)
        graphics.set_thickness(3)
        graphics.text(time_string, 0, HEIGHT - 20, 600, 1)
        graphics.update()
    

Some extra code can be added to the end to mimic the menu's actions so that this can be run as a standalone application, which can be useful for debugging without having to trigger it from the menu each time, which would require two lengthy screen refreshes:

if __name__ == '__main__':
    import inky_helper as ih
    import gc
    from utime import sleep
    from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY
    graphics = PicoGraphics(DISPLAY)
    WIDTH, HEIGHT = graphics.get_bounds()

    try:
        from secrets import WIFI_SSID, WIFI_PASSWORD
        ih.network_connect(WIFI_SSID, WIFI_PASSWORD)
    except ImportError:
        print("Create secrets.py with your WiFi credentials")

    gc.collect()
    update()
    draw()
    ih.led_warn.off()
Enter fullscreen mode Exit fullscreen mode

To make it work from the menu, in main.py, one of the default menu items needs to be replaced by the new code. In launcher():

  • replace the description text for one of the menu items with a description of the new widget, but keep the capital letter at the start, which indicates to the user which button to press to select it, e.g.
graphics.text("A. Test", 35, HEIGHT - 325, 600, 3)
Enter fullscreen mode Exit fullscreen mode
  • in the while True loop, replace the string in the item equivalent to the one in which the description text has been replaced with the filename (without the .py extension) of the new widget, e.g.
ih.update_state("test")
Enter fullscreen mode Exit fullscreen mode

Here is the full listing of the test.py file with some helpful comments added.

# test.py
from ntptime import settime
from machine import RTC

# Set variables required by menu
graphics = None
WIDTH = None
HEIGHT = None
UPDATE_INTERVAL = 5

# Instantiate time_string and real time clock
time_string = None
rtc = RTC()

# Update data
def update():
    global time_string
    print("Update data...")

    # Set RTC time from network
    try:
        settime()
    except OSError:
        print("Unable to contact NTP server")

    # Get time tuple and unpack it
    rtc_time = rtc.datetime()
    year, month, mday, weekday, hour, minute, second, subsecond = rtc_time

    # If year is before 2023 assume time could not be updated
    if year < 2023:
        time_string = "Unable to update time"
    else:
        # Construct time string, padding each value with leading zeros to 2 digits
        time_string = f"{mday:0=2}/{month:0=2}/{year:0=2} at {hour:0=2}:{minute:0=2}"

# Draw screen
def draw():
    global time_string

    graphics.set_font("sans")

    # Make screen black
    graphics.set_pen(0)
    graphics.clear()

    # Make writing white
    graphics.set_pen(1)

    # Write test text
    graphics.set_thickness(5)
    graphics.text("Test", int(WIDTH / 2) - 120, int(HEIGHT / 2) - 20, 600, 4)

    # Write date and time
    graphics.set_thickness(3)
    graphics.text(time_string, 0, HEIGHT - 20, 600, 1)
    graphics.update()
    print("Draw screen")

# Mimic call from menu if running this file as standalone
if __name__ == '__main__':
    import inky_helper as ih
    import gc
    from utime import sleep

    # Change this next line if you are not running the 4" Inky Frame
    from picographics import PicoGraphics, DISPLAY_INKY_FRAME_4 as DISPLAY
    graphics = PicoGraphics(DISPLAY)
    WIDTH, HEIGHT = graphics.get_bounds()

    try:
        from secrets import WIFI_SSID, WIFI_PASSWORD
        ih.network_connect(WIFI_SSID, WIFI_PASSWORD)
    except ImportError:
        print("Create secrets.py with your WiFi credentials")

    gc.collect()
    print("Updating...")
    update()
    print("Drawing...")
    draw()
    print("Clearing LEDs...")
    ih.led_warn.off()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)