Welcome to ZPUI documentation!

ZPUI stands for ZeroPhone UI, it’s the official user interface for ZeroPhone (installed on ZeroPhone official SD card images). It allows you to interact with your ZeroPhone, using the 1.3” OLED and the 30-button numpad.

ZPUI is based on pyLCI, a general-purpose UI for embedded devices. However, unlike pyLCI, ZPUI is tailored for the ZeroPhone hardware, namely, the 1.3” monochrome OLED and 30-key numpad (though it still retains input&output drivers from pyLCI), and it also ships with ZeroPhone-specific applications.

References:

Development plans

Contact us

Working on documentation

Installing and updating ZPUI

Installing ZPUI on a ZeroPhone

ZPUI is installed by default on official ZeroPhone SD card images. However, if for some reason you don’t have it installed on your ZeroPhone’s SD card, or if you’d like to install ZPUI on some other OS, this is what you have to do:

Installation

git clone https://github.com/ZeroPhone/ZPUI
cd ZPUI/
#Install main dependencies (apt and pip packages), configure systemd, and create a system-wide ZPUI copy
sudo ./setup.sh
#Start the system to test your configuration - do screen and buttons work OK?
sudo python main.py
#Once tested:
sudo ./update.sh #Transfer the working system to your system-wide ZPUI copy

Behind the scenes

There are two ZPUI copies on your system - your local copy, which you downloaded ZPUI into, and a system-wide copy, which is where ZPUI is launched from when it’s started as a service (typically, /opt/zpui). When you run ./setup.sh, the system-wide (/opt/zpui) ZPUI copy is created, and a systemd unit file registered to run ZPUI from /opt/zpui at boot. The system-wide copy can then be updated from the local copy using the ./update.sh script. If you plan on modifying your ZPUI install, it’s suggested you stick to a workflow like this:

  • Make your changes in the local copy
  • Stop the ZPUI service (to prevent it from grabbing the input&output devices), using sudo systemctl stop zpui.service.
  • Test your changes in the local directory, using sudo python main.py
  • If your changes work, transfer them to the system-wide directory using sudo ./update.sh

Such a workflow is suggested to allow experimentation while making it harder to lock you out of the system, given that ZPUI is the primary interface for ZeroPhone and if it’s inaccessible, it might prevent you from knowing its IP address, connecting it to a wireless network or turning on SSH. In documentation, /opt/zpui will be referred to as system-wide copy, while the directory you cloned the repository into will be referred to as local copy.

Updating

To get new ZPUI changes from GitHub, you can run “Settings” -> “Update ZPUI” from the main ZPUI menu, which will update the system-wide copy by doing git pull.

If you want to sync your local copy to the system-wide copy, you can run update.sh It 1) automatically pulls new commits from GitHub and 2) copies all the changes from local directory to the system-wide directory.

Tip

To avoid pulling the new commits from GitHub when running ./update.sh, just comment the corresponding line out from the update.sh script.

Systemctl commands

To control the system-wide ZPUI copy, you can use the following commands:

  • systemctl start zpui.service
  • systemctl stop zpui.service
  • systemctl status zpui.service

Launching the system manually

For testing configuration or development, you will want to launch ZPUI directly so that you will see the logs and will be able to stop it with a simple Ctrl^C. In that case, just run ZPUI with sudo python main.py from your local (or system-wide) directory.


Installing the ZPUI emulator

_images/ZPUI_Emulator.png

If you want to develop ZPUI apps, but don’t yet have the ZeroPhone hardware, there’s an option to use the emulator with a Linux PC - the emulator can use your screen and keyboard instead of ZeroPhone hardware. The emulator works very well for app development, as well as for UI element and ZPUI core feature development.

System requirements

  • Some kind of Linux - there are install instructions for Ubuntu, Debian and OpenSUSE, but it will likely work with other systems, too
  • Graphical environment (the emulator is based on Pygame)
  • A keyboard (the same keyboard that you’re using for the system will work great)

Ubuntu/Debian installation

Assuming Python 2 is the default Python version:

sudo apt-get update
sudo apt-get install python-pip git python-dev build-essential python-pygame
sudo pip install luma.emulator
git clone https://github.com/ZeroPhone/ZPUI
cd ZPUI
./setup_emulator
#Run the emulator
python main.py

Arch Linux installation

sudo pacman -Si python2-pip git python2-pygame
sudo pip2 install luma.emulator

git clone https://github.com/ZeroPhone/ZPUI
cd ZPUI
./setup_emulator
#Run the emulator
python2 main.py

OpenSUSE installation

sudo zypper install python2-pip git python2-devel gcc python2-curses python2-pygame #If python2- version is not available, try python- and report on IRC - can't test it now
sudo pip2 install luma.emulator
git clone https://github.com/ZeroPhone/ZPUI
cd ZPUI
./setup_emulator
#Run the emulator
python2 main.py

Emulator credits

Most of the emulator research and work was done by Doug, and later refactored by Brian Dunlay. The input driver was done by Arsenijs. OpenSUSE instructions were compiled with help of piajesse. Arch Linux instructions were compiled by monsieurh.

ZPUI configuration files

ZPUI config.json

Important

By default, ZeroPhone SD card images and ZPUI installs ship with config.json files that are suitable for usage out-of-the-box. Unless you want to tweak your IO drivers’ initialization parameters or need to debug ZPUI in case of hardware trouble, you won’t need to edit ZPUI configuration files.

ZPUI depends on a config.json file to initialize the input and output devices. To be exact, it expects a JSON-formatted file in one of the following paths (sorted by order in which ZPUI attempts to load them):

  • /boot/zpui_config.json
  • /boot/pylci_config.json
  • {ZPUI directory}/config.json
  • {ZPUI directory}/config.example.json (a fallback file that you shouldn’t edit manually)

Note

The config.json tells ZPUI which output and input hardware it needs to use, so invalid configuration might lock you out of the system. Thus, it’s better to make changes in /boot/zpui_config.json - if you screw up and lock yourself out of ZPUI, it’s easier to revert the changes since you can do it by just plugging your microSD card in another computer and editing the file. You can also delete (or rename) the file to make ZPUI fallback on a default config file.

ZPUI config format

Here’s the default ZPUI config right now:

{
 "input":
 [
  {
   "driver":"custom_i2c"
  }
 ],
 "output":
 [
  {
   "driver":"sh1106",
   "kwargs":
   {
    "backlight_interval":10
   }
  }
 ]
}

Here’s the config file format:

{
  "input":
  [{
    "driver":"driver_filename",
    "args":[ "value1", "value2", "value3"...]
  }],
"output":
  [{
    "driver":"driver_filename",
    "kwargs":{ "key":"value", "key2":"value2"}
  }]
}

