PineTime Smart Watch running a [druid] app with a Label Widget and a Button Widget

Porting [druid] Rust Widgets to PineTime Smart Watch

UPDATE: This code in this article has been archived in the pre-lvgl branch of pinetime-rust-mynewt. The pinetime-rust-mynewt firmware has been revamped to support Rust Watch Faces on LVGL. Check out the updates

A button that responds to our tapping and increments a counter… That’s what we shall accomplish today on the PineTime Smart Watch

The Watch App we see in the video was created with the [druid] Crate in Rust.

In [druid], UI controls are known as Widgets. Our app contains two Widgets: A Label Widget that displays the counter, and a Button Widget that increments the counter.

Lemme explain the code in the [druid] app

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

When the app is launched, we create a new window, passing it the ui_builder function that will return a list of Widgets and their layouts. (More about ui_builder later.)

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Here we set the Application State to 0. What’s this Application State?

Remember the counter value that’s incremented every time we tap the button? This counter value is stored in the [druid] Application State. Widgets will interact with the Application State: Our Button Widget updates the state and our Label Widget displays the state.

That’s why [druid] is known as a Data-Oriented Rust UI… [druid] Widgets are bound to data values in the Application State.

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Recall that main_window contains a list of Widgets and their layouts. We’ll pass this to the AppLauncher to launch the app window. The app shall be launched with data, the Application State, set to 0. If the launch fails, we’ll stop with an error launch failed.

Now let’s look at ui_builder and how it creates the Widgets…

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

First we create a text string that contains the counter value. How shall we get the value of the text string? Through this Rust Closure…

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

What’s a Rust Closure? Think of it as a Rust Function without a name. The Closure above is equivalent to this verbose Rust Function…

So it’s simpler to use the Closure form. The Closure simply returns the value of the counter in data (the Application State) as the value of the text string. The into part converts the returned value from number to string.

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Now that we have the counter value stored in our text string, let’s display it with a Label Widget.

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Here we create a button labelled increment that calls this Rust Closure whenever the button is tapped…

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

This Closure is equivalent to the Rust Function…

data contains our Application State, which is our counter value. The Closure simply increments our counter value by 1, every time the button is tapped.

Hey Presto! That’s the magic behind our Data-Oriented UI… The button increments the counter in our Application State, the label displays the value of the counter in our Application State!

[druid] completes the magic act by wiring up the Widgets to the Application State… When our Application State changes (upon tapping the button), [druid] automagically refreshes our Label Widget to show the new counter value.

Let’s tell [druid] how to layout our Label and Button Widgets for display…

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

We’ll display the Label and Button Widgets in a single column col. The Label Widget goes on top, centered horizontally, with a padding of 5 pixels.

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

The Button Widget appears next in col. Also with a padding of 5 pixels.

Note that the second argument to add_child is the same for both the label and the button: 1.0. This means that the label and button shall occupy equal vertical spacing, i.e. the label shall fill the top half of the screen, the button shall fill the bottom half. This is similar to the flexbox concept in CSS.

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

And lastly, we return the col to the caller. To recap: col contains the Label and Button Widgets, the Closures for displaying and incrementing the Application State counter, and the layout of the Widgets.

That’s the clever simplicity of [druid] apps. [druid] apps have a Declarative UI like SwiftUI and Flutter… Without the legacy baggage of iOS and Android. That’s why I think [druid] is perfect for building Watch Apps for PineTime!

Here’s the complete [druid] Watch App for PineTime…

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs


Powering [druid] with Type Inference

The above Rust source code for the watch app looks so trivial (compared with SwiftUI and Flutter)… Surely something must be missing in that app?

Oh yes, plenty is missing from the app… But the Rust Compiler performs Type Inference and fills in all the missing bits for us! Here’s how it looks with the missing bits filled in…

Note that the Widgets have been expanded as Label<u32> and Button<u32>. That’s because Label and Button are actually Generic Types in Rust.

Quick recap of Generic Types: Vec is a Generic Type that represents a vector (array) of values. When we write Vec<i32>, it refers to a vector of integer values (i32).

Thus Label<u32> and Button<u32> are Generic Widgets that are backed by u32, our Application State that contains the counter. Reminds us that [druid] is really a Data-Oriented UI!

Some types (like WindowDesc) are Generic Types with two parameters. So this line of code in our app…

let main_window = WindowDesc::new(ui_builder);

Actually expands to this complicated mess…

let main_window = WindowDesc::<u32,Flex<u32>>::new(ui_builder);

What a bloody clever compiler! The Rust Compiler makes it easy for us to write Watch Apps. Under the hood, things are a lot more complicated with the Generic Types. Read on to learn how [druid] Widgets are implemented on PineTime…


Downsizing [druid] from Desktop to Embedded

[druid] is a GUI toolkit that produces desktop apps for Windows, macOS and Linux. It doesn’t support Embedded Platforms like PineTime. So I had to change some features to make [druid] work on PineTime. (Only the [druid] implementation was changed, not the [druid] APIs… The same [druid] desktop application code can be compiled for PineTime without any code changes! Compare the above [druid] code for PineTime with the original version)

