diff --git a/Makefile b/Makefile index fac9f59..5cdd4df 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/TODO.md b/TODO.md index bfab04f..546770a 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/docs/appguide.rst b/docs/appguide.rst new file mode 100644 index 0000000..8e7b469 --- /dev/null +++ b/docs/appguide.rst @@ -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: diff --git a/docs/conf.py b/docs/conf.py index 434f1ee..bf9b080 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,6 +30,7 @@ author = 'Daniel Thompson' # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.graphviz', 'recommonmark', ] diff --git a/docs/hello.py b/docs/hello.py new file mode 120000 index 0000000..dd0c2a1 --- /dev/null +++ b/docs/hello.py @@ -0,0 +1 @@ +../wasp/apps/hello.py \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 5249459..c88f8c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Welcome to WASP-OS's documentation! :caption: Contents: README + appguide wasp TODO license diff --git a/docs/wasp.rst b/docs/wasp.rst index a44803d..0bab8e7 100644 --- a/docs/wasp.rst +++ b/docs/wasp.rst @@ -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: diff --git a/hello.py b/hello.py new file mode 100644 index 0000000..586082a --- /dev/null +++ b/hello.py @@ -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) diff --git a/wasp/apps/hello.py b/wasp/apps/hello.py new file mode 100644 index 0000000..586082a --- /dev/null +++ b/wasp/apps/hello.py @@ -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) diff --git a/wasp/apps/template.py b/wasp/apps/template.py index 4ff5c0e..e611545 100644 --- a/wasp/apps/template.py +++ b/wasp/apps/template.py @@ -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