Documentation for input and output drivers might have sample config.json sections for each driver. "args" and "kwargs" get passed directly to drivers’ __init__ method, so you can read the driver documentation or source to see if there are options you could tweak.

Verifying your changes

You can use jq to verify that you didn’t make any JSON formatting mistakes:

jq '.' config.json

If the file is correct, it’ll print it back. If there’s anything wrong with the JSON formatting, it’ll print an error message:

pi@zerophone:~/ZPUI#$ jq '.' config.json parse error: Expected separator between values at line 7, column 10

You might need to install jq beforehand:

sudo apt-get install jq

If you’re editing the config.json file externally, you might not have access to the command-line. In that case, you can use an online JSON validator, such as jsonlint.com - copy-paste contents of config.json there to see if the syntax is correct.

App-specific configuration files

TODO

This section is not yet ready. Sorry for that!

Useful examples

Blacklisting the phone app to get access to UART console

You might find yourself with a cracked screen one day, and needing to connect to your ZeroPhone nevertheless. In the unfortunate case you can’t connect it to a wireless network in order to SSH into it (as the interface is inaccessible with a cracked screen), you can use a USB-UART to get to a console accessible on the UART port.

Unfortunately, console on the UART is disabled by default - because UART is also used for the GSM modem. However, you can tell ZPUI to not disable UART by disabling the phone app, and thus enabling the USB-UART debugging. To do that, you need to:

  1. Power down your ZeroPhone - since you can’t access the UI, you have no other choice but to shutdown it unsafely by unplugging the battery.
  2. Unplug the MicroSD card and plug it into another computer - both Windows and Linux will work
  3. On the first partition (the boot partition), locate the zpui_config.json file
  4. In that file, add an "app_manager" dictionary (a “collection” in JSON terms)
  5. Add the path to the phone app to a "do_not_load" list inside of it

The resulting file should look like this, as a result:

{
 "input": ... ,
 "output": ... ,
 "app_manager": {
    "do_not_load":
       ["apps/phone/"]
  }
}

Now, boot your phone with this config and you should be able to log in over UART!

Note

Since you’re editing the config.json file externally, you should make sure it’s valid JSON - here’s a guide for that.

How to…

Do you want to improve your ZPUI app or solve your problem by copy-pasting a snippet in your app code? This page is for you =)

Basics

What’s the minimal ZPUI app?

In app/main.py:

menu_name = "Skeleton app"

i = None #Input device
o = None #Output device

def init_app(input, output):
    #Gets called when app is loaded
    global i, o
    i = input; o = output

def callback():
    #Gets called when app is selected from menu
    pass

app/__init__.py has to be an empty file:



What’s the minimal class-based app?

In app/main.py:

from apps import ZeroApp

class YourGreatApp(ZeroApp):
    menu_name = "Skeleton app"

    def on_start():
        #Gets called when app is selected from menu
        pass

app/__init__.py has to be an empty file, as with the previous example.


Experiment with ZPUI code

You can use the sandbox app to try out ZPUI code. First, stop the system-wide ZPUI process if it’s running (use sudo systemctl stop zpui). Then, run this in the install folder:

sudo python main.py -a apps/example_apps/sandbox
[...]
Python 2.7.13 (default, Nov 24 2017, 17:33:09)
[GCC 6.3.0 20170516] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

Available variables:

>>> dir()
['__builtins__', '__code__', '__doc__', '__file__', '__name__', '__package__',
'callback', 'context', 'i', 'init_app', 'menu_name', 'o', 'set_context']

In short, you get i, o, a context object, and you can import all the usual things you’d import in your app - like UI elements

>>> from ui import Canvas
>>> c = Canvas(o, interactive=True)
>>> c.centered_text("Hello world!")
_images/canvas_test_7.png

User-friendliness

Whether your app involves a complex task, a task that could be done in multiple different ways or just something plain and simple, there are UI elements, functions and snippets that can help you make your app more accessible to the user.

Confirm a choice

In case you’re unsure the user will want to proceed with what you’re doing, you might want them to confirm their actions. Here’s how to ask them that:

from ui import DialogBox

message = "Are you sure?"
choice = DialogBox ('ync', i, o, message=message, name="HDD secure erase app erase confirmation").activate()
if choice:
    erase_hdd(device_path)

By default, Yes returns True, No returns False and Cancel returns None.

Pick one thing out of many

If you have multiple things and you need your user to pick one, here’s how to let them choose:

from ui import Listbox, PrettyPrinter
...
# You pass lists of two elements - first one is the user-friendly label,
# second is something that your code can actually use
# (doesn't have to be a string)
lc = [["Kingston D4", "/dev/bus/usb/001/002"], ["Sandisk Ultra M3", "/dev/bus/usb/001/002"]]
# The user will want to know what is it you want them to choose;
# Showing a quick text message is a good way to do it
PrettyPrinter("More than one drive found, pick a flash drive", i, o, 5)
path = Listbox(lc, i, o, name="USB controller flashing app drive selection menu").activate()
if path: # if the user pressed left key to cancel the choice, None is returned
    print(path)

Note

If you autogenerate the listbox contents from an external source (for example, your user needs to pick one flash drive from a list of all connected flash drives), it’s best if you check that the user really has any choice in the matter - as in, maybe there’s only one flash drive connected?


Enable/disable options

If you want user to be able to enable or disable settings or let them filter through a really long list of options to choose from, here’s what you can do:

from ui import Checkbox
...
# You pass lists of two/three elements - first one is the user-friendly label
# second is something that you'll receive as a response dictionary key,
# and you can optionally add the third element telling the default state
# (True/False)
# (doesn't have to be a string)
cc = [["Replace files that were changed", "replace_on_change", config["replace_on_change"]],
      ["Delete files from destination", "delete_in_destination", config["delete_in_destination"]],
      ["Save these settings", "save_settings"]]
choices = Checkbox(cc, i, o, name="Backup app options dialog").activate()
if choices: # if the user pressed left key to cancel the choice, None is returned
    print(choices)
# {"replace_on_change":True, "delete_in_destination":False, "save_settings":False}

Pick a file/directory

In case your user needs to work with files, here’s how you can make the file picking process easy for them:

from ui import PathPicker
...
# You might already have some kind of path handy - maybe the one that your user
# picked last time?
path = os.path.split(last_path)[0] if last_path else '/'
new_path = PathPicker(path, self.i, self.o, name="Shred app file picker").activate()
if new_path: # As usual, the user can cancel the selection
    self.last_path = new_path # Saving it for usability

The PathPicker also supports a callback attribute which, instead of letting the user pick one file and returning it, lets the user just click on files and calls a function on each one of them as they’re selected. An example of this working is the “File browser” app in “Utils” category of the main menu.

Allow exiting a loop on a keypress

