Application Writer’s Guide

Introduction

Wasp-os, the Watch Application System in Python, has one pervasive goal that influences almost everything about it, from its name to its development roadmap: make writing applications easy (and fun).

Applications that can be loaded, changed, adapted and remixed by the user are what really distinguishes a smart watch from a “feature watch”[1]. In other words if we want a watch built around a tiny microcontroller to be “smart” then it has to be all about the applications.

This guide will help you get started writing applications for wasp-os. Have fun!

[1]The fixed function mobile phones that existed before iOS and Android took over the industry were retrospectively renamed “feature phones” to distinguish them from newer devices. Many of them were superficially similar to early Android devices but is was the application ecosystem that really made smart phones into what they are today.

Hello World for wasp-os

Let’s start by examining a simple “Hello, World!” application for wasp-os.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# SPDX-License-Identifier: MY-LICENSE
# Copyright (C) YEAR(S), AUTHOR

import wasp

class HelloApp():
    """A hello world application for wasp-os."""
    NAME = "Hello"

    def __init__(self, msg="Hello, world!"):
        self.msg = msg

    def foreground(self):
        self._draw()

    def _draw(self):
        draw = wasp.watch.drawable
        draw.fill()
        draw.string(self.msg, 0, 108, width=240)

Some of the key points of interest in this example application are:

  1. Applications have a NAME, which is shown in the launcher. Most applications also provide an ICON but a default will be displayed if this is omitted.
  2. This example uses __init__() to initialize the state of the application, these variables are used to remember the state of the application when when it is deactivated.
  3. foreground() is the only mandatory application entry point and it is responsible for redrawing the screen. This application does not implement background() because there is nothing for us to do!
  4. The use of _draw() is optional. We could just do the work in foreground() but this application follows a common wasp-os idiom that is normally used to pattern to distinguish a full refresh of the screen from an fast update (a redraw).

Application life-cycle

Applications in wasp-os are triggered by and do all their processing from calls their entry points. The entry points can be coarsely categorized event notifications, timer callbacks (the application tick) and system actions.

System actions control the application life-cycle and that lifecyle is shown below. The system actions are used to tell the application about any change in its lifecycle.

digraph lifecycle {
    START -> BACKGROUND [ label=" __init__()   " ];
    BACKGROUND -> START [ label=" __del__()   " ];
    BACKGROUND -> ACTIVE [ label=" foreground()   " ];
    ACTIVE -> BACKGROUND [ label=" background()   " ];
    ACTIVE -> GO_TO_CLOCK [ label=" sleep() -> False   " ];
    GO_TO_CLOCK -> BACKGROUND [ label=" background()   " ];
    ACTIVE -> SLEEPING [ label=" sleep() -> True   " ];
    SLEEPING -> ACTIVE [ label=" wake()   " ];

    START [ shape=box ];
    BACKGROUND [ shape=box, style=rounded ]
    ACTIVE [ shape=box, style=rounded ]
    SLEEPING [ shape=box, style=rounded ]
    GO_TO_CLOCK [ label="GOTO ClockApp" ];
}

When an application is initialized is enters the BACKGROUND state. A backgrounded application will not execute but it should nevertheless maintain its user visible state whilst deactivated. To conserve memory wasp-os does not permit two applications to run simultaneously but because each application remembers its state when it is not running then it will appear to the user as though all applications are running all the time.

For example, a stopwatch application should record the time that it was started and remember that start time, regardless of whether it is running or not so that when it restarted is can continue to run as the user expects.

A backgrounded application enters the ACTIVE state via a call to foreground(). When it is active the application owns the screen and must draw and maintain its user interface.

If the system manager wants to put the watch to sleep then it will tell the active application to sleep(). If the application returns True then the application will remain active whilst the watch is asleep. It will receive no events nor the application tick whilst the system is asleep and, instead, must wait for a wake() notification telling the application that the device is waking up and that it may update the screen if needed.

If an application does not support sleeping then it can simply not implement sleep() or wake(). In this case the system manager will automatically return to the default application, typically the main clock face.

Some applications may support sleeping only under certain circumstances. For example a stopwatch may choose to remain active when the watch sleeps only if the stopwatch is running. This type of application must implement sleep() and return False when it does not want to remain active when the system resumes.

Note

Most applications should not implement sleep() since it is often a better user experience for the watch to return to the default application when they complete an interaction.

API primer