On Embedded Rust we declare our programs as #![no_std]. This means that our embedded programs will call the Rust Core Library, instead of the usual Rust Standard Library which has many more features. Any code in [druid] that uses the following features from the Rust Standard Library will NOT compile on PineTime…

  1. Vec vectors, because it uses Heap Memory to support resizable vectors. And Heap Memory is not available on embedded platforms (by default).
  2. String (though &str is supported), because it also uses Heap Memory.
  3. format!, because internally it uses a String to write formatted strings.
  4. Box, RefCell, Arc (Atomically Reference Counted) and Cow (Clone On Write), because they use Heap Memory as well.

Why isn’t Heap Memory available on Embedded Rust? Embedded platforms like PineTime often have little RAM. (Only 64 KB RAM for PineTime’s Nordic nRF52832 microcontroller) When RAM is limited, we prefer to budget our memory requirements in advance… Just to be sure that our Smart Watch doesn’t allocate too many Widgets at runtime and crash.

Hence all space for Widgets should be preallocated in Static Memory before the Watch App starts. (There is a way to implement Heap Memory on embedded platforms, check this for details)

So many features are missing from Embedded Rust… Is it still possible to run [druid] apps on PineTime? Yes, I used some tricks to make a quick (and somewhat dirty) port of [druid] to PineTime… Which you can also use for porting Rust desktop libraries to embedded platforms! Read on to learn more…


Porting Vectors, Strings and format! to Embedded

The Vec and String types are used in many Rust libraries. On embedded platforms we can’t allow vectors and strings to be resized on demand… But we can create vectors and strings that have a maximum size.

Here’s the quickest (and dirtiest) way to use the [heapless] crate to replace standard types Vec and String by vectors and strings with fixed size limits…

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/localization.rs#L87-L91

(Yep told you this would be dirty… Should never override standard Rust types!)

Most of the surrounding code that calls Vec and String should compile with this simple modification. (We may need to replace &str by String) Watch out for the extra copies of vectors and strings that our program will now be copying… Keep them small!

Once the code works on our embedded gadget, Do The Right Thing and rename Vec and String to something sensible, like FixedVec and FixedString.

There’s a similar trick for porting format! to embedded platforms. Let’s say we are converting a number v to a text string…

format!("{}", v)

This fails to compile because format! uses an internal String to store the formatted result. The [heapless] solution uses the write! macro like this…

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/argvalue.rs#L63-L66


Boxing up Widgets and Windows

Without Heap Memory, [druid] can’t use Box to allocate Widgets on the heap…

Original [druid] code that allocates Widgets on the heap. From https://github.com/xi-editor/druid/blob/master/druid/src/widget/flex.rs#L103-L151

Why does [druid] use Box Widgets? Because [druid] has Container Widgets (Flex / Row / Column) that contain references to Child Widgets, like we see above.

In our embedded version, [druid] uses a custom WidgetBox type that pretends to be a Widget in a Box

Modified [druid] code that uses static WidgetBox. From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/flex.rs#L126-L151

Here’s a crude implementation of WidgetBox… Instead of the heap, WidgetBox stores Widgets in an array in Static Memory. WidgetBox then returns the array index (Widget ID) at which the Widget is stored….

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/widgetbox.rs

WidgetBox implements the Traits (features) of a Widget by forwarding the function calls to the Widget stored in Static Memory…

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/widgetbox.rs

Storing Widgets statically can be tricky… Our Widgets are Generic Types that depend on the data type of the Application State. Earlier we have seen that the Rust Compiler expands our Watch App with labels and buttons as Label<u32> and Button<u32>

The solution is to use a Specialised Trait like this…

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/widgetbox.rs