Say, you have a loop that doesn’t have an UI element in it - you’re just doing something repeatedly. You’ll want to let the user exit that loop, and the reasonable way is to interrupt the loop when the user presses a key (by default, KEY_LEFT). Here’s how to allow that:

from helpers import ExitHelper
...
eh = ExitHelper(i).start()
while eh.do_run():
    ... #do something repeatedly until the user presses KEY_LEFT

Stopping a foreground task on a keypress

If you have some kind of task that’s running in foreground (say, a HTTP server), you will want to let the user exit the UI, at least - maybe even stop the task. If a task can be stopped from another thread, you can use ExitHelper, too - it can call a custom function that would signal the task to stop.

from helpers import ExitHelper
...
task = ... # Can be run in foreground with ``task.run()``
# Can also be stopped from another thread with ``task.stop()``
eh = ExitHelper(i, cb=task.stop).start()
task.run() # Will run until the task is not stopped

Draw on the screen

Display an image

You can easily draw an image on the screen with ZPUI. The easiest way is by using the display_image method of OutputProxy object:

o.display_image(image) #A PIL.Image object

However, you might want a user-friendly wrapper around it that would allow you to easily load images by filename, invert, add a delay/exit-on-key etc. In this case, you’ll want to use the GraphicsPrinter UI element, which accepts either a path to an image you want to display, or a PIL.Image instance and supports some additional arguments:

from ui import GraphicsPrinter
...
# Will display the ZPUI splash image for 1 second
# By default, it's inverted
GraphicsPrinter("splash.png", i, o, 1)
# Same, but the image is not inverted
GraphicsPrinter("splash.png", i, o, 1, invert=False)
# Display an image from the app folder - using the local_path helper
GraphicsPrinter(local_path("image.png"), i, o, 1)
# Display an image you drew on a Canvas
GraphicsPrinter(c.get_image(), i, o, 1)

In case you have a Canvas object and you just want to display it, there’s a shorthand:

c.display()

Draw things on the screen - basics

Uou can use the Canvas objects to draw on the screen.

from ui import Canvas
...
c = Canvas(o) # Create a canvas
c.point((1, 2)) # Draw a point at x=1, y=2
c.point( ( (2, 1), (2, 3), (3, 4) ) ) # Draw some more points
... # Draw other stuff here
c.display() # Display the canvas on the screen
_images/canvas_test_1.png

Draw text

You can draw text on the screen, and you can use different fonts. By default, a 8pt font is used:

c = Canvas(o)
c.text("Hello world", (0, 0)) # Draws "Hello world", starting from the top left corner
c.display()
_images/canvas_test_2.png

You can also use a non-default font - for example, the Fixedsys62 font in the ZPUI font storage:

c.text("Hello world", (0, 0), font=("Fixedsys62.ttf", 16)) # Same, but in a 16pt Fixedsys62 font
c.text("Hello world", (0, 0), font=(local_path("my_font.ttf"), 16) ) # Using a custom font from your app directory

Draw centered text

You can draw centered text, too!

c = Canvas(o)
c.centered_text("Hello world") # Draws "Hello world" in the center of the screen
c.display()
_images/canvas_test_7.png

You can also draw text that’s centered on one of the dimensions:

c = Canvas(o)
ctc = c.get_centered_text_bounds("a") # Centered Text Coords
# ctc == Rect(left=61, top=27, right=67, bottom=37)
c.text("a", (ctc.left, 0))
c.text("b", (str(ctc.left-ctc.right), ctc.top)) # ('-6', 27)
c.text("c", (ctc.left, str(ctc.top-ctc.bottom))) # (61, '-10')
c.text("d", (0, ctc.top))
c.display()
_images/canvas_test_8.png

Draw a line

c = Canvas(o)
c.line((10, 4, "-8", "-4")) # Draws a line from top left to bottom right corner
c.display()
_images/canvas_test_3.png

Draw a rectangle

c = Canvas(o)
c.rectangle((10, 4, 20, "-10")) # Draws a rectangle in the left of the screen
c.display()
_images/canvas_test_4.png

Draw a circle

c = Canvas(o)
c.circle(("-8", 8, 4)) # Draws a circle in the top left corner - with radius 4
c.display()
_images/canvas_test_5.png

Note

There’s also a Canvas.ellipse() method, which takes four coordinates instead of two + radius.


Invert a region of the screen

If you want to highlight a region of the screen, you might want to invert it:

c = Canvas(o)
c.text("Hello world", (5, 5))
c.invert_rect((35, 5, 80, 17)) # Inverts, roughly, the right half of the text
c.display()
_images/canvas_test_6.png

Note

To invert the whole screen, you can use the invert method.


Make your app easier to support

Add logging to your app

In case your application does something more complicated than printing a sentence on the display and exiting, you might need to add logging - so that users can then look through the ZPUI history, figure out what was it that went wrong, and maybe submit a bugreport to you!

from helpers import setup_logger # Importing the needed function
logger = setup_logger(__name__, "warning") # Getting a logger for your app,
# default level is "warning" - this level controls logging statements that
# will be displayed (and saved in the logfile) by default.

...

try:
    command = "my_awesome_script"
    logger.info("Calling the '{}' command".format(command))
    output = call(command)
    logger.debug("Finished executing the command")
    for value in output.split():
        if value not in expected_values:
            logger.warning("Unexpected value {} found when parsing command output; proceeding".format(value))
except:
    logger.exception("Exception while calling the command!")
    # .exception will also log the details of the exception after your message

Add names to your UI elements

UI elements aren’t perfect - sometimes, they themselves cause exceptions. In this case, we’ll want to be able to debug them, to make sure we understand what was it that went wrong. Due to the nature of ZPUI and how multiple apps run in parallel, we need to be able to distinguish logs from different UI elements - so, each UI element has a name attribute, and it’s included in log messages for each UI element. By default, the attribute is set to something non-descriptive - we highly suggest you set it to tell:

  • which app the UI element belongs to
  • which part of the app the UI element is created

For example:

