Approximate Engineering - Task
0.1.0
  • Simple Usage
  • Menu Generation
    • Menu Definitions
      • Multiple Menus
      • More Concise Nested Menus
      • Returning a Value from a Menu
    • Implementing a MenuTask
    • Registering Menu Tasks
  • API
  • Change Log
Approximate Engineering - Task
  • Docs »
  • Menu Generation

Menu Generation¶

Most robots will require some kind of menu system, to select challenges, configure settings etc. To build this we need three things:

  1. A definition of the menu structure.

  2. Some way to display the current state.

  3. Some way to navigate around the menus, select items etc.

You could do all this with a bunch of if statements, but that approach very rapidly becomes unmaintainable. Instead this library provides a way to configure a menu system from a dictionary (explicitly defined, or loaded from a YAML file), and then automatically builds the corresponding tasks to display and navigate around those configured items.

Because all robots are different, you will have to write a bit of custom code to handle display and navigation. You do this by subclassing approxeng.task.menu.MenuTask and implementing two methods, get_menu_action() and display_menu(). You then use this class when calling register_menu_tasks() (to load from a dictionary) or register_menu_tasks_from_yaml() when loading menu definitions from a YAML file.

If your display and navigation system needs access to resources (it almost certainly does) such as joysticks, or display hardware, these are provided in the normal way to the menu tasks through the world object. You have access to this when implementing your custom menu task class, along with other properties you’ll need such as the currently selected item, name of the active menu and similar. When you call the register function you can explicitly specify which resources your class needs to function, this works the same way as for any other task (the menus are just tasks themselves).

To activate a menu, just switch active task to the name of the menu. This is probably something you’d do as a root task in most robots, but you might want to explicitly activate them at other times - there’s no limit to the number of menus you can have, and there’s nothing special about the root one, that’s just the task you choose to start with.

Menu Definitions¶

We’ll use YAML syntax here, but you can also build dictionaries directly (YAML is more concise, and easier to edit than a Python dict). Firstly the most simple definition - a single menu which will give the user the option to launch one of two tasks:

- name: top_menu
  title: Main Menu
  items:
    - title: First task
      task: task_a
    - title: Second task
      task: task_b

For those not familiar with YAML, this is a single item list. That item is a dictionary, with keys name, title, and items. items is a list of dictionaries with keys title and task (in this case). Hopefully it should be possible to follow what’s happening!

There’s a single menu defined here. It’s called top_menu, and should display Main Menu in some form when active. It has two possible options, First task and Second task, which will launch task_a and task_b respectively when selected. When passed in to register_menu_tasks_from_yaml() this will register a single task called top_menu, this can then be launched like any other task.

Multiple Menus¶

You can specify multiple menus in a single file, just add more items to the top level list:

- name: top_menu
  title: Main Menu
  items:
    - title: First task
      task: task_a
    - title: Go to submenu
      task: sub_menu
- name: sub_menu
  title: Sub Menu
  items:
    - title: Back
      task: top_menu
    - title: Do a thing
      task: task_c
    - title: Do another thing
      task: task_d

This defines two menus, and also provides navigation between them. There’s nothing special about menus, they just turn into named tasks so in this case the item Go to a submenu activates a task called sub_menu, and the second menu in this YAML definition creates a task called sub_menu, which in turn has an item Back which activates the top_menu task. So we have a nested menu structure, but it’s a bit clunky.

More Concise Nested Menus¶

The example above is a bit verbose, it creates an item which calls a menu task, then creates a menu task which has an item that calls the top menu task etc. This is sufficiently common that the library has a special syntax to handle it. The following will produce the same menu structure as the previous example, but with two differences:

  1. It’s a much shorter definition.

  2. The sub-menu task will have a name (all tasks do) but you haven’t explicitly defined it. That means you can’t manually activate that sub-menu. Not an issue here, if you need to do this use the form shown above.

- name: top_menu
  title: Main Menu
  items:
    - title: First task
      task: task_a
    - menu:
          title: Sub Menu
          items:
            - title: Do a thing
              task: task_c
            - title: Do another thing
              task: task_d

With a nested definition like this you don’t have to define the back task, as the menu system knows what the parent menu is and handles it with a special up action.

Returning a Value from a Menu¶

If a task returns TaskStop when called, the run() function will return the value wrapped in the TaskStop. If we wanted to use the menu system to get user input we could therefore define a set of Task instances, or equivalent functions decorated with task to return different values, and then reference those tasks from the menu structure. This would be very clunky though, and it’s a sufficiently common case that the library supports it directly.

