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.
Guides:¶
References:¶
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¶
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
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:
- 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.
- Unplug the MicroSD card and plug it into another computer - both Windows and Linux will work
- On the first partition (the boot partition), locate the
zpui_config.json
file - In that file, add an
"app_manager"
dictionary (a “collection” in JSON terms) - 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.
Crash course¶
This is a crash course on writing apps for ZPUI, with links to more in-depth explanations.
Basics¶
- What does a “do nothing” ZPUI app need? One directory with two files and 5 lines of code
- What does a “Hello, world” app need? One more line of code.
- Can you do an app as an object? Sure, here’s how.
- Want to experiment with the code using REPL? Use the sandbox.
Showing things on the screen¶
- Want to display some text real quick? Use display_data.
- Want to display some text in a more user-friendly fashion, with UX bells&whistles? Use PrettyPrinter.
- Want to display an image? Use display_image.
- Want to display an image in a more user-friendly fashion, with UX bells&whistles? Use GraphicsPrinter.
- Want to construct an image dynamically? Use Canvas.
Interactivity¶
- Want some very basic interactivity? Setup some input callbacks and start an input thread.
- How do callbacks work, what’s a keymap and how do you set one? Read here.
- Want to make a very basic loop and allow the user to interrupt it? Use ExitHelper.
- Want to make a menu for your application? Use a Menu.
- Want to make a “pick any items out of many and accept” choice? Use a Checkbox.
- Want to make a “pick one out of many” choice? Use a Listbox.
- Want to make a “Yes”/”No”[/”Cancel”] choice? Use DialogBox.
- Want to make a status screen? Use a Refresher.
- Want to input some text? Use UniversalInput.
- Want to adjust a number? Use IntegerAdjustInput.
- Want to pick a directory/file? Use PathPicker.
- Want to pick a date from the calendar? Use DatePicker.
- Want to pick a time? Use TimePicker.
- Want to show a lot of text on the screen? Use TextReader.
- Want to indicate that some task is in progress? Use LoadingBar.
- Want to indicate a task is in progress, with a progress estimate? Use ProgressBar.
- Want to make an UI element react to more buttons? Here’s how you do that.
App internals¶
- Want to add logging? It’s very easy - here’s a snippet for adding logging.
- Want to include some resource files with your app - i.e. sounds? Here’s how to access them the proper way.
- Want to have a place to store variables for your app? Here’s a snippet to use a config file.
- Want to learn about things you should do while writing an app? We have some guidelines here and here.
- Want to learn about things you should not do while writing an app? Here are some more examples.
- Want to run things on app startup/launch/in the background? Here are the basics of 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"
# These two variables will be automatically assigned by init_app
# unless you define your own init_app. However, you do need to define them
# - for now, just for readability.
i = None #Input device
o = None #Output device
def callback():
#Gets called when app is selected from menu
pass
app/__init__.py
has to be an empty file:
“Hello, world!”¶
In app/main.py
:
menu_name = "Hello, world!"
# An UI element that does most of the legwork for us
from ui import PrettyPrinter as Printer
i = None #Input device
o = None #Output device
def callback():
# will show text on screen for 3 seconds and then exit
Printer("Hello, world!", i, o, 3)
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!")
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}
Indicate progress¶
If you’re going to launch a background task, it’s best if the user knows what’s happening. The simplest way is to print something on the screen:
from ui import PrettyPrinter
...
PrettyPrinter("Scanning ports", i, o, 5)
results = scan_ports()
print_results(results)
Or, a little bit prettier:
from ui import Canvas
...
c = Canvas(o)
c.centered_text("Scanning ports")
c.display()
results = scan_ports()
print_results(results)
Or, even better - use a LoadingIndicator UI element, which is much prettier and user-friendly:
from ui import LoadingIndicator
...
with LoadingIndicator(i, o, message="Scanning ports"):
results = scan_ports()
print_results(results)
What if you actually know how much of the task is completed? Then, you can use a ProgressBar, which is going to show the user a percentage of the task completed:
from ui import ProgressBar
...
ports = [22, 23, 80, 111, 443]
with ProgressBar(i, o, message="Scanning ports") as pb:
process = PortScanner(ports)
process.start()
while process.is_ongoing():
current_port_index = ports.index(process.current_port)
# Calculating progress from 0 to 100
progress = int( 100.0/len(ports) * current_port_index )
pb.progress = progress
print_results(results)
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
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()
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()
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()
Draw a line¶
c = Canvas(o)
c.line((10, 4, "-8", "-4")) # Draws a line from top left to bottom right corner
c.display()
Draw a rectangle¶
c = Canvas(o)
c.rectangle((10, 4, 20, "-10")) # Draws a rectangle in the left of the screen
c.display()
Draw a circle¶
c = Canvas(o)
c.circle(("-8", 8, 4)) # Draws a circle in the top left corner - with radius 4
c.display()
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()
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
Use failsafe item fetching¶
When working with dictionaries, it’s tempting to write straightforward
code that uses straightforward item fetching, like my_dict["key1"]
.
However, in certain cases this might fail - the most obvious one is, what if the dictionary
is outside of your control and you end up with a dict that doesn’t have the “key1” key?
It will throw KeyError
, of course, stopping your code from reaching the goal.
Here’s how to fetch items from untrusted dictionaries:
value = my_dict.get("key1", "default_value")
Of course, it’s not needed everywhere, but it does make sense to do it when, say, working with user input, data generated by other software out of your control, and even config files (they’re there to be changed, which means they will be broken at some point). To sum up, this is a great trick for foolproofing your app.
Config (and other) files¶
Read JSON from a config file located in the app directory¶
You’ll want to configure your application from time to time - typically, to allow users to change your app’s configuration, but it’s also useful for storing user-specific data, allow other software to change your app’s configuration, or simply a way to hide all those magic numbers in your code out of plain sight.
JSON dictionaries are a good fit in that they convert to Python objects pretty easily - you can store strings, numbers, dictionaries and lists. A suggested config file for an app would be a dictionary (an “object” in JSON terms), here’s an example of how that could look like for a music player app, one to needs to store a few settings that were set by the user:
{
"shuffle":true,
"repeat":true,
"last_directory":"/home/pi/music",
"disabled_plugins":["lyrics", "thumbnails"]
}
Here’s the simplest way to read data from a config file located in an app’s 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))
Do you have more requirements for your config file = like, easily saving it, restoring it on failure, as well as some primitive migrations as you update your app? The next example will probably work for your needs.
Read a config file with “restore to defaults on error”, migrations and save_config() method¶
There’s, however, a way to work with config files that you’re the most likely to use.
It allows you to read an app-specific config, restore it to defaults if the reading/parsing
fails for some reason and get a convenient save_config()
method to save it.
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.
In addition to that, if the highest level of your config is a dictionary, it allows you to perform small migrations - specifically, auto-adding new keys with default values to the config as your app is updated to rely on those.
Say, here’s a config you have, created from the default config and then changed by the user:
{
"your":"non-default",
"config":"to_use"
}
And here’s a new default config, with additional "but_now"
key that you roll out through
an app upgrade:
default_config = '{"your":"default", "config":"to_use", "but_now":"its_updated"}'
The resulting config received from read_or_create_config
will look like this:
{
"your":"non-default",
"config":"to_use",
"but_now":"its_updated"
}
“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(self, 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(input, output):
# if we define our own init_app, we need to do this
global i, o
i = input; o = output
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.
Readability¶
When writing a ZPUI app, keep in mind that other people might refer to it afterwards, trying to understand how it works (possibly, also debugging).
How to arrange imports¶
One step towards readability is rearranging your import statements. Here’s something you might start with:
from ui import GraphicsPrinter # ZPUI libraries
import json # built-in library
import smbus # external library, needs to be installed
...
ZPUI-proposed way to arrange imports is:
- Built-in libraries
- ZPUI libraries
- External libraries (that you need to install from pip/apt)
- Local imports (something in the same folder as your
main.py
It’s best if you separate these groups with a single empty line. This is especially helpful once your app grows big. Here’s an example of the end result:
import json # built-in
from ui import GraphicsPrinter # ZPUI
import smbus # external
import smbus_funcs # local
...
Frequent mistakes¶
Using variables named i
in a function-based app¶
If you decided to go the easy way and make a function-based app, do keep in mind
that they require global variables named i
and o
. Therefore, if you use
constructs like this in a function:
for i in range(8):
print(i) # do stuff
the local i
will overwrite the global i
variable locally. So, this code:
for i in range(8):
print(i)
Printer("Done!", i, o) # this will fail
will fail. Solutions? Don’t use i
as a local name in the same function where you’ll
need to access the global i
. Also, class-based apps won’t suffer from this (admittedly
minor) flaw.
Dynamically building lists/dictionaries with lambdas¶
If you’re dynamically building contents of a menu/listbox/whatever (for example, using
a for
loop or a list/dictionary comprehension), you will likely need to use lambdas,
like this:
interfaces = ["eth0", "wlan0", "lo0"]
# No! Bad!
menu_contents = [[if_name, lambda: show_ip(if_name)] for if_name in interfaces]
Menu(menu_contents, i, o).activate()
However, the lambdas constructed will not refer to the if_name
by value - instead,
it’s referred by its name and the value will only be resolved at runtime when the
lambda is called. So, all the show_ip
lambdas constructed will execute with
"lo0"
as their first argument (the last value that the if_name
variable
was assigned). There’s a workaround - you can create a temporary keyword argument
for the lambda with the default value of if_name
:
interfaces = ["eth0", "wlan0", "lo0"]
# The right way
menu_contents = [[if_name, lambda x=if_name: show_ip(x)] for if_name in interfaces]
Menu(menu_contents, i, o).activate()
This way, a temporary variable is created, and the if_name
variable is copied into
it by value at list generation time, so the resulting lambda will use the proper value
as the positional argument.
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:
More about UI elements:
Canvas¶
from ui import Canvas
...
c = Canvas(o)
c.text("Hello world", (10, 20))
c.display()
-
class
ui.
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 devicebase_image
: a PIL.Image to use as a base, if neededname
: a name, for internal usageinteractive
: 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 theCanvas
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, wherex*
&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, wherex1
&y1
are coordinates of the start, andx2
&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 aPIL.ImageFont
object or a tuple of(path, size)
, which are then supplied toCanvas.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)
-
vertical_text
(text, coords, **kwargs)[source]¶ Draw vertical 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 aPIL.ImageFont
object or a tuple of(path, size)
, which are then supplied toCanvas.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)
-
custom_shape_text
(text, coords_cb, **kwargs)[source]¶ Draw text on the canvas, getting the position for each character from a supplied function. Coordinates are expected in (x, y) format, where
x
&y
are coordinates of the top left corner of the character.You can pass a
font
keyword argument to it - it accepts either aPIL.ImageFont
object or a tuple of(path, size)
, which are then supplied toCanvas.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, wherex1
&y1
are coordinates of the top left corner, andx2
&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, wherexX
andyX
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, wherexc
&yc
are coordinates of the circle center andr
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, wherex1
&y1
are coordinates of the top left corner, andx2
&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_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).
-
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
.
-
centered_text
(text, cw=None, ch=None, font=None)[source]¶ Draws centered text on the canvas. This is mostly a convenience function, used in some UI elements. You can also pass alternate screen center values so that text is centered related to those, as opposed to the real screen center.
-
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, cw=None, ch=None, 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 asfont
. You can also pass alternate screen center values so that text is centered related to those, as opposed to the real screen center.
-
class
ui.
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"]
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, skippable=True)
Printer("Long lines will be autosplit", i, o, 1)
-
ui.
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 pressing ENTER.
-
ui.
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 pressing ENTER.
-
ui.
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. The GraphicsPrinter automatically uses the
fit_image_to_screen
function to make sure the image can display regardless of image or screen size.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 imageinvert
: Invert the image before displaying (True by default).
Listbox UI element¶
from ui import Listbox
...
lbc = [
["Option1", "option_1"],
["option_2"], # will be used as both name and value
]
choice = Listbox(lbc, i, o, name="My listbox of my app").activate()
if choice: # user didn't cancel and selected something
# do things
Listbox
will return the selected option’s value (element[1]
), or name
(element[0]
) if no value was passed. Otherwise, if the user exited Listbox
by pressing LEFT, returns None
.
-
class
ui.
Listbox
(*args, **kwargs)[source]¶ Bases:
ui.base_list_ui.BaseListUIElement
Implements a listbox to choose one thing from many.
Attributes:
contents
: list of listbox entries- Listbox entry is a list, where:
entry[0]
(entry’s label) is usually a string which will be displayed in the UI, such as “Option 1”. Ifentry_height
> 1, can be a list of strings, each of those strings will be shown on a separate display row.entry[1]
(entry’s value) is the value to be returned when entry is selected. If it’s not supplied, entry’s label is returned instead.
You can also pass
Entry
objects as entries -text
will be used as label andname
will be used as name.If you want to set contents after the initalisation, please, use set_contents() method.
pointer
: currently selected entry’s number inself.contents
.in_foreground
: a flag which indicates if listbox is currently displayed. If it’s not set, inhibits any of listboxes actions which can interfere with other UI element being displayed.
-
__init__
(*args, **kwargs)[source]¶ Initialises the Listbox object.
Args:
contents
: listbox contentsi
,o
: input&output device objects
Kwargs:
name
: listbox name which can be used internally and for debugging.selected
: value (that is,entry[1]
) of the element to be selected. If no element with this value is found, this is ignored.entry_height
: number of display rows one listbox entry occupies.append_exit
: appends an “Exit” entry to listbox.
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
set_contents
(contents)¶ Sets the UI element contents and triggers pointer recalculation in the view.
PathPicker UI element¶
This is an UI element that allows the app’s user to navigate the local filesystem and pick files or directories.
Picking a file:
from ui import PathPicker
...
# If initial_path is a directory, PathPicker will start in that directory
# If initial_path is a file, PathPicker will start in its base directory and move to that file
path = PathPicker(initial_path, i, o, name="My app's PathPicker for picking a file").activate()
if path: # User might exit PathPicker at any time and it will return None
purge_file_from_existence(path) # Do something with that file
Picking a directory, i.e. to read files from or to store files in:
from ui import PathPicker
...
path = PathPicker(initial_path, i, o, dirs_only=True, name="My app's PathPicker for picking a directory").activate()
if path:
purge_dir_from_existence(path)
-
class
ui.
PathPicker
(path, i, o, callback=None, name=None, file=None, display_hidden=False, dirs_only=False, append_current_dir=True, current_dot=False, prev_dot=True, scrolling=True, **kwargs)[source]¶ Bases:
ui.menu.Menu
-
__init__
(path, i, o, callback=None, name=None, file=None, display_hidden=False, dirs_only=False, append_current_dir=True, current_dot=False, prev_dot=True, scrolling=True, **kwargs)[source]¶ Initialises the PathPicker object.
Args:
path
: a path to start from. If path to a file is passed, will start from that file (unless overridden withfile
keyword argument).i
,o
: input&output device objects.
Kwargs:
callback
: if set, PathPicker will call the callback with path as first argument upon selecting path, instead of exiting the activate()file
: if set, PathPicker will locate the file in thepath
passed and move its pointer to that file (provided it is found).dirs_only
: if True, PathPicker will only show directoriesappend_current_dir
: if False, PathPicker won’t add “Dir: %/current/dir%” first entry when dirs_only is enabledcurrent_dot
: if True, PathPicker will show ‘.’ pathprev_dot
: if True, PathPicker will show ‘..’ pathdisplay_hidden
: if True, PathPicker will display hidden files
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
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
(*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 toCheckbox
constructor or tocheckbox.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``.
You can also pass
Entry
objects as entries -text
will be used as label andname
will be used as name.If you want to set contents after the initalisation, please, use set_contents() method.
pointer
: currently selected menu element’s number inself.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 incontents
(default:False
)final_button_name
: label for the last button that confirms the selection (default:"Accept"
)
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
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 active.
-
set_contents
(contents)¶ Sets the UI element contents and triggers pointer recalculation in the view.
DialogBox UI element¶
This UI element allows you to make sure the user actually wants to proceed with some kind of action/decision.
from ui import DialogBox
...
choice = DialogBox("ync", i, o, name="My dialogbox of my app").activate()
if choice: "Yes" was selected
# do things
By default, you can pass string values like “ync” (or “yn”, or “yc”, or “cy”), where
the “y”, “n” and “c” characters will be parsed as “Yes” (True
), “No” (False
)
and “Cancel” (None
) options respectively (True
, False
and None
being
return values). Exiting by using LEFT will also result in None
being returned.
You can also pass custom labels/return values like this:
choice = DialogBox([["Abort", "abort"], ["Retry", "retry"], ["Ignore", "ignore"]], i, o, name="My dialogbox of my app").activate()
-
class
ui.
DialogBox
(values, i, o, message='Are you sure?', name='DialogBox', **kwargs)[source]¶ Bases:
ui.base_ui.BaseUIElement
Implements a dialog box with given values (or some default ones if chosen).
-
__init__
(values, i, o, message='Are you sure?', name='DialogBox', **kwargs)[source]¶ Initialises the DialogBox object.
Args:
values
: values to be used. Should be a list of[label, returned_value]
pairs.- You can also pass a string “yn” to get “Yes(True), No(False)” options, or “ync” to get “Yes(True), No(False), Cancel(None)” options.
- Values put together with spaces between them shouldn’t be longer than the screen’s width.
i
,o
: input&output device objects
Kwargs:
message
: Message to be shown on the first line of the screen when UI element is activatedname
: UI element name which can be used internally and for debugging.
-
set_start_option
(option_number)[source]¶ Allows you to set position of the option that’ll be selected upon DialogBox activation.
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
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
(refresh_function, i, o, refresh_interval=1, keymap=None, name='Refresher', **kwargs)[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', **kwargs)[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 byscreen.display_data()
orscreen.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()
- Tuples and lists - are converted to lists and passed to
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 want to override it, make sure that user can still exit the Refresher.name
: Refresher name which can be used internally and for debugging.
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
print_name
()¶ A debug method. Useful for hooking up to an input event so that you can see which UI element is currently active.
-
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.
IntegerAdjustInput
(number, i, o, message='Pick a number:', interval=1, name='IntegerAdjustInput', mode='normal', max=None, min=None)[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', max=None, min=None)[source]¶ Initialises the IntegerAdjustInput object.
Args:
number
: number to be operated oni
,o
: input&output device objects
Kwargs:
message
: Message to be shown on the first line of the screen while 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)min
: minimum value, will not go lower than that.max
: maximum value, will not go higher than that.
-
print_number
()[source]¶ A debug method. Useful for hooking up to an input event so that you can see current number value.
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
print_name
()¶ A debug method. Useful for hooking up to an input event so that you can see which UI element is currently active.
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.
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 displayname
: UI element name which can be used internally and for debugging.
-
activate
()¶ A method which is called when the UI element needs to start operating. Is blocking, sets up input&output devices, refreshes the UI element, then calls the
idle_loop
method while the UI element is active.self.in_foreground
is True, while callbacks are executed from the input device thread.
-
deactivate
()¶ Deactivates the UI element, exiting it.
-
UI element utilities¶
-
ui.utils.
fit_image_to_screen
(image, o)[source]¶ Fits a given image to fit on any sized screen whilst maintaining the aspect ratio. Any remaining space is filled with borders. The resized image is returned as
image
.Args:
image
: A PIL image to be resized.o
: output device object. Used to find the width and height of the screen.
UI element internals¶
What happens when you call, say, menu.activate()
?
In other words, activate()
blocks execution of your code until the user exits the UI element
(or it exits through other means, say, by calling deactivate()
from another thread).
When writing child UI elements based on BaseUIElement
:
- There are hooks that you can use to execute things at various stages of UI element’s
activate()
- namely,before_foreground()
,before_activate()
andafter_activate()
- When overriding, don’t forget to execute the original hook, too - we might put stuff there in the future, and there will definitely be useful code there if you inherit from a child UI element - i.e
Menu
usesbefore_foreground()
for “contents hook” functionality (dynamic fetching ofcontents
).- The difference between
before_activate()
andbefore_foreground()
is thatbefore_activate()
will only be called once - duringactivate()
.to_foreground()
, however, can be called multiple times - once duringactivate()
, but also every time when the UI element goes back to foreground (i.e. parent menu going back to foreground after a child menu finished executing). Consequently,before_foreground
will be called each timeto_foreground
is called, that is, one or more times.- You can also override functions like
idle_loop
(used inRefresher
and tests), in that case, make sure to call the original function too (and read its code to check for side effects).
How does the input processing work?
Notes:
- Simple example - all the move_up/move_down calls in i.e. a Menu are actually executed from a different thread - not the one where
activate()
runs, but a thread that was launched byactivate()
.- The input thread will only check the exit flag between callback invocations. So, when you signal the input thread to exit (by using stop_listen), it will not exit while it’s still in the middle of processing a callback - only after it’s finished processing 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__
tolocal_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 devicekeys
: all the keys that should trigger an exit. You can also pass “*” so that it catches all of the keys (allowing you to make an “exit on any key” action).cb
: the callback that should be executed once one of the keys is pressed. By default, sets an internal flag that you can check withdo_exit
anddo_run
.
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
andfinished
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
andfinished
attributes as the callable progresses. This method catches exceptions, storessys.exc_info
inself.exc_info
, unsetsself.running
and re-raises the exception. Function’s return value is stored asself.return_value
.Not to be called directly!
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
Keymaps¶
What’s a keymap?¶
Keymap is a mapping between keys pressed by the user and functions (callbacks) that are called by the input processor. For example, if you have a currently active UI element el
that, in its keymap, maps a KEY_LEFT
key to its deactivate
method, el.deactivate
will be called by the input processor (from a separate thread) when user presses KEY_LEFT
.
Key names¶
Key names are strings that start with "KEY_"
. They’re modelled after the Linux input keycode names, but might be different where developer-friendliness is concerned. For more info about ZeroPhone key names, see this Wiki page. There are 5 basic keys that you can expect to be available on any platform (including ZeroPhone) - "KEY_DOWN"
, "KEY_UP"
, "KEY_LEFT"
, "KEY_RIGHT"
and "KEY_ENTER"
.
If you’re writing your own UI elements or apps that define their own callbacks, it’s best if you can map all the main functions onto these 5 keys - though there’s nothing wrong if you’re making an app that’s fundamentally ZeroPhone-tailored, i.e. implementing a calculator with only arrow keys would be tricky.
Typical keymaps¶
Keymaps, fundamentally, are dictionaries where keys are key names and values are callbacks. UI elements and apps need to take care to ensure that, when they’re active, the correct keymap is set, while not interfering with other UI elements that might use the same input proxy object.
For example, imagine a (parent) menu that links to a child menu. When the parent menu calls the child menu, child menu sets its own keymap - and parent menu needs to not change the keymap until the child menu exits. Once the child menu exits, however, the parent menu needs to set its keymap again (as the input proxy’s keymap was previously set to the child menu’s keymap).
Thankfully, all this is taken care of when you’re using stock UI elements - you only need to worry about this if you manually use the i.set_callback
, i.set_keymap
and other functions. This is mostly described so that you have insight into how ZPUI input processing works.
Key states¶
By default, callbacks are only called when key is pressed, as that covers the majority of usecase and is an intuitive choice. However, you can also make your callbacks receive key events by using a decorator:
from helpers import cb_needs_key_state, KEY_HELD # Also has KEY_PRESSED and KEY_RELEASED
@cb_needs_key_state
def up_held_cb(state):
if state == KEY_HELD:
print("UP key held!")
i.set_callback("KEY_UP", up_held_cb)
Of course, for now, this does imply that you need to set a single callback for the same key, even if you need to process different states.
Streaming callbacks also receive the key name:
from helpers import cb_needs_key_state, KEY_PRESSED, KEY_RELEASED, KEY_HELD
@cb_needs_key_state
def state_cb(key, state):
state_name = {KEY_PRESSED:"down", KEY_HELD:"hold", KEY_RELEASED:"up", None:"down"}[state]
print("{} - {}".format(key, state_name))
i.set_streaming(key_state_cb)
Note
The function will be modified in-place. If you need the cb_needs_key_state
to return a new function instead of modifying the existing one, call it with
new_function=True
.
Warning
Not all drivers support key state (though it will likely be a matter of time
and requests to add it to drivers), in that case, you will get None
.
Also, not all drivers support “key held” state - but the default ZP keypad
driver and the HID driver do.
Changing UI elements’ keymaps in your own apps¶
Most UI elements (specifically, BaseUIElement-based-ones) allow you to add and override keymap entries, both for external and internal functions. Let’s take the keymap of an IntegerAdjustElement, for instance:
def generate_keymap(self):
return {
"KEY_RIGHT":'reset',
"KEY_UP":'increment',
"KEY_DOWN":'decrement',
"KEY_F3":lambda: self.increment(multiplier=10),
"KEY_F4":lambda: self.decrement(multiplier=10),
"KEY_ENTER":'select_number',
"KEY_LEFT":'exit'
}
You will notice that some elements in the keymap are strings, and some are functions. The main difference is - the string callbacks refer to the internal methods of the UI element itself, i.e. "KEY_LEFT":"deactivate"
for an IntegerAdjustElement
named ia
means that, once you press KEY_LEFT
, ia.deactivate
will be called. This allows to define keymap callbacks in a more straightforward way, both when writing an UI element and when remapping its callbacks. In addition to that, when string callbacks are used, the UI element will not go into background while processing it (so, any redraws will still happen).
In comparison, function callbacks will be 1) executed directly (with no positional/keyword arguments supplied) 2) will suspend the UI element into background during execution (so, redraws will not happen if UI element’s refresh() is wrapped into to_be_foreground
).
How can you use this?¶
First of all, when instantiating an UI element, you can replace some of the callbacks by using a keymap={}
init argument, For example, if you create an IntegerAdjustElement
object like this: IntegerAdjustElement(0, i, o, name="...", ..., keymap={"KEY_F1":your_function})
, once it’s active, your_function will be called when the user presses KEY_F1
(and the UI element will go into background, so you can set your own callbacks and draw on the screen all you want). This way, you can create all kinds of context menus. If there’s an existing callback set on a key you want to use, it will be replaced.
Then, you can also remap internal methods of the UI element. For example, if you want to flip IntegerAdjustElement
’s up/down key actions, you can initialize it like this: IntegerAdjustElement(0, i, o, name="...", ..., keymap={"KEY_UP":"decrement", "KEY_DOWN":"increment"})
. This way, when the user presses UP, the number will decrement instead of incrementing, and vice-versa.
Warning
Keep in mind that KEY_LEFT
is a special key, as it’s the default “go back” key and UI elements are built in a way that enforces this guideline. If KEY_LEFT is present in an external keymap for UI elements like Refresher
and Menu
(and derivative UI elements), it will be replaced with the default "deactivate"
callback. To avoid that, you should set the override_left
keyword argument to False
when instantiating the UI element.
Shortcuts¶
Do you always need to use the keymap=
replacements? No, there’s often a better way.
- If you need to add a “F1 and F2 buttons do something” function to an UI element, use the
FunctionKeyOverlay
- it will also show button labels on the screen.- If you need to add a “help is shown on F5” function to an UI element, use the
HelpOverlay
- it will also show a small “H” icon in the top left, which is something users can recognize as a “help available” marker.
Remapping keys globally¶
It’s possible to remap keys from your input devices, i.e. if your keyboard sends KEY_KPENTER
and you want the UI elements to receive KEY_ENTER
. For that, you will want to edit ZPUI’s config.json
file as follows:
{
"input":
[
{
"driver":"custom_i2c",
"kwargs":
{
"name_mapping": {"KEY_KPENTER":"KEY_ENTER"}
}
}
],
...
}
Note
Keep in mind that many drivers already have their own (override-able) replacement rules. I.e. the KEY_KPENTER=>KEY_ENTER
rule is already hardcoded into the HID and pygame (emulator) drivers.
Warning
Usual config.json editing rules apply - if you’re changing the config file for a ZeroPhone, it’s best if you edit /boot/zpui_config.json
, as if you make a syntax mistake, a failsafe config file will be used.
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.
Attaching to the ZPUI instance¶
In case where you need to debug some hiccup or simulate some condition in the
framework itself, you can attach to the ZPUI instance and debug the framework’s
behaviour. This will not allow you to debug individual apps, however,
if you need to attach to an app, you can modify its code in the same way that ZPUI
main.py
itself was modified.
First, you need to install python-rfoo
from Debian repositories, if it’s not
installed already. ZeroPhone SD card images will have it installed by default,
otherwise, here’s how you do it:
sudo apt install python-rfoo
If you don’t yet have it installed, it can be done any time before actually launching the debug console. Then, find PID of the current ZPUI instance:
ps ax|grep python
You might have multiple entries, one of them will look like “python main.py”, get the PID from it. PID is the process number, which in our case is likely going to be from 3 to 5 digits long. Once you have the PID number, signal ZPUI to launch the console:
kill -USR2 74856
(where74856
is your PID number)
Then, launch a Python interpreter and run the following commands:
from rfoo.utils import rconsole
rconsole.interact(port=9377)
pi@zerophone-prototype:~ $ python
Python 2.7.13 (default, Sep 26 2018, 18:42:22)
[GCC 6.3.0 20170516] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from rfoo.utils import rconsole
>>> rconsole.interact(port=9377)
Python 2.7.13 (default, Sep 26 2018, 18:42:22)
[GCC 6.3.0 20170516] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(ProxyConsole)
>>>
Tab completion is supported, and don’t be afraid to use dir()
to find out more
about components.
TODO: move this into debugging.rst
, once debugging.rst
is brought up to date.
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.
Useful links¶
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 loadsmain.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 amain.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.
- To set a main menu name for your submenu, you need to add
- All application modules are loading when ZPUI loads. When choosing an application in the main menu/submenu, its global
callback
orZeroApp.on_load()
is called. It’s usually set as theactivate()
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 theinit
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. In addition to that, you can test the code
blocks for errors using docs/test.sh
- you need to have rstcheck
installed
from pip for that to work.
Contributing your changes¶
Send us a pull request!
Useful links¶
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.