(UNFINISHED) Draw your own PineTime Watch Face... From WebAssembly to Embedded Rust

PineTime Smart Watch with Hand-Drawn Watch Face

📝 18 Nov 2020

NOTE: THIS ARTICLE IS UNFINISHED

Q: We're in the middle of the pandemic... Is there something fun and useful that we can learn on our own?

A: Try learning Embedded Programming!

Q: I need to buy an Arduino or a Microcontroller Board? And have it shipped sloooowly to me at exorbitant prices?

A: Nope! Today we can build and test Embedded Programs in a Web Browser... Without any Embedded Hardware! Made possible with awesome WebAssembly tools.

Q: But a Web Browser Simulator doesn't behave like Real Hardware right?

A: We have a real PineTime Smart Watch that's connected to the web for everyone to test our Embedded Programs... Try it for yourself and compare!

Q: People say that Embedded Programming is hard... Needs strange languages like C

A: Not any more! Today we'll code with Rust, a friendly, modern language. (And less frustating for new coders)

Q: Great! But my computer is kinda old, slow and weak... Do I need a powerful Linux desktop?

A: Any Linux, macOS AND Windows computer works great for coding in Embedded Rust and WebAssembly... Even a Raspberry Pi!

If you prefer zero setup, we can build our Embedded Rust and WebAssembly programs in the Cloud! (Provided free of change by GitHub Actions and GitLab CI)

It feels strange... But building and testing Embedded Programs will work on a Mobile Web Browser!

Q: Fantastic! Can't wait to build my very first Blinking LED program!

A: Well it's 2020, and we have progressed waaaay beyond Blinking LEDs ;-)

Today we'll learn to build a Smart Watch Face in Rust. We'll be coding for a Smart Watch with Colour Touchscreen, Bluetooth Networking and Real Time Clock... Just like the expensive watches by A***e and S*****g!

As promised, our Watch Face will run in a Web Browser, so it will be easy to test and troubleshoot.

And when you're done... Please flash your Watch Face to our Remote PineTime over the web. Show the world your embedded creation... Running on a real watch!

Q: But what's the catch?

None really. Now's the perfect time to Learn and Experiment with Embedded Programming... At our own pace, with whatever materials we have.

Read on and join me for the learning adventure! :-)

1 Hand-Drawn Watch Face

Let's make a Hand-Drawn Watch Face like the pic above. The Watch Face consists of 4 hand-drawn images that will be a-changin' with the times...

Watch Face Images

We start by zooming to the top left...

How shall we load the top left image... With the first digit of the hour?

In 3 steps...

  1. We compute the first digit of the hour. So if the hour is 23, the first digit is 2.

  2. We fetch the hand-drawn bitmap for the digit, i.e. 2

  3. We load the bitmap into the top left image

Here's how we do it in Rust: src/lib.rs

//  Update the top left image with the first digit of the hour

//  Compute the first digit of the hour
let digit = state.time.hour / 10;  

//  Fetch the bitmap for the digit as a constant pointer
let bitmap: *const img::lv_img_dsc_t =    
    &self.bitmaps[digit as usize];

img::set_src(                //  Set the source...
    self.top_left_image,     //  Of the the top left image...
    bitmap as *const c_void  //  To the digit bitmap
) ? ;                        //  Quit in case of error

Yep it looks daunting... These are the 3 hardest lines of code in our Watch Face!

But let's step through each line bit by bit and uncover the mystique of Rust.

(I promise you... The rest of the code will be much simpler!)

First digit of hour

2 Compute the hour digit

Given an hour like 23, compute the first digit i.e. 2. How shall we do this in Rust?

Here's how: src/lib.rs

//  Compute the first digit of the hour
let digit = state.time.hour / 10;

We interpret state.time.hour like a nested fairy tale...

Once upon a time, there was an object named state... That contained an object named time... That contained a field named hour... The current hour of the day (from 0 to 23)

