Wojtek @suda Siudzinski


Python/Node/Golang/Rust developer, DIY hacker, rookie designer, 3D print junkie. CEO @ Gaia Charge


React/Flux like pattern for embedded UI

When building desktop applications, whether it's with native frameworks or with web ones, we take for granted how easy it is to handle. I'm in the process of building a portable synthesizer that runs on Particle Xenon with limited resources and on a very slow SPI screen which made me appreciate web frameworks a bit more.

The problem

The problem I had, boiled down to figuring out a straightforward (and fast) way to reconcile many inputs/outputs.

Many to many

There are many ways to go about it but I wanted a clear and straightforward way of describing what happened and how to react. The last word of the previous sentence might give you some indication about what I went with ;)

React and Flux

React is a popular frontend library, enabling reusable, reactive components for the user interface. Flux is an architecture of how events should flow through the system, that complements React. Note that Flux isn't a library but rather a concept that libraries like Redux took and implemented. But for the purpouse of this project, Flux was simple enough.

Just to be clear, I didn't implement React/Flux in C but rather used some of the principles to develop a pattern. From React, I took partial rendering of only the things that changed and from Flux the idea of actions, stores and dispatchers.

Actions, stores, views and dispatchers

Flux action flow

There's a great explanation of all those principles in Flux's documentation. I made some adjustments, mostly to accomodate limitations of the microcontroller and statically typed C. What I ended up with is:

Actions

To identify each Action, they are being defined with unique identifier, so when they are dipatched, they can be passed to the correct View.

#define ACTION_INCREMENT_COUNTER 1

Stores

A Store (rather than many stores) was implemented as a struct:

struct Store {
  uint32_t counter;
}

#define STORE_PROP_COUNTER 1

The Store is being shared between Views mostly to allow mixing global state with local one. Additionally this allows a easy way to serialize/deserialize and store it in EEPROM.

For each Store property there's a #define that is used to indicate which property has been changed.

Views

A View is just a class that implements two methods:

  • void handleAction(uint8_t action, int16_t args[])
  • void handleStoreUpdate(uint8_t storeProp)

If the View is currently active (there's an assumption that only one View can be active at the time), it will receive all the dispatched Actions. The handleAction method should decide if it will react to it. If it does, it can dispatch another Action or modify the Store. If it does update the Store, it should call handleStoreUpdate() with respective Store property, to update the UI.

An example action handler might look like this:

void MyView::handleAction(uint8_t action, int16_t args[]) {
  switch (action) {
    // For the increment action...
    case ACTION_INCREMENT_COUNTER:
      // increment the counter in the store by the first argument
      this->store->counter += args[0];
      // Update the UI that displays the counter
      this->handleStoreUpdate(STORE_PROP_COUNTER);
      break;
  }
}

and the update handler like this:

void MyView::handleStoreUpdate(uint8_t storeProp) {
  switch (storeProp) {
    // For the counter property...
    case STORE_PROP_COUNTER:
      // ..update the UI
      this->screen->drawCounter();
      break;
  }
}

Dispatchers

A Dispatcher is a main class that contains the list of Views including which of them is currently active and routes all dispatched Actions to it.
Actions can take arguments and for simplicity, it's an array of four int16_t. To make calling easier, there are two macros to help:

#define MK_ARGS(name, arg0, arg1, arg2, arg3) int16_t name[] = {arg0, arg1, arg2, arg3};
#define NO_ARGS(name) MK_ARGS(name, 0, 0, 0, 0)

the idea behind this is to change the macros if the type (or number) of arguments changes instead of changing each piece of code that dispatches an Action.

To dispatch an Action the dispatchAction() method has to be called:

MK_ARGS(args, 1, 0, 0, 0)
dispatcher.dispatchAction(ACTION_INCREMENT_COUNTER, args);

all of this can be summarized in one, nifty chart:

Flow chart

Where next?

I developed the pattern above while working on my ps-01 synthesizer and it's the main place where a "reference implementation" lives.

I also made a video going through the early iteration of this pattern (where View is called a Page and Dispatcher an UI):

The slides from the video are also available on SpeakerDeck.

If you have any comments or ideas about this pattern, please let me know in the comments (or on social media) 🙂

comments powered by Disqus