from ui import Menu
...
Menu(contents, i, o, name="Main menu of Frobulator app".activate()

Note

The only UI elements that don’t support the name attribute are Printers: Printer, GraphicsPrinter and PrettyPrinter

Config (and other) files

Read JSON from a config file located in the app directory

from helpers import read_config, local_path_gen
config_filename = "config.json"

local_path = local_path_gen(__name__)
config = read_config(local_path(config_filename))

Read a config file with an easy “save” function and “restore to defaults on error” check

from helpers import read_or_create_config, local_path_gen, save_config_gen
default_config = '{"your":"default", "config":"to_use"}' #has to be a string
config_filename = "config.json"

local_path = local_path_gen(__name__)
config = read_or_create_config(local_path(config_filename), default_config, menu_name+" app")
save_config = save_config_gen(local_path(config_filename))

To save the config, use save_config(config) from anywhere in your app.

Note

The faulty config.json file will be copied into a config.json.faulty file before being overwritten

Warning

If you’re reassigning contents of the config variable from inside a function, you will likely want to use Python global keyword in order to make sure your reassignment will actually work.


“Read”, “save” and “restore” - in a class-based app

from helpers import read_or_create_config, local_path_gen, save_config_method_gen
local_path = local_path_gen(__name__)

class YourApp(ZeroApp):

    menu_name = "My greatest app"
    default_config = '{"your":"default", "config":"to_use"}' #has to be a string
    config_filename = "config.json"

    def __init__(self, *args, **kwargs):
        ZeroApp.__init__(self, *args, **kwargs)
        self.config = read_or_create_config(local_path(self.config_filename), self.default_config, self.menu_name+" app")
        self.save_config = save_config_method_gen(local_path(self.config_filename))

To save the config, use self.save_config() from anywhere in your app class.


Get path to a file in the app directory

Say, you have a my_song.mp3 file shipped with your app. However, in order to use that file from your code, you have to refer to that file using a path relative to the ZPUI root directory, such as apps/personal/my_app/my_song.mp3.

Here’s how to get that path automatically, without hardcoding which folder your app is put in:

from helpers import local_path_gen
local_path = local_path_gen(__name__)
mp3_file_path = local_path("my_song.mp3")

In case of your app having nested folders, you can also give multiple arguments to local_path():

song_folder = "songs/"
mp3_file_path = local_path(song_folder, "my_song.mp3")

Run tasks on app startup

How to do things on app startup in a class-based app?

def __init__(self, *args, **kwargs):
    ZeroApp.__init__(self, *args, **kwargs)
    # do your thing

Run a short task only once when your app is called

This is suitable for short tasks that you only call once, and that won’t conflict with other apps.

def init_app(i, o):
    ...
    init_hardware() #Your task - short enough to run while app is being loaded

Warning

If there’s a chance that the task will take a long time, use one of the following methods instead.


Run a task only once, first time when the app is called

This is suitable for tasks that you can only call once, and you’d only need to call once the user activates the app (maybe grabbing some resource that could conflict with other apps, such as setting up GPIO or other interfaces).

from helpers import Oneshot
...
def init_hardware():
    #can only be run once

#since oneshot is only defined once, init_hardware function will only be run once,
#unless oneshot is reset.
oneshot = Oneshot(init_hardware)

def callback():
    oneshot.run() #something that you can't or don't want to init in init_app
    ... #do whatever you want to do

Run a task in background after the app was loaded

This is suitable for tasks that take a long time. You wouldn’t want to execute that task directly in init_app(), since it’d stall loading of all ZPUI apps, not allowing the user to use ZPUI until your app has finished loading (which is pretty inconvenient for the user).

from helpers import BackgroundRunner
...
def init_hardware():
    #takes a long time

init = BackgroundRunner(init_hardware)

def init_app(i, o):
    ...
    init.run() #something too long that just has to run in the background,
    #so that app is loaded quickly, but still can be initialized.

def callback():
    if init.running: #still hasn't finished
        PrettyPrinter("Still initializing...", i, o)
        return
    elif init.failed: #finished but threw an exception
        PrettyPrinter("Hardware initialization failed!", i, o)
        return
    ... #everything initialized, can proceed safely

Context management

Contexts are the core concept of ZPUI multitasking. They allow you to switch between apps dynamically, use notifications, global hotkeys etc. One common usage of contexts would be creating menus that appear on a button press.

Get the context object

In order to interact with your app’s context object, you first need to get it. If your app is a simple one (function-based), you need to add a set_context() method that needs to accept a context object as its first argument. This function will be called after init_app is called. In case of a class-based app, you need to have a set_context() method in the app’s class. Once you get the context object, you can do whatever you want with it and, optionally, save it internally. Here’s an example for the function-based apps:

def set_context(received_context):
    global context
    context = received_context
    # Do things with the context

Here’s an example for the class-based apps:

def set_context(self, received_context):
    self.context = received_context
    # Do things with the context

Check and request focus for your app

User can switch from your app at any time, leaving it in the background. You won’t receive any key input in the meantime - the screen interactions will work as intended regardless of whether your app is the one active, but the actual screen won’t be updated with your images until the user switches back to your app. Here’s how to check whether your app is the one active, and request the context manager to switch to your app:

if not context.is_active():
    has_switched = context.request.switch()
    if has_switched:
        ... # Request to switch has been granted, your app is now the one active

Warning

Don’t overuse this capability - only use it when it’s absolutely necessary, otherwise the user will be annoyed. Also, keep in mind that your request might be denied.

Set a global key callback for your app

You can define a hotkey for your app to request focus - or do something else. This way, you can have a function from your app be called when a certain key is pressed from any place in the interface.

# Call a function from your app without switching to it
context.request_global_keymap({"KEY_F6":function_you_want_to_call})
# Request switch to your app
context.request_global_keymap({"KEY_F6":self.context.request_switch})

The request_global_keymap call returns a dictionary with a keyname as a key for each requested callback, with True as the value if the key was set or, if an exception was raised while setting the , an exception object.

UI element reference

UI elements are used in applications and some core system functions to interace with the user. For example, the Menu element is used for making menus, and can as well be used to show lists of items.

Using UI elements in your applications is as easy as doing:

from ui import ElementName

and initialising them, passing your UI element contents and parameters, as well as input and output device objects as initialisation arguments.

UI elements:

Canvas

from ui import Canvas
...
c = Canvas(o)
c.text("Hello world", (10, 20))
c.display()
class ui.canvas.Canvas(o, base_image=None, name='', interactive=False)[source]

Bases: object

This object allows you to work with graphics on the display quicker and easier. You can draw text, graphical primitives, insert bitmaps and do other things that the PIL library allows, with a bunch of useful helper functions.

Args:

  • o: output device
  • base_image: a PIL.Image to use as a base, if needed
  • name: a name, for internal usage
  • interactive: whether the canvas updates the display after each drawing
background_color = 'black'

default background color to use for drawing

default_color = 'white'

default color to use for drawing

width = 0

width of canvas in pixels.

height = 0

height of canvas in pixels.

size = (0, 0)

a tuple of (width, height).

image = None

PIL.Image object the Canvas is currently operating on.

load_font(path, size, alias=None, type='truetype')[source]

Loads a font by its path for the given size, then returns it. Also, stores the font in the canvas.py font_cache dictionary, so that it doesn’t have to be re-loaded later on.

Supports both absolute paths, paths relative to root ZPUI directory and paths to fonts in the ZPUI font directory (ui/fonts by default).

point(coord_pairs, **kwargs)[source]

Draw a point, or multiple points on the canvas. Coordinates are expected in ((x1, y1), (x2, y2), ...) format, where x* & y* are coordinates of each point you want to draw.

Keyword arguments:

  • fill: point color (default: white, as default canvas color)
line(coords, **kwargs)[source]

Draw a line on the canvas. Coordinates are expected in (x1, y1, x2, y2) format, where x1 & y1 are coordinates of the start, and x2 & y2 are coordinates of the end.

Keyword arguments:

  • fill: line color (default: white, as default canvas color)
  • width: line width (default: 0, which results in a single-pixel-wide line)
text(text, coords, **kwargs)[source]

Draw text on the canvas. Coordinates are expected in (x, y) format, where x & y are coordinates of the top left corner.

You can pass a font keyword argument to it - it accepts either a PIL.ImageFont object or a tuple of (path, size), which are then supplied to Canvas.load_font().

Do notice that order of first two arguments is reversed compared to the corresponding PIL.ImageDraw method.

Keyword arguments:

  • fill: text color (default: white, as default canvas color)
rectangle(coords, **kwargs)[source]

Draw a rectangle on the canvas. Coordinates are expected in (x1, y1, x2, y2) format, where x1 & y1 are coordinates of the top left corner, and x2 & y2 are coordinates of the bottom right corner.

Keyword arguments:

  • outline: outline color (default: white, as default canvas color)
  • fill: fill color (default: None, as in, transparent)
polygon(coord_pairs, **kwargs)[source]

Draw a polygon on the canvas. Coordinates are expected in ((x1, y1), (x2, y2), (x3, y3),  [...]) format, where xX and yX are points that construct a polygon.

Keyword arguments:

  • outline: outline color (default: white, as default canvas color)
  • fill: fill color (default: None, as in, transparent)
circle(coords, **kwargs)[source]

Draw a circle on the canvas. Coordinates are expected in (xc, yx, r) format, where xc & yc are coordinates of the circle center and r is the radius.

Keyword arguments:

  • outline: outline color (default: white, as default canvas color)
  • fill: fill color (default: None, as in, transparent)
ellipse(coords, **kwargs)[source]

Draw a ellipse on the canvas. Coordinates are expected in (x1, y1, x2, y2) format, where x1 & y1 are coordinates of the top left corner, and x2 & y2 are coordinates of the bottom right corner.

Keyword arguments:

  • outline: outline color (default: white, as default canvas color)
  • fill: fill color (default: None, as in, transparent)
get_image()[source]

Get the current PIL.Image object.

get_center()[source]

Get center coordinates. Will not represent the physical center - especially with those displays having even numbers as width and height in pixels (that is, the absolute majority of them).

invert()[source]

Inverts the image that Canvas is currently operating on.

display()[source]

Display the current image on the o object that was supplied to Canvas.

clear(coords=None, fill=None)[source]

Fill an area of the image with default background color. If coordinates are not supplied, fills the whole canvas, effectively clearing it. Uses the background color by default.

check_coordinates(coords, check_count=True)[source]

A helper function to check and reformat coordinates supplied to functions. Currently, accepts integer coordinates, as well as strings - denoting offsets from opposite sides of the screen.

check_coordinate_pairs(coord_pairs)[source]

A helper function to check and reformat coordinate pairs supplied to functions. Each pair is checked by check_coordinates.

get_text_bounds(text, font=None)[source]

Returns the dimensions for a given text. If you use a non-default font, pass it as font.

get_centered_text_bounds(text, font=None)[source]

Returns the coordinates for the text to be centered on the screen. The coordinates come wrapped in a Rect object. If you use a non-default font, pass it as font.

invert_rect(coords)[source]

Inverts the image in the given rectangle region. Is useful for highlighting a part of the image, for example.

class ui.canvas.MockOutput(width=128, height=64, type=None, device_mode='1')[source]

A mock output device that you can use to draw icons and other bitmaps using Canvas.

Keyword arguments:

  • width
  • height
  • type: ZPUI output device type list (["b&w-pixel"] by default)
  • device_mode: PIL device.mode attribute (by default, '1')

Printer UI element

from ui import Printer
Printer(["Line 1", "Line 2"], i, o, 3, exitable=True)
Printer("Long lines will be autosplit", i, o, 1)
ui.printer.Printer(message, i, o, sleep_time=1, skippable=True)[source]

Outputs a string, or a list of strings, on a display as soon as it’s called. A string will be split into a list, a list will not be modified. The resulting list is then displayed string-by-string. If resulting strings will take more than one screen, they’ll be split into multiple screenfuls and shown one-by-one.

Args:

  • message: A string or list of strings to display.
  • i, o: input&output device objects. If you’re not using skippable=True and don’t need exit on KEY_LEFT, feel free to pass None as i.

Kwargs:

  • sleep_time: Time to display each the message (for each of resulting screens).
  • skippable: If set, allows skipping message screens by presing ENTER.
ui.printer.PrettyPrinter(text, i, o, *args, **kwargs)[source]

Outputs string data on display as soon as it’s called. Will pass the data through format_for_screen function before passing it on to Printer. If text will take more than one screen, it’ll be split into multiple screenfuls to fit.

Args:

  • message: A string to be displayed.
  • i, o: input&output device objects. If you’re not using skippable=True and don’t need exit on KEY_LEFT, feel free to pass None as i.

Kwargs:

  • sleep_time: Time to display each screenful of text.
  • skippable: If set, allows skipping screens by presing ENTER.
ui.printer.GraphicsPrinter(image_or_path, i, o, sleep_time=1, invert=True)[source]

Outputs image on the display, as soon as it’s called. You can use either a PIL image, or a relative/absolute path to a suitable image

Args:

  • image_or_path: Either a PIL image or path to an image to be displayed.
  • i, o: input&output device objects. If you don’t need/want exit on KEY_LEFT, feel free to pass None as i.

Kwargs:

  • sleep_time: Time to display the image
  • invert: Invert the image before displaying (True by default)

Refresher UI element

from ui import Refresher
counter = 0
def get_data():
    counter += 1
    return [str(counter), str(1000-counter)] #Return value will be sent directly to output.display_data
Refresher(get_data, i, o, 1, name="Counter view").activate()
class ui.refresher.Refresher(refresh_function, i, o, refresh_interval=1, keymap=None, name='Refresher')[source]

A Refresher allows you to update the screen on a regular interval. All you need is to provide a function that’ll return the text/image you want to display; that function will then be called with the desired frequency and the display will be updated with whatever it returns.

__init__(refresh_function, i, o, refresh_interval=1, keymap=None, name='Refresher')[source]

Initialises the Refresher object.

Args:

  • refresh_function: a function which returns data to be displayed on the screen upon being called, in the format accepted by screen.display_data() or screen.display_image(). To be exact, supported return values are:
    • Tuples and lists - are converted to lists and passed to display_data()
    • Strings - are converted to a single-element list and passed to display_data()
    • PIL.Image objects - are passed to display_image()
  • i, o: input&output device objects

Kwargs:

  • refresh_interval: Time between display refreshes (and, accordingly, refresh_function calls).
  • keymap: Keymap entries you want to set while Refresher is active. By default, KEY_LEFT deactivates the Refresher, if you wan tto override it, do it carefully.
  • name: Refresher name which can be used internally and for debugging.
activate()[source]

A method which is called when refresher needs to start operating. Is blocking, sets up input&output devices, renders the refresher, periodically calls the refresh function&refreshes the screen while self.in_foreground is True, while refresher callbacks are executed from the input device thread.

deactivate()[source]

Deactivates the refresher completely, exiting it.

print_name()[source]

A debug method. Useful for hooking up to an input event so that you can see which refresher is currently active.

Checkbox UI element

from ui import Checkbox
contents = [
["Apples", 'apples'], #"Apples" will not be checked on activation
["Oranges", 'oranges', True], #"Oranges" will be checked on activation
["Bananas", 'bananas']]
selected_fruits = Checkbox(checkbox_contents, i, o).activate()
class ui.checkbox.Checkbox(*args, **kwargs)[source]

Implements a checkbox which can be used to enable or disable some functions in your application.

Attributes:

  • contents: list of checkbox entries which was passed either to Checkbox constructor or to checkbox.set_contents().

    Checkbox entry structure is a list, where:
    • entry[0] (entry label) is usually a string which will be displayed in the UI, such as “Option 1”. In case of entry_height > 1, can be a list of strings, each of which represents a corresponding display row occupied by the entry.
    • entry[1] (entry name) is a name returned by the checkbox upon its exit in a dictionary along with its boolean value.
    • entry[2] (entry state) is the default state of the entry (checked or not checked). If not present, assumed to be`` default_state``.

    If you want to set contents after the initalisation, please, use set_contents() method.

  • pointer: currently selected menu element’s number in self.contents.

  • in_foreground : a flag which indicates if checkbox is currently displayed. If it’s not active, inhibits any of menu’s actions which can interfere with other menu or UI element being displayed.

__init__(*args, **kwargs)[source]

Args:

  • contents: a list of element descriptions, which can be constructed as described in the Checkbox object’s docstring.
  • i, o: input&output device objects

Kwargs:

  • name: Checkbox name which can be used internally and for debugging.
  • entry_height: number of display rows that one checkbox element occupies.
  • default_state: default state for each entry that doesn’t have a state (entry[2]) specified in contents (default: False)
  • final_button_name: label for the last button that confirms the selection (default: "Accept")
activate()

A method which is called when UI element needs to start operating. Is blocking, sets up input&output devices, renders the UI element and waits until self.in_foreground is False, while UI element callbacks are executed from the input listener thread.

deactivate()

Sets a flag that signals the UI element’s activate() to return.

print_contents()

A debug method. Useful for hooking up to an input event so that you can see the representation of current UI element’s contents.

print_name()

A debug method. Useful for hooking up to an input event so that you can see which UI element is currently processing input events.

set_contents(contents)

Sets the UI element contents and triggers pointer recalculation in the view.

Numeric input UI elements

from ui import IntegerAdjustInput
start_from = 0
number = IntegerAdjustInput(start_from, i, o).activate()
if number is None: #Input cancelled
    return
#process the number
class ui.number_input.IntegerAdjustInput(number, i, o, message='Pick a number:', interval=1, name='IntegerAdjustInput', mode='normal')[source]

Implements a simple number input dialog which allows you to increment/decrement a number using which can be used to navigate through your application, output a list of values or select actions to perform. Is one of the most used elements, used both in system core and in most of the applications.

Attributes:

  • number: The number being changed.
  • initial_number: The number sent to the constructor. Used by reset() method.
  • selected_number: A flag variable to be returned by activate().
  • in_foreground : a flag which indicates if UI element is currently displayed. If it’s not active, inhibits any of element’s actions which can interfere with other UI element being displayed.
__init__(number, i, o, message='Pick a number:', interval=1, name='IntegerAdjustInput', mode='normal')[source]

Initialises the IntegerAdjustInput object.

Args:

  • number: number to be operated on
  • i, o: input&output device objects

Kwargs:

  • message: Message to be shown on the first line of the screen when UI element is active.
  • interval: Value by which the number is incremented and decremented.
  • name: UI element name which can be used internally and for debugging.
  • mode: Number display mode, either “normal” (default) or “hex” (“float” will be supported eventually)
activate()[source]

A method which is called when input element needs to start operating. Is blocking, sets up input&output devices, renders the UI element and waits until self.in_background is False, while callbacks are executed from the input device thread. This method returns the selected number if KEY_ENTER was pressed, thus accepting the selection. This method returns None when the UI element was exited by KEY_LEFT and thus it’s assumed changes to the number were not accepted.

deactivate()[source]

Deactivates the UI element, exiting it and thus making activate() return.

print_number()[source]

A debug method. Useful for hooking up to an input event so that you can see current number value.

print_name()[source]

A debug method. Useful for hooking up to an input event so that you can see which UI element is currently processing input events.

decrement(*args, **kwargs)[source]

Decrements the number by selected interval

increment(*args, **kwargs)[source]

Increments the number by selected interval

reset(*args, **kwargs)[source]

Resets the number, setting it to the number passed to the constructor.

select_number(*args, **kwargs)[source]

Selects the currently active number value, making activate() return it.

Character input UI elements

from ui import CharArrowKeysInput
password = CharArrowKeysInput(i, o, message="Password:", name="My password dialog").activate()
if password is None: #UI element exited
    return False #Cancelling
#processing the input you received...
class ui.char_input.CharArrowKeysInput(i, o, message='Value:', value='', allowed_chars=['][S', '][c', '][C', '][s', '][n'], name='CharArrowKeysInput', initial_value='')[source]

Implements a character input dialog which allows to input a character string using arrow keys to scroll through characters

__init__(i, o, message='Value:', value='', allowed_chars=['][S', '][c', '][C', '][s', '][n'], name='CharArrowKeysInput', initial_value='')[source]

Initialises the CharArrowKeysInput object.

Args:

  • i, o: input&output device objects

Kwargs:

  • value: Value to be edited. If not set, will start with an empty string.

  • allowed_chars: Characters to be used during input. Is a list of strings designating ranges which can be the following:

    • ‘][c’ for lowercase ASCII characters
    • ‘][C’ for uppercase ASCII characters
    • ‘][s’ for special characters
    • ‘][S’ for space
    • ‘][n’ for numbers
    • ‘][h’ for hexadecimal characters (0-F)

    If a string does not designate a range of characters, it’ll be added to character map as-is.

  • message: Message to be shown in the first row of the display

  • name: UI element name which can be used internally and for debugging.

activate()[source]

A method which is called when input element needs to start operating. Is blocking, sets up input&output devices, renders the element and waits until self.in_background is False, while menu callbacks are executed from the input device thread. This method returns the selected value if KEY_ENTER was pressed, thus accepting the selection. This method returns None when the UI element was exited by KEY_LEFT and thus the value was not accepted.

deactivate()[source]

Deactivates the UI element, exiting it and thus making activate() return.

print_value()[source]

A debug method. Useful for hooking up to an input event so that you can see current value.

print_name()[source]

A debug method. Useful for hooking up to an input event so that you can see which UI element is currently processing input events.

move_up(*args, **kwargs)[source]

Changes the current character to the next character in the charmap

move_down(*args, **kwargs)[source]

Changes the current character to the previous character in the charmap

move_right(*args, **kwargs)[source]

Moves cursor to the next element.

move_left(*args, **kwargs)[source]

Moves cursor to the previous element. If first element is chosen, exits and makes the element return None.

accept_value(*args, **kwargs)[source]

Selects the currently active number value, making activate() return it.

Helpers

These are various objects and functions that help you with general-purpose tasks while building your application - for example, config management, running initialization tasks or exiting event loops on a keypress. They can help you build the logic of your application quicker, and allow to not repeat the code that was already written for other ZPUI apps.

local_path_gen helper

helpers.local_path_gen(_name_)[source]

This function generates a local_path function you can use in your scripts to get an absolute path to a file in your app’s directory. You need to pass __name__ to local_path_gen. Example usage:

from helpers import local_path_gen
local_path = local_path_gen(__name__)
...
config_path = local_path("config.json")

The resulting local_path function supports multiple arguments, passing all of them to os.path.join internally.

ExitHelper

class helpers.ExitHelper(i, keys=['KEY_LEFT'], cb=None)[source]

A simple helper for loops, to allow exiting them on pressing KEY_LEFT (or other keys).

You need to make sure that, while the loop is running, no other UI element sets its callbacks. with Printer UI elements, you can usually pass None instead of i to achieve that.

Arguments:

  • i: input device
  • keys: all the keys that should trigger an exit
  • cb: the callback that should be executed once one of the keys is pressed. By default, sets an internal flag that you can check with do_exit and do_run.
start()[source]

Clears input device keymap, registers callbacks and enables input listener.

do_exit()[source]

Returns True once exit flag has been set, False otherwise.

do_run()[source]

Returns False once exit flag has been set, True otherwise.

reset()[source]

Clears the exit flag.

stop()[source]

Stop input listener and remove the created keymap. Shouldn’t usually be necessary, since all other UI elements are supposed to make sure their callbacks are set.

Usage:

from helpers import ExitHelper
...
eh = ExitHelper(i)
eh.start()
while eh.do_run():
    ... #do something until the user presses KEY_LEFT

There is also a shortened usage form:

...
eh = ExitHelper(i).start()
while eh.do_run():
    ... #do your thing

Oneshot helper

class helpers.Oneshot(func, *args, **kwargs)[source]

Oneshot runner for callables. Each instance of Oneshot will only run once, unless reset. You can query on whether the runner has finished, and whether it’s still running.

Args:

  • func: callable to be run
  • *args: positional arguments for the callable
  • **kwargs: keyword arguments for the callable
run()[source]

Run the callable. Sets the running and finished attributes as the function progresses. This function doesn’t handle exceptions. Passes the return value through.

reset()[source]

Resets all flags, allowing the callable to be run once again. Will raise an Exception if the callable is still running.

running

Shows whether the callable is still running after it has been launched (assuming it has been launched).

finished

Shows whether the callable has finished running after it has been launched (assuming it has been launched).

Usage:

from helpers import Oneshot
...
def init_hardware():
    #can only be run once

#since oneshot is only defined once, init_hardware function will only be run once,
#unless oneshot is reset.
oneshot = Oneshot(init_hardware)

def callback():
    oneshot.run() #something that you can't or don't want to init in init_app
    ... #do whatever you want to do

BackgroundRunner helper

class helpers.BackgroundRunner(func, *args, **kwargs)[source]

Background runner for callables. Once launched, it’ll run in background until it’s done.. You can query on whether the runner has finished, and whether it’s still running.

Args:

  • func: function to be run
  • *args: positional arguments for the function
  • **kwargs: keyword arguments for the function
running

Shows whether the callable is still running after it has been launched (assuming it has been launched).

finished

Shows whether the callable has finished running after it has been launched (assuming it has been launched).

failed

Shows whether the callable has thrown an exception during execution (assuming it has been launched). The exception info will be stored in self.exc_info.

threaded_runner(print_exc=True)[source]

Actually runs the callable. Sets the running and finished attributes as the callable progresses. This method catches exceptions, stores sys.exc_info in self.exc_info, unsets self.running and re-raises the exception. Function’s return value is stored as self.return_value.

Not to be called directly!

run(daemonize=True)[source]

Starts a thread that will run the callable.

reset()[source]

Resets all flags, restoring a clean state of the runner.

Usage:

from helpers import BackgroundRunner
...
def init_hardware():
    #takes a long time

init = BackgroundRunner(init_hardware)

def init_app(i, o):
    ...
    init.run() #something too long that just has to run in the background,
    #so that app is loaded quickly, but still can be initialized.

def callback():
    if init.running: #still hasn't finished
        PrettyPrinter("Still initializing...", i, o)
        return
    elif init.failed: #finished but threw an exception
        PrettyPrinter("Hardware initialization failed!", i, o)
        return
    ... #everything initialized, can proceed safely

Combining BackgroundRunner and Oneshot

from helpers import BackgroundRunner, Oneshot
...
def init_hardware():
    #takes a long time, *and* can only be run once

init = BackgroundRunner(Oneshot(init_hardware).run)

def init_app(i, o):
    #for some reason, you can't put the initialization here
    #maybe that'll lock the device and you want to make sure
    #that other apps can use this until your app started to use it.

def callback():
    init.run()
    #BackgroundRunner might have already ran
    #but Oneshot inside won't run more than once
    if init.running: #still hasn't finished
        PrettyPrinter("Still initializing, please wait...", i, o)
        eh = ExitHelper(i).start()
        while eh.do_run() and init.running:
            sleep(0.1)
        if eh.do_exit(): return #User left impatiently before init has finished
        #Even if the user has left, the hardware_init will continue running
    elif init.failed: #finished but threw an exception
        PrettyPrinter("Hardware initialization failed!", i, o)
        return
    ... #everything initialized, can proceed safely

Hacking on UI

If you want to change the way ZPUI looks and behaves for you, make a better UI for your application by using more graphics or even design your own UI elements, these directions will help you on your way.

Using the ZPUI emulator

ZPUI has an emulator that will allow you to test your applications, UI tweaks and ZPUI logic changes, so that you don’t have to have a ZeroPhone to develop and test your UI.

It will require a Linux computer with a graphical interface running (X forwarding might work, too) and Python 2.7 available. Here are the setup and usage instructions.

Tweaking how the UI looks

ZPUI allows you to modify the way UI looks. The main way is tweaking UI element “views” ( a view object defines the way an UI element is displayed ). So, you can change the look of a certain UI element (say, main ZPUI menu), or a group of elements (like, force a certain view for all checkboxes). You can also define your own views, then apply them to UI elements using the same method. To know more about it, read here.

If your needs aren’t covered by this, feel free to modify the ZPUI code - it strives to be straightforward, and the parts that aren’t are either covered with comments and documentation, or will be covered upon request. If you need assistance, contact us on IRC or email!

Note

If you decide to modify the ZPUI code, here’s a starting point. Also, please open an issue on GitHub describing your changes - we can include it as a feature in the next versions of ZPUI!

Warning

Modifying ZPUI code directly might result in merge conflicts if you will update using git pull, or the built-in “Update ZPUI” app. Again, please do consider opening an issue on GitHub proposing your changes to be included in the mainline =)