Note the pattern for coding a Specialised Trait…

  1. Declare the Trait across all types:
    trait GlobalWidgets<D> { fn f(arg: D); ...
  2. Implement the Default Trait:
    impl<D> GlobalWidgets<D> { default fn f(arg: D) { ...
  3. Implement the Specialised Trait for each specific type like u32:
    impl GlobalWidgets<u32> { fn f(arg: u32) { ...

The same trick is used to port [druid] Windows to PineTime. Whenever we need to Box up a [druid] Window, we use a WindowBox instead. Windows and Window Handlers are now stored in a static array just like Widgets.


[druid] Dependencies

Connect [druid] to PineTime’s Display and Touch Controllers

Now that [druid] compiles successfully on PineTime, let’s connect our fork of [druid] to the PineTime display and touch hardware…

[druid] calls the [piet] crate to render graphics and text. In our first PineTime article, we have created a Rust display driver based on the [embedded-graphics] and [st7735-lcd] crates.

Hence the solution: Adapt [piet] to render graphics and text with [embedded-graphics]. Here’s the resulting fork…

Here’s a peek of [piet] adapted for [embedded-graphics]: Rendering a Bezier path with [embedded-graphics] primitives (so that we can have rounded buttons)…

Rendering Widgets with [embedded-graphics]. From https://github.com/lupyuen/piet-embedded/blob/master/piet-embedded-graphics/src/context.rs#L67-L199

The display driver from our first PineTime article display.rs has been relocated into the [piet] library.

💎Advanced Topic: The [embedded-graphics] version of [piet] make take a few seconds to blank the screen or fill a region with colour (e.g. for buttons). I may enhance the display driver [st7735-lcd] to use SPI with DMA for such operations.

What about touch? In our second PineTime article, we have created a Rust Touch Controller Driver. This Touch Controller Driver has been wired up to send touch events to [druid]. [druid] doesn’t support touch events yet, so we simulate the mouse down + mouse up events instead…

Handling Touchable Widgets. From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/win_handler.rs#L62-L90

💎Advanced Topic: If you check the video demo, most taps of the button respond within 1 second, but some taps on the button don’t seem to increment the counter, and some taps appear to increment the counter twice. That’s because the Touch Controller Driver has not been implemented completely.

The touchscreen controller supports multitouch, so the controller tracks each finger and reports whether each finger is a press (down) or release (up) action. The current Touch Controller Driver interprets only the first finger detected, and always assume it’s a press (down) action not a release (up) action.


Other Features Downsized in [druid] and [kurbo]

The above fork of [druid] has some features commented out…

  1. Localization: Our [druid] app uses a Localized String hello-counter… This actually refers to a Resource File that specifies the displayed text as Current value is <count>. This is probably overkill for a proof-of-concept, so it has been stubbed out and replaced by a simpler version that displays only the first argument.
  2. Themes and Environments: [druid] supports UI themes and configurable environment settings. For embedded platforms they should probably be implemented as compile-time constants. So Themes and Environments have been stubbed out for now.
  3. Menus and Keyboards: Since they are not necessary for Watch Apps

[druid] and [piet] use the [kurbo] crate for computing Bezier curves, which are necessary for drawing rounded buttons. [kurbo] has been downsized to work with PineTime and #![no_std]

  1. Bezier paths are limited to 16 points
  2. Double-precision floating-point (f64) math functions seem to be missing from the Rust Core Library, so we are using the [libm] crate instead. We could switch to single-precision floating-point (f32) and use [micromath] instead, because it’s optimised for embedded platforms.

Here is my fork of [kurbo] for PineTime…


PineTime ROM Usage for [druid] and Friends

[druid] + [piet] + [kurbo] + [embedded-graphics] + [st7735-lcd] + Rust Core Library + Mynewt OS… Will this long and exciting Conga Line even run on PineTime’s Nordic nRF52832 microcontroller with 512 KB ROM and 64 KB RAM?

Yes it does! Total ROM used is only 237 KB, less than half of the available ROM space!

And this was built in Debug Mode, not optimised Release Mode! (Which is hard to debug and learn)

What about ROM space for firmware upgrades? Well we have an additional 4 MB SPI Flash available on PineTime!

Total RAM used is 46 KB, which includes 32 KB of stack space (Probably too much)

So a functional Rust Widget UI for Watch Apps looks mighty feasible on PineTime!

I think it also helps when I replaced the Box references in [druid] by WidgetBox and WindowBox… The Rust Compiler does an incredible job of removing dead code when everything is statically defined.

We can analyse the ROM bloat by loading this Linker Output Map into a Memory Map Google Sheet that’s explained in this article

Functions that occupy the most ROM space, sorted by ROM size (in bytes). From https://docs.google.com/spreadsheets/d/1Lb217jqZGM7NlnOVCxpaLAI0CQcOTPnbMpksDSPVyiw/edit#gid=381366828&fvid=1643056349

The Google Sheet is here. It shows the functions that occupy the most ROM space, sorted by ROM size (in bytes). Some observations…

  1. [libm], the floating-point math library, seems to be taking the most ROM space. We could replace this by [micromath], a smaller math library for embedded platforms
  2. core::num::flt2dec::strategy needs closer study. And why dragon and his friend grisu are hiding in my watch!
  3. [druid] seems to take a fair amount of ROM space. Which is expected since [druid] was designed for desktop use. The ROM usage will probably drop drastically once we enable size optimisation in the Rust Release Build.

What’s Next?

Here are the other articles in the PineTime series…

I hope the maintainers of [druid], [piet], [kurbo], [embedded-graphics], [st7735-lcd] will consider incorporating some of the tweaks I have covered in this article. Together we can do so much more when the Rust Ecosystem embraces Embedded Platforms!

And I hope you agree with me that this is clearly the better way to code Watch Apps. There’s more work to be done to flesh this out fully for PineTime… Lemme know if you’re keen to help!

The source code in this article may be found in the pinetime branch of this repository…

The macOS build files for [druid], [piet], [kurbo], Embedded Rust and Mynewt OS for PineTime are available here…

I’ll be packaging a proper release when PineTime is available to the general public. Meanwhile if you have trouble building the source files (as explained in the first article), please drop me a note! The PineTime build, flash and debug steps on Visual Studio Code look like this…

PineTime build, flash and debug steps on Visual Studio Code

UPDATE: PineTime is now available for purchase by general public! I’m creating a new repository here that will have a prebuilt firmware image for PineTime. I’ll also include instructions for flashing the firmware to your PineTime with a Raspberry Pi.