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 ;).
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