This API primer introduces some of the most important and frequently used interfaces in wasp-os. For more comprehensive coverage see the Wasp-os Reference Manual which contains extensive API documentation covering the entire of wasp-os, including its drivers.

System management

The system management API provides a number of low-level calls that can register new applications and navigate between them. However most applications do not need to make these low level calls and will use a much smaller set of methods.

Applictions must call a couple of functions from their foreground() in order to register for event notifications and timer callbacks:

  • request_event() - register for UI events such as button presses and touch screen activity.
  • request_tick() - register to receive an application tick and specify the tick frequency.

Additionally if your application is a game or a similar program that should not allow the watch to go to sleep when it is running then it should arrange to call keep_awake() from the application’s tick() method.

Drawing

Most applications using the drawing toolbox, wasp.watch.drawable, in order to handle the display. Applications are permitted to directly access wasp.watch.display if they require direct pixel access or want to exploit specific features of the display hardware (inverse video, partial display, etc) but for most applications the drawing toolbox provides convenient and optimized drawing functions.

  • blit() - blit an image to the display at specified (x, y) coordinates, image type is detected automatically
  • fill() - fill a rectangle, without arguments the default is a black rectangle covering the entire screen which is useful to clear the screen prior to an update
  • string() - render a string, optionally centring it automatically
  • wrap() - automatically determine where to break a string so it can be rendered to a specified width

Most applications run some variant of the following code from their foreground() or _draw() methods in order to clear the display ready for a redraw.

draw = wasp.watch.drawable
draw.fill()
# now use draw to render the rest of the screen

Some applications customize the above code slightly if they require a custom background colour and it may even be omitted entirely if the application explicitly draws every pixel on the display.

Finally, wasp-os provides a small number of widgets that allow common fragments of logic and redrawing code to be shared between applications:

MicroPython

Many of the features of wasp-os are inherited directly from MicroPython. It is useful to have a basic understanding of MicroPython and, in particular, put a little time into learning the best practices when running MicroPython on microcontrollers.

How to run your application

Testing on the simulator

wasp-os includes a simulator that can be used to test applications before downloading them to the device. The simulator is useful for ensuring the code is syntactically correct and that there are not major runtime problems such as misspelt symbol names.

Note

The simulator does not model the RAM or code size limits of the real device. It may still be necessary to tune the application for minimal footprint after testing on the simulator.

To launch the simulator:

sh$ make sim
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=.:wasp/boards/simulator:wasp \\
python3 -i wasp/main.py
MOTOR: set on
BACKLIGHT: 2
Watch is running, use Ctrl-C to stop

From the simulator console we can register the application with the following commands:

1
2
3
4
5
6
7
8
 ^C
 Traceback (most recent call last):
 ...
 KeyboardInterrupt
 >>> from myapp import MyApp
 >>> wasp.system.register(MyApp())
 >>> wasp.system.run()
 Watch is running, use Ctrl-C to stop

When an application is registered it does not start automatically but it will have been added to the launcher and you will be able to select in the simulator by swiping or using the Arrow keys to bring up the launcher and then clicking on your application.

The application can also be registered automatically when you load the simulator if you add it to wasp/main.py. Try adding lines 5 and 6 from the above example into this file (between import wasp and wasp.system.run()).

The simulator accepts gestures such as up/down and left/right swipes but the simulator also accepts keystrokes for convenience. The arrow keys simulate swipes and the Tab key simulates the physical button, whilst the ‘s’ key can be used to capture screen shots to add to the documentation for your application.

Testing on the device

When an application is under development it is best to temporarily load your application without permanently stored on the device. Providing there is enough available RAM then this can lead to a very quick edit-test cycles.

Try:

sh$ tools/wasptool \\
        --exec myapp.py \\
        --eval "wasp.system.register(MyApp())"
