Basics

Introduction

Once you feel familiar with the tools from the previous section, you’re ready to advance to the next chapter: writing full-fledged applications that can draw graphics on the screen, respond to input and play sound!

The st3m framework is the main Python codebase you’ll be writing against. Instead of using standard Micropython libraries like machine or low level display drivers, the st3m framework provides custom modules made for applications cooperating peacefully within the operating system. For applications, we’ll be using the Application class from the st3m.application module. You can find the documentation for this and other relevant modules in the App API sections.

flow3r applications are very straightforward: Almost everything is handled in a single loop that just runs over and over for as long as your application is active. This loop calls the .think() method, which is the central almost-everything processor of flow3r.

The one thing that isn’t handled by .think() is drawing to the display. This is taken care of by the .draw() method; the reason for this split is that the framerate may be much slower than you want to react to inputs; for a musical instrument application that plays notes, .think() can typically operate with less than 10ms reaction time, while the display often is drawn every 30ms or less.

The operating system also needs to do its thing, so all in all our central loop in its simplest form looks like this:

# this is very simplified, object/method names are largely made up
# ins, delta_ms and ctx are conjured out of thin air for now
while True:
    os.do_things()
    application.think(ins, delta_ms)
    if graphics_backend.can_receive_data_for_next_frame():
        application.draw(ctx)

Now that we have a rough idea of how things are called, let’s examine the two applications that we have introduced in the Blinky section:

Input Processing: CaptouchBlinky explained

Let’s start with CaptouchBlinky. There’s just 4 methods:

__init__

def __init__(self, app_ctx):
    super().__init__(app_ctx)

    self.colors = (
        (1,1,0),
        (0,1,1),
        (1,0,1),
        (0,1,0),
        (0,0,1),
    )
    self.active_color = self.colors[0]

This method is called only once per boot when the application is opened for the first time (as opposed to on_enter() as explained further down). It sets up some data for our application: colors is a tuple of 5 RGB values, .active_color is a reference to whichever color is active a the moment. Simple enough, but there’s one more thing:

The super().__init__(app_ctx) call is easily forgotten, but it is important to set up the environment for the application properly. This is implemented in the Application class, which we can invoke with the super() method. This is a recurring pattern across application methods: We merely enhance them while still retaining the original (“super”) parent class behavior.

This may sound complicated if you’re not familiar with Python, but in practice it can be quite simple: Look up the documentation of st3m.application.Application for each method you are implementing, it will tell you what to do. As a rule of thumb, calling the super() variant at the start of your implementation rarely hurts.

think

def think(self, ins, delta_ms):
    super().think(ins, delta_ms)

    for x in range(0, 10, 2):
        if self.input.captouch.petals[x].whole.pressed:
            self.active_color = self.colors[x//2]

    leds.set_all_rgb(*self.active_color)
    leds.update()

This is the platonic ideal of .think(): We are processing some input, mangling it a little and then updating a driver accordingly. self.input provides us with edge detection (see st3m.input.InputController) to return True only in the think cycle where the user has just started pressing a petal.

Again, there’s a super() call. Try removing it, and you’ll notice that your captouch input just broke; the edge detection runs in .super().think(), and nobody is executing that.

Needless to say, care should be taken to not let think run for very long in most cases: If executing each .think() takes say 50ms, your UI will react very delayed or even drop shoulder button inputs. This isn’t always possible or even important: Say you’re loading a big file for something and just display a loading screen without reading any inputs; you don’t need to care about think rate in that situation. As soon as it is fully loaded though you probably want it to go back up into the 5-20ms range of course.

draw

def draw(self, ctx):
    ctx.rgb(*self.active_color).rectangle(-120, -120, 240, 240).fill()

flow3r is powered by the ctx graphics engine. The ctx object passed here allows you to generate a drawlist that is then sent off to the rendering engine when the draw method is complete. In this case, we fill the entire screen with one color. For details on ctx, look up its documentation in the API section or here

Note that this method does not block until the render is complete, but rather just prepares the render instructions, while the image is rendered by a different task that runs independently of micropython. The draw method is only called again when that render task is ready to receive new data.

Note the absence of a super() call this time; there’s simply nothing to do in the default case. We can call it anyways, it won’t hurt, but it also doesn’t do anything.

One last detail: We draw every single frame here, even if the image hasn’t changed. We don’t have to do that, in fact it is recommended to consider only to partially redraw your screen, but that topic deserves its own section.

get_help

def get_help(self):
    context_sensitive_help = (
        "This app changes color of all LEDs and the display "
        "when touching a top petal. "
    )
    context_sensitive_help += f"The current RGB values are {self.active_color}."
    return context_sensitive_help

flow3r has a built-in help reader that allows application programmers to provide a user manual. This allows to build deep UIs without having to explain it all on-screen. Even for the most trivial application it is good practice to add a small text here to explain what to expect from it; even simple features can be missed if users aren’t sure what they are supposed to be looking for.

For more involved applications, note that we can change the text output of this method depending on state; it is called again each time the user opens the help menu, so you can provide context sensitive help to protect the user from having to scroll through a huge wall of text.

No super() call needed here again, we leave it as an exercise to the reader to determine the reasons for this ;).