Making and modifying UI elements

If existing UI elements do not cover your usecase, you can also make your own UI elements! Contact us to find out how, or just use the code for existing UI elements as guidelines if you feel confident.

Also, check if the UI element you want is mentioned in ZPUI TODO and ZPUI GH issues- there might already be progress on that front, or you might find some useful guidelines.

Testing the UI

There are two ways to test UI elements:

1. Running existing tests

There’s a small amount of tests, they’re being added when bugs are found, sometimes also when features are added. From ui/tests folder, run existing tests like:

python -m unittest TEST_FILENAME (without .py at the end)

For example, try:

python -m unittest test_checkbox

2. Running example applications

There are example applications available for you to play with UI elements. You can run ZPUI in single-app mode to try out any UI element before using it:

python main.py -a apps/example_apps/checkbox_test

You can also, of course, use the code from example apps as a reference when developing your own applications.

Contributing your changes

Send us a pull request! If your changes affect the UI element logic, please try and make a test that checks whether it really works. If you’re adding a new UI element, add docstrings to it - describing purpose, args and kwargs, as well as an example application to go with it.

Logging configuration

Changing log levels

In case of problems with ZPUI, logs can help you understand it - especially when the problem is not easily repeatable. To enable verbose logging for a particular system/app/driver, go to "Settings"->"Logging settings" menu, then click on the part of ZPUI that you’re interested in and pick “Debug”. From now on, that part of ZPUI will log a lot more in zpui.log files - which you can then read through, or send to the developers.