(We'll learn the backstory of state in a while... And objects in Rust are named Structs)

Here we divide the hour by 10 (and truncate the result) to get the first digit.

So if hour is 23, then digit gets set to 2.

The Type of digit is missing. Is Rust a Typeless Language like JavaScript and Python?

Rust is a Statically Typed Language like C... All Variables have known Types during compilation. The Rust Compiler infers the Types for us.

Sounds spooky, but the Rust Compiler goes all Sherlock Holmes on our code to deduce our Variable Types...

I see what you did there... digit doesn't have a known type! Hmmm hour is a u8... 8-bit unsigned integer... After integer division we get another u8... So I deduce that digit is also u8!

To see this Sherlock smartness in action, mouse over digit in VSCode...

Type Inference

We see that u8 pops up for digit... The Rust Compiler has inferred that digit is an 8-bit unsigned integer. (Like uint8_t in C)

Now that we have the first digit of the hour, let's fetch the hand-drawn bitmap for the digit.

3 Fetch the digit bitmap

We have digit set to the first digit of the hour (0, 1 or 2). Here's how we fetch the bitmap for digit: src/lib.rs

//  Fetch the bitmap for the digit as a constant pointer
let bitmap: *const img::lv_img_dsc_t =    
    &self.bitmaps[digit as usize];

This looks... Strange. Let's break it down...

What's self.bitmaps?

self.bitmaps is an Array of 10 hand-drawn bitmaps, indexed from 0 to 9...

Bitmaps in Self

(We'll reveal our self later... Hint: We're inside an object!)

Thus to fetch the bitmap that corresponds to a digit, we do this...

let bitmap = self.bitmaps[digit];

What's usize?

Rust is extremely uptight about Types... Including the index for our Array self.bitmaps.

In Rust, Arrays are indexed by integers of the usize Type. (Somewhat like size_t in C)

Hence we need to convert (or cast) digit as usize like so...

let bitmap = self.bitmaps[digit as usize];

What's with the "&"?

We're not gonna pass around copies of the bitmap. (Because that would be awfully inefficient in a smart watch)

Instead we're passing a Reference to the bitmap. (Somewhat like a Pointer in C)

To get the Reference, we insert "&" like so...

let bitmap = &self.bitmaps[digit as usize];

What about *const img::lv_img_dsc_t?

Remember we set bitmap like so...

let bitmap: *const img::lv_img_dsc_t = ...

This means we're casting the bitmap Reference to a weird Type...

*const img::lv_img_dsc_t

This is a tough nugget to crack (unlike McNuggets)... But we'll learn its true meaning in a while.

(Hint: It's a Pointer (yep like C). And no we're not shopping for French luxury goods.)

4 Set the image source

Our story thus far: We have bitmap set to the hand-drawn digit (i.e. the first digit of the hour)...

Bitmap

Here's how we set the Top Left Image on our Watch Face to the bitmap: src/lib.rs

img::set_src(                //  Set the source...
    self.top_left_image,     //  Of the the top left image...
    bitmap as *const c_void  //  To the digit bitmap
) ? ;                        //  Quit in case of error

Let's savour this chunk by chunk (the old chunky way)...

What's self.top_left_image?

We have 4 images inside self...

Images in Self

So self.top_left_image refers to the Top Left Image on our Watch Face.

Why the studs in img::set_src?

Rust is fussy about keeping things neat, tidy and modular.

"img::" refers to the Module named img. When we write...

img::set_src( ... );

It means we're calling the function set_src defined in the Module img. (Similar to namespaces in C++)

Are we done yet?

Let's recap...

  1. We have a function set_src (from Module img) that will set the bitmap for an image

  2. We have the Top Left image: self.top_left_image

  3. We have a Reference to the digit bitmap: bitmap

Thus to set the bitmap for the Top Left Image we may write...

img::set_src(             //  Set the source...
    self.top_left_image,  //  Of the the top left image...
    bitmap                //  To the digit bitmap
) ? ;                     //  What's this???

Why the questionable "?" at the end?

"?" is the Try Operator in Rust. It checks for errors.

If set_src returns an error, Rust stops executing the current function. And returns the error immediately to the caller.

(This is similar to try ... catch ... throw in JavaScript and Python)

Wait! We haven't covered these two sus chunks with *const...

//  Cast the bitmap as a constant pointer
let bitmap: *const img::lv_img_dsc_t = ... ;

//  Set the bitmap pointer as the image source
img::set_src( ... , bitmap as *const c_void ) ? ;

Yep they look highly sus... Is this really Embedded Rust?

Get ready for the shocking reveal...

5 It was C all along

Earlier we saw two highly sus chunks of code: src/lib.rs

C Pointers

What's *const?

*const is a Raw Pointer in Rust.

It works like a Pointer in C... It's an address that points to an object in memory.

We already have References in Rust (via the "&" operator). Why do we need Raw Pointers?

Time to fess up...

  1. bitmap is a Raw Pointer to a C Object

  2. set_src is a C Function

😮 But why??? Can't we do this in Rust instead of C?

That's the beauty... Rust and C are interoprapereraberble...

Ahem... Rust and C Functions can call each other!

Both Rust and C are low-level languages. It's perfectly OK to call C Functions from Rust (and the other way around).

That's why some folks are coding in Rust instead of C for creating Embedded Gadgets.

What C Functions are we calling?

We're calling C Functions from the open-source LVGL Graphics Library. It's great for creating Graphical User Interfaces on Embedded Devices with images, text labels, buttons, ...

The LVGL Library is used by many firmware developers to create Watch Faces for PineTime. We're calling the LVGL Library too... But from Rust.

Now our sus code...

//  Cast the bitmap as a constant pointer
let bitmap: *const img::lv_img_dsc_t = ... ;

//  Set the bitmap pointer as the image source
img::set_src( ... , bitmap as *const c_void ) ? ;

Makes more sense when we realise...

  1. img::lv_img_dsc_t is a C Struct from the LVGL Library

    More details

  2. img::set_src is a C Function from the LVGL Library

    More details

Today we won't talk much about casting C Pointers in Rust and passing them to C Functions. More details may be found in "The Rust Programming Languague" book...

Raw Pointers in Rust

Below is a handy map to keep us on track... It shows the code that we have explored thus far. The rest of the code awaits!

Map of Update Method

Here We Are

6 Update the Watch Face

Remember our 3 toughest lines of Rust code... For updating the top left image on our Watch Face?

Let's zoom out and watch how we use them: src/lib.rs

Update Method

Now we zoom in to the top... Where we declare the update method: src/lib.rs

6.1 Declare the method

Declare the Update Method

In Rust we declare a function (or a method) by writing...

fn ... -> ... {

(Similar to function in JavaScript and def in Python... But looks more mathematical)

What's update?

update is the method that's called to update the Watch Face every minute.

It accepts 2 parameters...

  1. &mut self

    This refers to our Watch Face object and the variables inside: bitmaps, top_left_label, ...

    (Similar to self in Python or this in JavaScript and C++)

    &mut means that the self object is passed as a Reference (instead of a copy), and that the self object is Mutable (can be modified).

  2. state: &WatchFaceState

    This says that state is a Reference to a WatchFaceState, an object that contains the values that will be rendered to our Watch Face.

    Through this state, our caller passes the time of the day as hour (0 to 23) and minute (0 to 59).

    We have seen state.hour earlier... We used it to render the hour of the day.

6.2 Return the result

What's MynewtResult in the declaration above?

Remember the Try Operator "?" for checking errors returned by Rust Functions?

This works only for functions and methods that return the Result Type. Thus we follow the Rust error-checking convention and return a kind of Result named MynewtResult.

(Mynewt refers to the Apache Mynewt embedded operating system that we're running on PineTime)

Here's how we return the result in our update method: src/lib.rs

Return the result

Ok( ... ) tells the caller that the result is OK, no errors.

We write Ok(()) when we have no result value to return.

(Think of () in Rust like void in C)

Note that we omit the trailing semicolon ";" when returning the result.

FYI: We return errors with Err( ... )

6.3 The other images

We've seen top_left_image... What about the other images: top_right_image, bottom_left_image and bottom_right_image?

Images in Self

The code to update the other 3 images looks similar. Check out the rest of the update method here: src/lib.rs

Map of Update Method

Here We Are

Congratulations! We're done with the update method... That's half of the Watch Face code!

Now we move on to the new method... For creating the Watch Face.

Watch Face Goodies

7 Create the Watch Face

Our Watch Face has plenty of goodies inside (like a Kinder Egg)...

  1. bitmaps: The hand-drawn bitmaps of the digits 0 to 9

  2. top_left_image, top_right_image, bottom_left_image and bottom_right_image: The 4 images on our Watch Face

We have used them earlier but...

How are the bitmaps and images created?

Let's watch and learn...

7.1 Create a bitmap

We create the bitmap for the digit 0 as a Rust Struct like so: src/lib.rs

//  Create the bitmap struct for the digit 0
img::lv_img_dsc_t {
    //  Bitmap data, size and header
    data: include_bytes!("../bitmaps/0.bin") as *const u8,
    data_size,
    header
}

(Rust Structs are structured objects with fields inside... Just like Structs in C and Classes in Python)

What's img::lv_img_dsc_t?

We're reusing the C Struct lv_img_dsc_t from Module img of the LVGL Library. The lv_img_dsc_t Struct represents a bitmap in LVGL.

(Rust Structs and C Structs are generally interchangeable, with the right settings)

In Rust, we create instances of Structs by writing...

struct_type {
    field_name: field_value,
    ...
}

Let's look at the 3 fields inside our bitmap Struct...

  1. data: The bitmap data

  2. data_size: The size of the bitmap

  3. header: The bitmap header (like the bitmap dimensions)

(How do we define a Struct and its fields? We'll find out later)

7.2 Load the bitmap data

What's 0.bin? Why do we use it with include_bytes?

//  Load the bitmap file "0.bin" as the bitmap data field
data: 
    include_bytes!("../bitmaps/0.bin") 
        as *const u8

0.bin is the binary file that contains the hand-drawn bitmap for the digit 0. (Encoded in a special RGB565 format... Which we'll see later)

The file is located in the bitmaps source folder...

Watch Face Files

To embed the contents of 0.bin into our source file (at src/lib.rs), we call include_bytes.

Thus the field data will be compiled literally as...

//  include_bytes will be expanded like this...
data: 
    &[ 0x00, 0x01, 0x02, ... ]  //  Reference to a byte array
        as *const u8            //  Cast to a C Pointer

(Yep it looks like #include from C... But works on binary files. Nifty!)

Why *const u8?

Our bitmap Struct will be passed to a C Function... So we need to convert Rust References to C Pointers.

We write as *const u8 to convert the binary contents of 0.bin from a Rust Reference to a C Pointer.

Why is there a "!" after include_bytes?

Because include_bytes is a Rust Macro (not a Rust Function). It's interpreted by the Rust Compiler while compiling our Rust code.

Rust makes it so breezy easy to embed binary files (like bitmaps) into our Watch Face... Thanks to include_bytes!

More about include_bytes

7.3 Set the bitmap size

We've seen how the data field is loaded from the bitmap file 0.bin: src/lib.rs

//  Create the bitmap struct for the digit 0
img::lv_img_dsc_t {
    //  We have seen this...
    data: include_bytes!("../bitmaps/0.bin") as *const u8,
    //  Now let's do data_size and header
    data_size,
    header
}

We move on to the next field data_size, the number of bytes in the bitmap file.

data_size is computed like so: src/lib.rs

/// Width of each image and bitmap
const IMAGE_WIDTH: u32  = 80;

/// Height of each image and bitmap
const IMAGE_HEIGHT: u32 = 100;

/// 2 bytes per pixel, in RGB565 format
const BYTES_PER_PIXEL: u32 = 2;

//  Compute the image size
let data_size = IMAGE_WIDTH * IMAGE_HEIGHT * BYTES_PER_PIXEL;

(u32 means unsigned 32-bit integer)

Our bitmap 0.bin is 80 pixels wide and 100 pixels wide.

In RGB565 Encoding, each pixel is represented by 2 bytes of colour data.

So data_size works out to 80 * 100 * 2 or 16,000 bytes.

There's something odd about data_size... Shouldn't it be:

//  Create the bitmap struct for the digit 0
img::lv_img_dsc_t {
    //  Set the value of data_size
    data_size: data_size,
    ...

That's a handy short form in Rust... When creating Structs, we may omit the value if it has the same name as the field.

So the above code may be simplified as...

//  Create the bitmap struct for the digit 0
img::lv_img_dsc_t {
    //  Short form of `data_size: data_size`
    data_size,
    //  Short form of `header: header`
    header,
    ...

Let's talk about header, the header for our bitmap...

7.4 Set the bitmap header

According to the definition of the lv_img_dsc_t Struct in LVGL, we need to provide a header field that describes...

  1. Colour format of our bitmap (i.e. True Color)

  2. Width of our bitmap (i.e. 80 pixels)

  3. Height of our bitmap (i.e. 100 pixels)

We create the header like so: src/lib.rs

//  Compose the image header
let mut header = img::lv_img_header_t::default();
header.set_cf(img::LV_IMG_CF_TRUE_COLOR);  //  Color Format
header.set_w(IMAGE_WIDTH);                 //  Width
header.set_h(IMAGE_HEIGHT);                //  Height

7.5 Load all 10 bitmaps

We have seen this code for loading the bitmap for the digit 0...

//  Create the bitmap struct for the digit 0
img::lv_img_dsc_t {
    //  Load the bitmap bytes
    data: include_bytes!("../bitmaps/0.bin") as *const u8,
    //  Set the bitmap size
    data_size,
    //  Set the bitmap header
    header
}

Let's load all 10 bitmaps, for digits 0 to 9: src/lib.rs

//  Load the bitmaps
bitmaps: [
    img::lv_img_dsc_t { data: include_bytes!("../bitmaps/0.bin") as *const u8, header, data_size },
    img::lv_img_dsc_t { data: include_bytes!("../bitmaps/1.bin") as *const u8, header, data_size },
    img::lv_img_dsc_t { data: include_bytes!("../bitmaps/2.bin") as *const u8, header, data_size },
    //  Omitted: Bitmaps 3 to 8
    img::lv_img_dsc_t { data: include_bytes!("../bitmaps/9.bin") as *const u8, header, data_size },
]

bitmaps is the array of bitmaps that we have used earlier.

This code appears at the end of the new method for creating our Watch Face...

Map of New Method

Here We Are

Let's study the rest of the code in the new method...

7.6 Create the images

We create the top left image like so: src/lib.rs

top_left_image: {
    //  Create the top left image
    //  `?` will terminate the function in case of error
    let image = img::create(screen, ptr::null()) ? ; 

    //  Set image position to top left
    obj::set_pos(image, 40, 20) ? ;  

    //  Return the image as top_left_image
    image  //  Omit the semicolon                            
},

This form of Rust looks unusual, but think of it like this...

top_left_image: { ... ; image },

In Rust the curly brackets { ... } represent a block of code.

Every block of code in Rust evaluates to a value. Here the last line of code in the block, image, is returned as the value of the block. (Note that the semicolon ";" is omitted when we return values)

What's screen?

screen refers to the current active screen in LVGL.

screen is defined in src/lib.rs as...

//  Get the active screen
let screen = watchface::get_active_screen();

We create the top right, bottom left and bottom right images the same way: src/lib.rs

//  Create the top right image
top_right_image: {
    let image = img::create(screen, ptr::null()) ? ;
    obj::set_pos(image, 120, 20) ? ;  //  Set image position to top right
    image                             //  Return the image as top_right_image
},

//  Create the bottom left image
bottom_left_image: {
    let image = img::create(screen, ptr::null()) ? ;
    obj::set_pos(image, 40, 120) ? ;  //  Set image position to bottom left
    image                             //  Return the image as bottom_left_image
},

//  Create the bottom right image
bottom_right_image: {
    let image = img::create(screen, ptr::null()) ? ;
    obj::set_pos(image, 120, 120) ? ;  //  Set image position to bottom right
    image                              //  Return the image as bottom_right_image
},

And that's how we create images in the new Method.

The code is located in the middle of the new Method...

Map of New Method

Here We Are

7.7 Wrap them all up

We have seen various parts of the new Method... Let's wrap them all up into a proper Method Defintion: src/lib.rs

/// Create the Watch Face
fn new() -> MynewtResult<Self> {
    //  Get the active screen
    let screen = watchface::get_active_screen();

    //  Seen earlier: Compose the image header
    let mut header = ... ;

    //  Seen earlier: Compute the image size
    let data_size = ... ;

    //  Create the widgets
    let watch_face = Self {

        //  Seen earlier: Create the top left image
        top_left_image: { ... },

        //  Seen earlier: Create the top right image
        top_right_image: { ... },

        //  Seen earlier: Create the bottom left image
        bottom_left_image: { ... },

        //  Seen earlier: Create the bottom right image
        bottom_right_image: { ... },

        //  Seen earlier: Load the bitmaps
        bitmaps: [ ... ],
    };
    //  Return the watch face
    Ok(watch_face)
}

The new Method returns MynewtResult<Self>...

/// Create the Watch Face
fn new() -> MynewtResult<Self> { ...

Which means that the method returns either a Watch Face (i.e. Self) or an error (i.e. Err)

Why does Self refer to the Watch Face?

We'll find out in a while.

TODO

/// Create the Watch Face
fn new() -> MynewtResult<Self> {
    ...
    //  Create the widgets
    let watch_face = Self {
        ...
    };
    //  Return the watch face
    Ok(watch_face)
}

Map of New Method

Here We Are

8 WebAssembly Rust

TODO

9 Embedded Rust

TODO

Your Own Watch Face

10 What's Next

TODO

Lemme know if you're keen to help! :-)

In the meantime, please go right ahead to create your own Watch Faces and publish them on crates.io... So that all PineTime Owners can share, learn and enjoy :-)

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here...

pinetime-rust-mynewt/rust/ app/src/handdrawn.md