Writing an application for the menu system

To add the application to the menu we are missing one more thing: a flow3r.toml file which describes the application so flow3r knows where to put it in the menu system. Together with the Python code this file forms a so called bundle (see also BundleMetadata).

[app]
name = "CaptouchBlinky"
category = "Apps"

[entry]
class = "Demos"

[metadata]
author = "an identifying and/or empty string"
license = "pick one, LGPL/MIT/CC0 maybe?"
url = "https://git.flow3r.garden/you/mydemo"

Save this as flow3r.toml together with the Python code as __init__.py in a folder (name doesn’t matter) and put that folder into one of the possible application directories (see below) using `Disk Mode`_. Restart the flow3r and it should pick up your new application.

Medium

Path in Disk Mode

Path on Badge

Notes

Flash

sys/apps

/flash/sys/apps

“Default” apps.

Flash

apps

/flash/apps

Doesn’t exist by default. Split from sys to allow for cleaner updates.

SD

apps

/sd/apps

Doesn’t exist by default. Will be retained even across badge reflashes.

Note that if we now start this app from the flow3r menu instead of mpremote, then exit and re-enter the last selected color is still active. This is because the application object never was destroyed, so the .active_color attribute has not been reset.

OS Integration: AutoBlinky explained

Now that we know how CaptouchBlinky works, AutoBlinky provides little excitement, but it makes for an interesting example for OS Integration. Before going into that, let’s rush throug the singular interesting bit:

def think(self, ins, delta_ms):
    super().think(ins, delta_ms)
    self.timer_ms += delta_ms
    self.timer_ms %= self.blink_time_ms * len(self.colors)

    index = self.timer_ms // self.blink_time_ms
    self.active_color = self.colors[index]

    leds.set_all_rgb(*self.active_color)
    leds.update()

We’re using the delta_ms argument of .think() this time to make the LEDs change color at a somewhat constant time interval. It is only somewhat constant because .think() doesn’t react to the event directly; it just checks whenever a think cycle starts whether enough time has passed, which may be more. Note that the code is structured in such a way that this error does not accumulate.

Depending on your programming background you might wonder how to install an ISR timer or the likes to trigger such processes on a fixed interval; this is not quite trivial with micropython and it generally does not encourage this use case. At this point in time flow3r has no official concurrency support in micropython execution.

Let’s make this code a tiny bit more interesting and modify it to have a few more colors to step through:

import random

class MegaBlinky(AutoBlinky):
    def __init__(self, app_ctx):
        super().__init__(app_ctx)

        self.timer_ms = 0
        self.colors = (
            (
                random.random(),
                random.random(),
                random.random()
            )
        for x in 10000)
        self.active_color = self.colors[0]
        self.blink_time_ms = 500

Let’s see, 10000 colors, 3 floats each, 32bit per float, say 100% micropython overhead - that is at least 240kB of data. Application objects are never destroyed, so this data shall live in RAM forever and all eternity or until the next power cycle, whichever happens earlier. Let’s be nice and get rid of it when we exit the app:

class PoliteMegaBlinky(AutoBlinky):
    def __init__(self, app_ctx):
        super().__init__(app_ctx)

        self.timer_ms = 0
        self.blink_time_ms = 500

    def on_enter(self, vm):
        super().on_enter(vm)
        self.colors = (
            (
                random.random(),
                random.random(),
                random.random()
            )
        ) for x in 10000)
        self.active_color = self.colors[0]

    def on_exit(self):
        super().on_exit()
        self.colors = None

The .on_enter() method gets called every time the app is launched from the menu (unlike .__init__(), which is only called the first time the app is launched during that boot cycle). .on_exit() is called on each exit. These are called before the first and after the last .think() call of the “opened phase” respectively. Note that the latter doesn’t hold true for``draw()``: It may be called after ``on_exit()`` has executed.

