docs: Introduce the Application Writer's Guide.

This commit is contained in:
Daniel Thompson 2020-05-10 14:10:04 +01:00
parent bb8e95cdd4
commit a2fd52ef2b
10 changed files with 427 additions and 13 deletions

View File

@ -72,8 +72,7 @@ docs:
sim:
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=$(PWD)/wasp/boards/simulator:$(PWD)/wasp \
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=.:wasp/boards/simulator:wasp \
python3 -i wasp/main.py
.PHONY: bootloader reloader docs micropython

View File

@ -64,10 +64,10 @@ applications.
* [X] Stopwatch app
* [X] Settings app
* [X] PC-hosted simulation platform
* [o] Documentation
* [O] Documentation
- [X] Sphinx framework and integration with github.io
- [X] Document bootloader protocols
- [ ] Application writer's guide
- [X] Application writer's guide
- [ ] Write full docstring documentation for all WASP components
* [X] Application Launcher
* [X] Debug notifications

350
docs/appguide.rst Normal file
View File

@ -0,0 +1,350 @@
Application Writer's Guide
==========================
.. contents::
:local:
Introduction
------------
WASP, 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"[#]_.
In other words if we want a watch built around a tiny microcontroller to be
sufficiently "smart" then it has to be all about the applications.
This guide will help you get started writing applications for WASP. Have fun!
.. [#] 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 smart.
Hello World for WASP
~~~~~~~~~~~~~~~~~~~~
Let's start by examining a simple "Hello, World!" application for WASP.
.. literalinclude:: hello.py
:linenos:
There are a couple of points of interest:
1. Applications have a :py:data:`~.TemplateApp.NAME`, which is shown in the
launcher. Most applications also provide an :py:data:`~.TemplateApp.ICON`
but a default is displayed if this is omitted.
2. This example uses :py:meth:`~.TemplateApp.__init__` to initialize
the state of the application, this ensure the state remains "sticky"
when the application is activated and deactivated.
3. :py:meth:`~.TemplateApp.foreground` is the only mandatory application entry
point and is responsible for redrawing the screen. This application does
not implement :py:meth:`~.TemplateApp.background` because there is nothing
for us to do!
4. The use of :py:meth:`~.TemplateApp._draw` is optional. We could just do
the work in :py:meth:`~.TemplateApp.foreground` but this application follows
a common WASP 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 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 notifications.
System notifications control the application life-cycle and the entry point
calls, together with the implicit application states are shown below.
.. graphviz::
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 in the background. To conserve
memory WASP does not permit two applications to run simultaneously but
because each application preserves its state when in the background 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 it's state, until either the
stopwatch is stopped of the application is destroyed.
A backgrounded application can enter the ``ACTIVE`` state via a call to
:py:meth:`~.TemplateApp.foreground`. When it is active the application owns the
screen and should draw and maintain its UI.
If the system manager want to put an active application to sleep then it will
call :py:meth:`~.TemplateApp.sleep`. If the application returns True then the
application will stop running (e.g. receive no events and no application tick)
but instead must wait to receive a notification via
:py:meth:`~.TemplateApp.wake` 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
:py:meth:`~.TemplateApp.sleep` (or :py:meth:`~.TemplateApp.wake`) although it
can also return False from :py:meth:`~.TemplateApp.sleep` if this is preferred.
In this case the system manager will automatically return to the default
application, typically the main clock face.
Note: *Most applications do not need to support 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 for WASP. For more comprehensive coverage see the
:ref:`WASP Reference Manual` which contains extensive API documentation
covering the entire of WASP, including its drivers.
System management
~~~~~~~~~~~~~~~~~
The system management API does provide a number of low-level calls that
can register new applications and navigate between them. However most
applications need not use these. Instead most applications use a small
set of methods. In particular almost all applictions need to call a couple of
methods from :py:meth:`~.TemplateApp.foreground` in order to register
for notifications:
* :py:meth:`~.Manager.request_event` - register for UI events such as button
presses and touch screen activity.
* :py:meth:`~.Manager.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 then it should arrange to call
:py:meth:`~.Manager.keep_awake` from the application's
:py:meth:`~.TemplateApp.tick` method.
Drawing
~~~~~~~
Most applications using the drawing toolbox, :py:data:`wasp.watch.drawable`,
in order to handle the display. Applications are permitted to directly access
:py:data:`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 simple applications then the following simple drawing
functions are sufficient.
* :py:meth:`~.Draw565.blit` - blit an image to the display at specified (x, y)
coordinates, image type is detected automatically
* :py:meth:`~.Draw565.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
* :py:meth:`~.Draw565.string` - render a string, optionally centring it
automatically
* :py:meth:`~.Draw565.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
:py:meth:`~.TemplateApp.foreground` or :py:meth:`~.TemplateApp._draw` methods
in order to clear the display ready for a redraw.
.. code-block:: python
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 provides a small number of widgets that allow common fragments of
logic and redrawing code to be shared between applications:
* :py:class:`.BatteryMeter`
* :py:class:`.ScrollingIndicator`
MicroPython
~~~~~~~~~~~
Many of the features of WASP are inherited directly from MicroPython_. It is
useful to have a basic understanding of MicroPython and, in particular, put
in a little time learning the best ways to copy with running
`MicroPython on microcontrollers`__.
.. _MicroPython: https://micropython.org/
__ http://docs.micropython.org/en/latest/reference/constrained.html
How to run your application
---------------------------
Testing on the simulator
~~~~~~~~~~~~~~~~~~~~~~~~
WASP provides 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
(e.g. missing symbols).
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.*
Firstly launch the simulator:
.. code-block:: sh
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
code:
.. code-block:: python
:linenos:
^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 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()``).
Testing on the device
~~~~~~~~~~~~~~~~~~~~~
If we have an application under development when we can launch a quick test
that does not result in the application being permanently stored on the device.
Providing there is enough available RAM then this can lead to a very quick
edit-test cycles.
Try:
.. code-block:: sh
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 does not start
automatically but it will have been added to the launcher and can be launched
using the normal gestures to control the device.
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.*
Making it permanent
~~~~~~~~~~~~~~~~~~~
To ensure you application survives a system reset (press the hardware
button for around five seconds until the splash screen is seen, wait
five seconds and then press again) 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 binary. See next section
for additional details.*
To copy your application to the external FLASH try:
.. code-block:: sh
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 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 between ``import wasp``
and the ``wasp.system.run()``:
.. code-block:: python
from myapp import MyApp
wasp.system.register(MyApp())
Having done that we can use ``wasptool`` to upload the modified file
to the watch:
.. code-block:: sh
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 .*
Freezing your application into the WASP binary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Freezing your application causes it to consume dramatically less RAM. That is
because the code is both pre-compiled (meaning we don't need any RAM budget to
run the compiler) **and** it can execute directly from the internal FLASH
memory.
Freezing your application simply 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 to ``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.*
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.
.. automodule:: apps.template
:members:
:private-members:
:special-members:

View File

@ -30,6 +30,7 @@ author = 'Daniel Thompson'
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.graphviz',
'recommonmark',
]

1
docs/hello.py Symbolic link
View File

@ -0,0 +1 @@
../wasp/apps/hello.py

View File

@ -11,6 +11,7 @@ Welcome to WASP-OS's documentation!
:caption: Contents:
README
appguide
wasp
TODO
license

View File

@ -1,5 +1,7 @@
WASP Internals
==============
.. _WASP Reference Manual:
WASP Reference Manual
=====================
System management
-----------------
@ -27,10 +29,6 @@ Applications
:members:
:undoc-members:
.. automodule:: apps.template
:members:
:undoc-members:
.. automodule:: apps.testapp
:members:
:undoc-members:
@ -79,7 +77,6 @@ Libraries
.. automodule:: fonts.sans24
:members:
:undoc-members:
.. automodule:: logo
:members:

19
hello.py Normal file
View File

@ -0,0 +1,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)

19
wasp/apps/hello.py Normal file
View File

@ -0,0 +1,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)

View File

@ -1,13 +1,40 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2020 Daniel Thompson
"""Template application implementing all application method calls.
"""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.
"""
import wasp
import icons
class TemplateApp():
"""Template application ready to use as a basis for new applications.
"""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.
.. data:: 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.
.. data:: 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.
"""
NAME = 'Template'
ICON = icons.app