Preparing to run myapp.py:
[##################################################] 100%

Like the simulator, when an application is registered it is added to the launcher and it does not start automatically.

Note

If the progress bar jams at the same point each time then it is likely your application is too large to be compiled on the target. You may have to adopt the frozen module approach from the next section.

To remove the application simply reboot the watch by pressing and holding the physical button until the watch enters OTA mode (this takes around five seconds). Once the watch is in OTA mode then press the physical button again to return to normal mode with the application cleared out.

Uploading in source code form

To ensure you application survives a reboot then we must copy it to the device and ensure it gets launched during system startup.

Note

Applications stored in external FLASH have a greater RAM overhead than applications that are frozen into the wasp-os binary. If you app does not fix then see next section for additional details on how to embed your app in the wasp-os binary itself..

To copy your application to the external FLASH try:

sh$ ./tools/wasptool --upload myapp.py
Uploading myapp.py:
[##################################################] 100%

At this point your application is stored on the external FLASH but it will not automatically be loaded. This requires you to update the main.py file stored in the external FLASH. When wasp-os runs for the first time it automatically generates this file (using wasp/main.py as a template) and copies it to the external FLASH. After this point main.py is user modifiable and can be used to tweak the configuration of the watch before it starts running.

Edit wasp/main.py to add the following two lines and the end of the file (after the wasp.system.schedule():

from myapp import MyApp
wasp.system.register(MyApp())

Having done that we can use wasptool to upload the modified file to the watch:

sh$ ./tools/wasptool --upload wasp/main.py
Uploading wasp/main.py:
[##################################################] 100%

Note

If the new code on the watch throws an exception (including an out-of-memory exception) then your watch will display a black screen at startup. If that happens, and you don’t know how to debug the problem, then you can use wasptool to restore the original version of main.py .

Uploading in binary form

Some applications are too large to be compiled on the target. These applications need to be pre-compiled and can then either be uploaded in binary form to the wasp-os filesystem or included in the firmware image to further reduce the RAM overhead.

To pre-compile your application:

sh$ ./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m myapp.py

To copy the binary to the wasp-os filesystem try:

sh$ ./tools/wasptool --binary --upload myapp.mpy
Uploading myapp.mpy:
[##################################################] 100%

At this point your application is stored on the external FLASH but it will not automatically be loaded but it can be imported using import myapp. The application can be registered from main.py using exactly the same technique as uploads in source code.

Freezing your application into the wasp-os binary

Freezing your application causes it to consume dramatically less RAM. That is because they can execute directly from the internal FLASH rather than running from RAM. Additionally the code is pre-compiled, which also means we don’t need any RAM budget to run the compiler.

Freezing your application requires you to modify the manifest.py file for your board (e.g. wasp/boards/pinetime/manifest.py) to include your application and then the whole binary must be re-compiled as normal.

After that you an use the same technique described in the previous section to add an import and register for you application from main.py

Note

The micropython import path “prefers” frozen modules to those found in the external filesystem. If your application is both frozen and copied to external FLASH then the frozen version will be loaded.

In many cases it is possible to avoid rebuilding the binary in order to test new features by directly parsing the code in the global namespace (e.g. using wasptool --exec rather than wasptool --upload combined with import). With the code in the global namespace it can then be patched into the system. For example the following can be used to adopt a new version of the CST816S driver:

./tools/wasptool\
    --exec wasp/drivers/cst816s.py\
    --eval "watch.touch = CST816S(watch.i2c)"`

Application entry points

Applications provide entry points for the system manager to use to notify the application of a change in system state or an user interface event.

The complete set of wasp-os application entry points are documented below as part of a template application. Note that the template does not rely on any specific parent class. This is because applications in wasp-os can rely on duck typing making a class hierarchy pointless.

class apps.template.TemplateApp

Template application.

The template application includes every application entry point. It is used as a reference guide and can also be used as a template for creating new applications.

NAME = 'Template'

Applications must provide a short NAME that is used by the launcher to describe the application. Names that are longer than 8 characters are likely to be abridged by the launcher in order to fit on the screen.

ICON = RLE2DATA

Applications can optionally provide an icon for display by the launcher. Applications that expect to be installed on the quick ring will not be listed by the launcher and need not provide any icon. When no icon is provided the system will use a default icon.

The icon is an opportunity to differentiate your application from others so supplying an icon is strongly recommended. The icon, when provided, must not be larger than 96x64.

__init__()

Initialize the application.

__weakref__

list of weak references to the object (if defined)

_draw()

Draw the display from scratch.

_update()

Update the dynamic parts of the application display.

background()

De-activate the application.

foreground()

Activate the application.

press(button, state)

Notify the application of a button-press event.

preview()

Provide a preview for the watch face selection.

preview() must be implemented by watch face applications because it is called by the watch face selector. When called the application should redraw the screen as through it was the foreground() application. The application will not be active after the preview.

Other applications should not implement this entry point.

sleep()

Notify the application the device is about to sleep.

swipe(event)

Notify the application of a touchscreen swipe event.

tick(ticks)

Notify the application that its periodic tick is due.

touch(event)

Notify the application of a touchscreen touch event.

wake()

Notify the application the device is waking up.