Alternatively, you can change the log_conf.ini file directly. In it, add a new section for the app you want to learn, like this:

[path.to.code.file]
level = debug

path.to.code.file would be the Python-style path to the module you want to debug, for example, input.input, context_manager or apps.network_apps.wpa_cli.

Managing and developing applications

General information

  • Applications are simply folders which are made importable by Python by adding an __init__.py file. ZPUI loads main.py file residing in that folder.
  • You can combine UI elements in many different ways, including making nested menus, which makes apps less cluttered.
  • ZPUI main menu can have submenus. Submenu is just a folder which has __init__.py file in it, but doesn’t have a main.py file. It can store both application folders and child submenu folders.
    • To set a main menu name for your submenu, you need to add _menu_name = "Pretty name" in __init__.py file of a submenu.
    • Submenus can be nested - just create another folder inside a submenu folder. However, submenu inside an application folder won’t be detected.
  • All application modules are loading when ZPUI loads. When choosing an application in the main menu/submenu, its global callback or ZeroApp.on_load() is called. It’s usually set as the activate() method of application’s main UI element, such as a menu.
  • You can prevent any application from autoloading (but still have an option to load it manually) by placing a do_not_load file (with any contents) in application’s folder (for example, see skeleton application folder).

Getting Started

ZPUI enables two way of developping apps. One is function-based, the other one is class-based.