If you create a menu such as:

- name: top_menu
  title: Main Menu
  items:
    - title: Return Foo
      return: Foo
    - title: Return Bar
      return: Bar

This will automatically create two new tasks, each of which simply returns a TaskStop wrapping the two strings Foo and Bar respectively, and rewrites the menu structure so that these new tasks are called when those options are selected.

These three kinds of things (tasks, sub-menus, and return values) can be freely mixed in any menu, the library will do the appropriate thing in each case.

Implementing a MenuTask¶

To connect the menu system to your control and display resources you’ll need to create a new subclass of MenuTask, implementing two methods:

class approxeng.task.menu.MenuTask(name, title, parent_task, resources=None)[source]

A single menu, consisting of a title and a set of items, each of which will launch a task when selected. Optionally menus may have a parent.

abstract display_menu(world, title, item_title, item_index, item_count)[source]

Display the current menu state

Parameters
  • world – Provides any resources needed to display the menu

  • title – Title for the current menu

  • item_title – Title for the currently selected item

  • item_index – Index of the currently selected item

  • item_count – Number of available items

abstract get_menu_action(world)[source]

Get the action, if any, to take on this menu item.

Parameters

world – Provides any resources needed to read the appropriate action

Returns

None for no action, or an instance of MenuAction for navigation, or an int to immediately select that index item.

As an example, let’s suppose we’re using approxeng.input as the input library, and that we’ve put the controller object into the world as a resource called joystick. The implementation of the get_menu_action() might look like this:

from approxeng.task.menu import MenuClass, MenuAction

class MyMenuClass(MenuClass):

    def get_menu_action(world):

        # Get any buttons pressed since last check
        buttons_pressed = world.joystick.presses

        if 'dleft' in buttons_pressed:
            return MenuAction.previous
        elif 'dright' in buttons_pressed:
            return MenuAction.next
        elif 'dup' in buttons_pressed:
            return MenuAction.up
        elif 'cross' in buttons_pressed:
            return MenuAction.select

This implementation gets the controller object from the world, then gets a button presses object representing any new button presses. It then checks in turn for the d-pad left, right, and up buttons, and for the cross button. And that’s all you need to do for navigation.

Next suppose we have a display module attached, it’s a simple object with a couple of methods used to set the two lines of text. We register it as a resource called display and use it to implement display_menu() in our new class to finish it:

from approxeng.task.menu import MenuClass, MenuAction

class MyMenuClass(MenuClass):

    def get_menu_action(self, world):

        # Get any buttons pressed since last check
        buttons_pressed = world.joystick.presses

        if 'dleft' in buttons_pressed:
            return MenuAction.previous
        elif 'dright' in buttons_pressed:
            return MenuAction.next
        elif 'dup' in buttons_pressed:
            return MenuAction.up
        elif 'cross' in buttons_pressed:
            return MenuAction.select

    def display_menu(self, world, title, item_title, item_index, item_count):

        # Get the display
        display = world.display

        display.set_line_1(title+' '+(item_index+1)+'/'+item_count)
        display.set_line_2(item_title)

This will show the menu title, along with an indication of the number of items on the first line of the display, and the current selected item on the second line. We can navigate through the items within a menu using the left and right d-pad buttons, select on by pressing the cross button, or go up in the menu structure (if there’s a parent menu) by pressing the d-pad up button.

Registering Menu Tasks¶

Now we have a menu definition, along with a mechanism to display and navigate it, we just have to build the corresponding tasks and register them. In this example we’re going to use a YAML file, but you could also pass in the dictionary of menus directly, the structure is exactly the same in both cases.

from approxeng.task.menu import register_menu_tasks_from_yaml
from approxeng.task import run

class MyMenuClass(MenuClass)
    # Class you created in the previous stage

register_menu_tasks_from_yaml(filename='menu_definition.yml',
                              menu_task_class=MyMenuClass,
                              resources=['joystick','display'])

run(root_task='top_menu')

This loads in the definition from the file, creates tasks for each menu, configures the menu system to use the menu class you defined, and to make the joystick and display resources available to those tasks when running. It then runs the task called top_menu, which is the task for the menu with that name in that file.

Note - you don’t ever construct an instance of your menu class, the library does that for you. Just pass in the actual class (so no brackets!).

Next Previous

© Copyright 2019, 2020 Tom Oinn Last updated on Jan 14, 2020.

Built with Sphinx using a theme provided by Read the Docs.