~n0mn0m/gencon-portal

45d0c88e951956db16ed1b96c447d4de4ee1bfd9 — Alexander Hagerman 10 months ago c30b71a + f6ec2f2
Distinguish between menu and screen in code. These are closely related for the project but adds clarity. Create a new function to handle menu transitions.
3 files changed, 142 insertions(+), 58 deletions(-)

M code.py
M lib/menu.py
M lib/utils.py
M code.py => code.py +41 -48
@@ 1,10 1,18 @@
"""
Main module for the Gen Con 2019 PyPortal badge.

Setup the PyPortal with a connection to a time api for the countdown clock functionality
and then monitor for button events and route to the menu.py module.

For more information about the PyPortal see https://learn.adafruit.com/adafruit-pyportal
"""
import board
from random import randint
from time import sleep
from analogio import AnalogIn
from adafruit_pyportal import PyPortal
from utils import countdown_formatter
from menu import render_menu, clear_menu, keep_alive, new_background, RETURN_MENU
from menu import new_screen, init_home_screen, RETURN_MENU

try:
    from secrets import secrets  # noqa


@@ 27,8 35,10 @@ pyportal = PyPortal(
pyportal.preload_font()

# Render default home menu
buttons = render_menu(pyportal)
buttons = init_home_screen(pyportal)

# Setup our super loop to manage screen brightness and monitor our home menu for
# button events.
while True:
    # Calibrate light sensor on start to deal with different lighting situations
    # If the mode change isn't responding properly, reset your PyPortal to recalibrate


@@ 44,45 54,39 @@ while True:
            mode = 0

    touch = pyportal.touchscreen.touch_point
    # In the case of a button event figure out which button was pressed and render
    # that buttons screen.
    if touch:
        for button in buttons:
            if button.contains(touch):
                if button.name == "exhibit_hall_map":
                    clear_menu(pyportal)
                    new_background(pyportal, "/images/2019-exhibit-map.bmp")
                    buttons = render_menu(pyportal, RETURN_MENU)
                    keep_alive(pyportal, buttons)
                    clear_menu(pyportal)
                    new_background(pyportal)
                    buttons = render_menu(pyportal)
                    buttons = new_screen(
                        pyportal,
                        new_background="/images/2019-exhibit-map.bmp",
                        menu=RETURN_MENU,
                    )
                    break
                elif button.name == "con_map":
                    clear_menu(pyportal)
                    new_background(pyportal, "/images/2019-con-map.bmp")
                    buttons = render_menu(pyportal, RETURN_MENU)
                    keep_alive(pyportal, buttons)
                    clear_menu(pyportal)
                    new_background(pyportal)
                    buttons = render_menu(pyportal)
                    buttons = new_screen(
                        pyportal,
                        new_background="/images/2019-con-map.bmp",
                        menu=RETURN_MENU,
                    )
                    break
                elif button.name == "schedule":
                    clear_menu(pyportal)
                    new_background(pyportal, "/images/2019-schedule.bmp")
                    buttons = render_menu(pyportal, RETURN_MENU)
                    keep_alive(pyportal, buttons)
                    clear_menu(pyportal)
                    new_background(pyportal)
                    buttons = render_menu(pyportal)
                    buttons = new_screen(
                        pyportal,
                        new_background="/images/2019-schedule.bmp",
                        menu=RETURN_MENU,
                    )
                    break
                elif button.name == "d20":
                    roll = randint(1, 20)
                    clear_menu(pyportal)
                    new_background(pyportal, "/images/d20.bmp")
                    buttons = render_menu(
                    buttons = new_screen(
                        pyportal,
                        [
                        new_background="/images/d20.bmp",
                        menu=[
                            (
                                str(roll),
                                str(randint(1, 20)),
                                "return",
                                (138, 100),
                                (45, 45),


@@ 92,18 96,13 @@ while True:
                            )
                        ],
                    )
                    keep_alive(pyportal, buttons)
                    clear_menu(pyportal)
                    new_background(pyportal)
                    buttons = render_menu(pyportal)
                    break
                elif button.name == "countdown":
                    clear_menu(pyportal)
                    new_background(pyportal, "/images/gen-con-logo.bmp")
                    day = pyportal.fetch()
                    buttons = render_menu(
                    buttons = new_screen(
                        pyportal,
                        [
                        new_background="/images/gen-con-logo.bmp",
                        menu=[
                            (
                                countdown_formatter(day),
                                "return",


@@ 115,19 114,13 @@ while True:
                            )
                        ],
                    )
                    keep_alive(pyportal, buttons)
                    clear_menu(pyportal)
                    new_background(pyportal)
                    buttons = render_menu(pyportal)
                    break
                elif button.name == "badge":
                    clear_menu(pyportal)
                    new_background(pyportal, "/images/2019-con-badge.bmp")
                    buttons = render_menu(pyportal, RETURN_MENU)
                    keep_alive(pyportal, buttons)
                    clear_menu(pyportal)
                    new_background(pyportal)
                    buttons = render_menu(pyportal)
                    buttons = new_screen(
                        pyportal,
                        new_background="/images/2019-con-badge.bmp",
                        menu=RETURN_MENU,
                    )
                    break
                else:
                    break

M lib/menu.py => lib/menu.py +91 -10
@@ 4,14 4,20 @@ from adafruit_button import Button
from adafruit_bitmap_font import bitmap_font


# Load the font at the beginning for use across the application.
FONT = bitmap_font.load_font("/fonts/Arial-Bold-12.bdf")

# label, name, pos, size, fill_color, outline_color, label_color
RETURN_MENU = [("<  ", "return", (285, 205), (30, 30), (255, 170, 0), 0x222222, 0x0)]


def default_menu():
    # label, name, pos, size, fill_color, outline_color, label_color
def _home_menu():
    """
    Return the structure for the Gen Con badge home menu.

    See the render_menu function docs for more information on the expected structure
    for a menu object.
    """
    return [
        ("Badge", "badge", (5, 35), (150, 50), (255, 170, 0), 0x222222, 0x0),
        ("Countdown", "countdown", (5, 95), (150, 50), (255, 170, 0), 0x222222, 0x0),


@@ 31,7 37,12 @@ def default_menu():


@collect
def clear_menu(portal):
def _clear_menu(portal):
    """
    Remove the buttons that have been added to the portal splash object. This should
    be done when transitioning from one menu to another, for instance the home
    menu to a screen with the return menu.
    """
    items = len(portal.splash) - 1

    if items >= 1:


@@ 40,13 51,22 @@ def clear_menu(portal):


@collect
def new_background(portal, img_or_color=0xFFFFFF):
def _set_new_background(portal, img_or_color=0xFFFFFF):
    """
    Set a new background on the PyPortal and perform garbage collection to remove
    references to open resources such as images that can have a large impact on
    memory use.
    """
    portal.set_background(img_or_color)
    del img_or_color


@collect
def keep_alive(portal, buttons):
def _keep_alive(portal, buttons):
    """
    After transitioning to a new screen we want to stay on that screen
    until the user selects a new screen to transition to.
    """
    stay = True

    while stay:


@@ 54,7 74,7 @@ def keep_alive(portal, buttons):
        if touch:
            for button in buttons:
                if button.name == "return":
                    clear_menu(portal)
                    _clear_menu(portal)
                    stay = False
                    break



@@ 62,11 82,15 @@ def keep_alive(portal, buttons):


@collect
def render_menu(portal, menu=None):
    buttons = []
def _render_menu(portal, menu):
    """
    Provided a new menu render that menu on screen.

    if not menu:
        menu = default_menu()
    The menu object is a list of tuples.

    The tuple layout is (label, name, pos, size, fill_color, outline_color, label_color)
    """
    buttons = []

    for item in menu:
        button = Button(


@@ 86,3 110,60 @@ def render_menu(portal, menu=None):
        buttons.append(button)

    return buttons


@collect
def new_screen(portal, new_background=0xFFFFFF, menu=None):
    """
    This function manages transitioning from one screen to another. In the scope of
    this project it specifically handles transitioning from the home screen/menu
    to a new screen and back.

    This function is almost a mini FSM, but doesn't maintain a lot of state since
    for the Gen Con portal badge the state is either Home Menu, or a menu item screen
    that only allows you to return to the home menu.

    If the menu object is provided it needs to be compatible with the _render_menu
    function which expects a menu to represented by the following data structure format:

    [
        (label, name, pos, size, fill_color, outline_color, label_color),
    ]

    With N tuples allowed although menus with more than 6 buttons or extremely
    large buttons in quantities greater than 4 may provde unstable.
    """

    # If we are not given a new menu assume we are transitioning back to the home menu
    # which is the default menu for this project.
    if not menu:
        menu = _home_menu()

    # Begin transition to the menu item screen.
    _clear_menu(portal)
    _set_new_background(portal, new_background)

    # Capture the new menu buttons so we can wait for the next button event indicating
    # the next transition back to the home screen.
    buttons = _render_menu(portal, menu)
    _keep_alive(portal, buttons)

    # Begin transition back to home
    _clear_menu(portal)
    _set_new_background(portal)

    # Return the new buttons to the super loop to monitor for the next event on the
    # home menu.
    buttons = _render_menu(portal, _home_menu())
    return buttons


@collect
def init_home_screen(portal):
    """
    When the device first starts we need to load the home menu for the first time, but
    then return control to let the super loop takeover.
    """
    menu = _home_menu()
    buttons = _render_menu(portal, menu)
    return buttons

M lib/utils.py => lib/utils.py +10 -0
@@ 2,6 2,11 @@ import gc


def collect(func):
    """
    For potentially expensive operations perform a garbage collection cycle before
    and after the operation.
    """

    def collect_before_and_after(*args, **kwargs):
        gc.collect()
        val = func(*args, **kwargs)


@@ 12,6 17,11 @@ def collect(func):


def countdown_formatter(days):
    """
    Given we know what day Gen Con is occuring on do some math
    and format the string to provide a count down of days until
    Gen Con.
    """
    gen_con_day_of_year = 211

    count = gen_con_day_of_year - days