Function-based

Function-based apps need two functions to work : init_app and callback.

  • init_app(i, o) is called when the app is loaded. That is, when the UI boots. Avoid doing any heavy work here, it would slow down everything, and there is no guarantee the app is going to be activated at this point. You may want to keep a reference to the two parameters for later usage. See below.
  • callback() is called when the app is actually opened and brought to foreground. This is where most of your code should belong.
  • menu_name is a global variable that can be set to define the name of the application shown in the main menu. If not provided, it will fall back to the name of the parent directory.
  • global i, o are global variables commonly used to keep a reference to the input and output devices passed in the init function.

Usage example : skeleton_app

Class-based

Class-based apps need a single class inheriting from ZeroApp to work.

  • __init__(self, i, o) is called when the app is loaded. That is, when the UI boots. Avoid doing any heavy work here, it would slow down everything, and there is no guarantee the app is going to be activated at this point. You need to call the base class constructor to keep a reference to the input and output devices (self.i, self.o).
  • on_load(self) is called when the app is actually opened and brought to foreground. This is where most of your code should belong.
  • menu_name is a member variable that can be set to define the name of the application shown in the main menu. If not provided, it will fall back to the name of the parent directory.

You can see class skeleton app for an example.

Development tips

  • For starters, take a look at the skeleton app and class skeleton app
  • You can launch ZPUI in a “single application mode” using main.py -a apps/app_folder_path. There’ll be no main menu constructed, and exiting the application exits ZPUI.
  • You should not set input callbacks or output to screen while your application is not the one active. It’ll cause screen contents set from another application to be overwritten, which is bad user experience. Make sure your application is the one currently active before outputting things and setting callbacks.

Working on this documentation

If you want to help the project by working on documentation, this is the tutorial on how to start!

Pre-requisites

  • Fork the ZPUI repository on GitHub

  • Create a separate branch for your documentation needs

  • Install the necessary Python packages for testing the documentation locally:

    pip install sphinx sphinx-autobuild sphinx-rtd-theme

Find a task to work on

  • Look into ZPUI GitHub issues and see if there are issues concerning documentation
  • Unleash your inner perfectionist
  • If you’re not intimately familiar with reStructuredText markup, feel free to look through the existing documentation to see syntax and solutions that are already used.

Testing your changes locally

You can build the documentation using make html from the docs/ folder. Then, you can run ./run_server.py to run a HTTP server on localhost, serving the documentation on port 8000. If you make changes to the documentation, just run make html again to rebuild the documentation - webserver will serve the updated documentation once it finishes building.

Contributing your changes

Send us a pull request!

Contact us

ZPUI development discussions happen on IRC, #ZeroPhone on freenode. If you have found a problem with ZPUI, want to suggest something or found that something isn’t documented well, please open an issue on GitHub. You can also email the main developer if you would like personal assistance.