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.
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!")
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
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
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 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)
-
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
.
-
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
.
-
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 imageinvert
: 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 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 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.
-
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 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``.
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 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 oni
,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.
-
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.
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 displayname
: 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.
-
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
-
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 exitcb
: 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
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.
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.
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.