These two method once again require their respective super() calls at the beginning.

Since this deinitializes resources, it is easy to accidentially make apps crash when exiting and/or re-entering when you access them. Since you cannot test this with mpremote run as it doesn’t allow you to use the normal OS utilities and exit/re-enter the app, so the final testing phase of each app should include installing it on flow3r manually as described in the previous section.

This example may seem a bit silly, but there’s a few resource types you really want to avoid hogging, such as open files or bl00mbox channels.

Another use of these functions is setting up hardware; when you exit an application, many hardware settings are overriden by the operating system and not restored upon reentry. Let’s say we want to have smooth color transitions in the example above, this would also be a job for .on_enter() instead of __init__() to make sure is applied at the second start as well:

class SmoothPoliteMegaBlinky(PoliteMegaBlinky):
    def on_enter(self, vm):
        super().on_enter(vm)
        self.colors = (
            (
                random.random(),
                random.random(),
                random.random()
            )
        ) for x in 10000)
        self.active_color = self.colors[0]
        leds.set_slew_rate(min(leds.get_slew_rate(), 100))

Handling assets

Using Application also gives you access to the ApplicationContext as self.app_ctx, which for example gives you a way to find out the base path of your app in app_ctx.bundle_path or its bundle metadata in app_ctx.bundle_metadata. It’s very important not to hardcode paths to your assets and use bundle_path instead, because applications can be installed in various places depending on installation method and moved between internal flash and SD card.

A simple app that does nothing but draws a file named image.png from its directory could look like this:

from st3m.application import Application
from ctx import Context
import st3m.run

class MyDemo(Application):
    def draw(self, ctx: Context) -> None:
        # Draw a image file
        ctx.image(f"{self.app_ctx.bundle_path}/image.png", -120, -120, 240, 240)

if __name__ == '__main__':
    # Continue to make runnable via mpremote run.
    st3m.run.run_app(MyDemo, "/flash/apps/MyDemo")

Note the default path provided in st3m.run.run_app call - this is the path that’s going to be used when running standalone via mpremote, so set it to the path where you’re putting your app files on your badge during development.

Best practices

Before you submit your application to the app store, here’s a checklist for some common pitfalls and the like:

General applications

  • Do not use sys_* API: The sys_* modules (like sys_audio) are not intended to be used by applications, they are for the operating system only. If you have a legitimate use case to expose some sys_* features to applications we’d kindly ask you to open an issue and we’ll see how we can do it safely. Some limitations however are necessary and/or intentional.

  • Check for crashes on re-entry: Does your application work well when you exit and re-enter? Does it do so in any state?

  • Don’t hog resources: Does your application free all significant resources (open files, bl00mbox channels, etc.) when it exits?

  • Avoid stale data on re-entry: Does your application still use data (for example IMU orientation) from its previous run after exiting and re-entering? Should it do that?

  • Hardware config not applied properly Most hardware configs need to be applied every time you enter the application. If you do it in .__init__() instead it might work on the first entry, but not on later re-entry.

  • Account for either button orientation: Does your application still make sense if you swap App and OS button in the global configuration?

  • Provide a nice help text: Do you think you provide enough information so that people can figure out your application?

  • If overriding OS button, provide proper exit paths: If you press down the app button enough times, every app should either eventually exit or make it clear to the user how it’s intended to be exited. This is usually handled automatically, but in case you use the override this is your responsibility.

  • Do not load from/save to flash: Saving to flash force-stops the music pipeline momentarily, which is very bad in a session. This is a hardware limitation and cannot be fixed, therefore apps should never save to flash.

  • Adhere to savefile best practices: See the Savefiles documentation page for how to integrate them nicely within flow3r’s operating system.

Music applications

  • Do not override volume control: We are reserving this “UI namespace” for an upcoming feature. Do not override the volume controls for music applications. It’s also just rude if stuff starts playing loudly and you can’t quickly turn it down.

  • Adhere to bl00mbox best practices: They are not yet in a convenient list for now but just in prose in their documentation section; we’ll provide a more compact version soon, but for now please do carefully read the sections for all features that you intend to use.

(These lists are an incomplete work in progress)

Distributing applications

We have an “App Store” where you can submit your applications: https://flow3r.garden/apps/

To add your application, follow the guide in this repository: https://git.flow3r.garden/flow3r/flow3r-apps