Flipper your way into hardware - UI


Read more in the "Flipper into hardware" series:

  1. Flipper your way into hardware
  2. Flipper your way into hardware - Stepping
  3. Flipper your way into hardware - UI

Let me read it for you!

At this point, we have a Flipper Zero application that steps a stepper motor. But, we want something that:

  1. Looks pretty
  2. Is configurable

So, we’ll (try to) make that here.

Requirements

Before, we were just exploring what it takes to write a Flipper Zero app that interacted with the pins - namely to step a stepper motor. Now we’ll take that groundwork and use it to make a somewhat functional app. What could we want to do with our app?

  1. Change the pins used to step and set direction of the motor
  2. Start and stop the motor

There may be some other stuff, but I’m honestly quite lazy when it comes to this “UI” thing. We just want to configure things and make the motor function.

But first, we need a different UI.

The Goal UI

The UI is a single view right now, but the Flipper Zero API gives a few more options. The first place that I looked at was submenus. These are just collections of options that you can press. Seems perfect, so we’ll have one for each option:

Goal UI

+---------------+
| GO!           |
| Direction pin |
| Step pin      |
+---------------+

It’s beautiful. Both of the pin options should be fairly easy since the official GPIO app has a view that selects pins - similar logic can be used here. That will definitely work out.

Then “GO!” will step the motor until back is pressed.

We will start with making this menu, then get selecting pins working.

The submenu

We will have a submenu with multiple options to go to different “views.” This is easily made with a view dispatcher - this will hold all of the views and switch between them when needed. To set this up, I used the view dispatcher example as a reference.

I won’t go into every necessary part for code snippets, if you need completeness, simply look at the code on Github.

First, we need to change the StepperContext. We removed the ViewPort and FuriMessageQueue and replaced them with the ViewDispatcher. Now the stepper context object (which is passed around a bunch of callbacks) is now this:

stepper.c

typedef struct {
  Gui *gui;
  ViewDispatcher *view_dispatcher;
  Submenu *submenu;
  Widget *stepping_widget;
  const GpioPin *dir_pin;
  const GpioPin *step_pin;
  bool state;
} StepperContext;

The stepping_widget will be used for the “GO!” view - our first view.

We need to change the stepper_context_alloc function to set up the view dispatcher and its views. To do that, remove the references to removed members (namely the ViewPort and FuriMessageQueue). Then, we’ll start by creating the submenu:

stepper.c

  // Create and initialize the Submenu view.
  context->submenu = submenu_alloc();
  submenu_add_item(context->submenu, "GO!", SubmenuIndexStep,
                   stepper_dispatcher_app_submenu_callback, context);

There’s a SubmenuIndexStep in there - that’s actually an enum value defined at the top:

stepper.c

// Enumeration of submenu items.
typedef enum {
  SubmenuIndexStep,
} SubmenuIndex;

Since we’re starting with just the step menu item, that enum only has one value, but we’ll add more when we build the submenu up.

Next we’ll create the “widget” that gets triggered when we switch to stepping mode:

stepper.c

  // Create the stepping widget.
  context->stepping_widget = widget_alloc();
  widget_add_string_multiline_element(context->stepping_widget, 64, 32,
                                      AlignCenter, AlignCenter, FontSecondary,
                                      "Stepping!");
  widget_add_button_element(context->stepping_widget, GuiButtonTypeCenter,
                            "Stop stepping", stepping_button_callback, context);

That’s just a string and a button. Pressing the button makes it stop stepping.

Now we’ll create the view dispatcher. This just manages the two views we just created:

stepper.c

  context->view_dispatcher = view_dispatcher_alloc();
  view_dispatcher_attach_to_gui(context->view_dispatcher, context->gui,
                                ViewDispatcherTypeFullscreen);
  view_dispatcher_add_view(context->view_dispatcher, ViewIndexSubmenu,
                           submenu_get_view(context->submenu));
  view_dispatcher_add_view(context->view_dispatcher, ViewIndexStepping,
                           widget_get_view(context->stepping_widget));
  view_dispatcher_set_custom_event_callback(context->view_dispatcher,
                                            stepper_event_callback);

  // Set the navigation, or back button callback. It will be called if the
  // user pressed the Back button and the event was not handled in the
  // currently displayed view.
  view_dispatcher_set_navigation_event_callback(context->view_dispatcher,
                                                navigation_callback);

  // The context will be passed to the callbacks as a parameter, so we have
  // access to our application object.
  view_dispatcher_set_event_callback_context(context->view_dispatcher, context);

The ViewIndexSubmenu and ViewIndexStepping are just part of an enum defined above:

stepper.c

typedef enum {
  ViewIndexSubmenu,
  ViewIndexStepping,
} ViewIndex;

The remaining parts are just adding those views and adding callbacks. The first callback is stepper_event_callback - we’ll see this later. The navigation_callback is simpler - it’s basically straight from the example. If we press back, and the app screen doesn’t handle it, the navigation callback will exit. We’ll see both of those soon, but first we have a couple more setup points.

This needs to be called from the “main” application code. After setting up the GPIO pins, we can “enter” the app easily with these calls:

stepper.c

  view_dispatcher_switch_to_view(context->view_dispatcher, ViewIndexSubmenu);
  view_dispatcher_run(context->view_dispatcher);

This will start at the submenu and run it. Then, we need to tear this down in stepper_context_free:

stepper.c

  view_dispatcher_remove_view(context->view_dispatcher, ViewIndexSubmenu);
  view_dispatcher_remove_view(context->view_dispatcher, ViewIndexStepping);
  view_dispatcher_free(context->view_dispatcher);
  submenu_free(context->submenu);

Okay, enough just rahashing the example. Now for new stuff.

The Callbacks

So we have four callbacks that we assigned:

  1. navigation_callback - called when “back” is pressed

  2. stepper_dispatcher_app_submenu_callback - called when a submenu button is pressed

  3. stepping_button_callback - called when the “Stop stepping” button is pressed in the widget

  4. stepper_event_callback - called by stepper_dispatcher_app_submenu_callback in order to start stepping

Now, it would be boring to show all of the code, so I’ll just show the interesting parts in each.

In navigation_callback, we exit with view_dispatcher_stop(context->view_dispatcher); - then it will return to where it was called from:

stepper.c

  // navigation_callback

  // Back means exit the application, which can be done by stopping the
  // ViewDispatcher.
  view_dispatcher_stop(context->view_dispatcher);

stepper_dispatcher_app_submenu_callback actually triggers a different event conditionally, based on the submenu “index”:

stepper.c

  // stepper_dispatcher_app_submenu_callback

  if (index == SubmenuIndexStep) {
    view_dispatcher_send_custom_event(context->view_dispatcher,
                                      ViewIndexStepping);
  }

Which triggers callback number 4 - we’ll get to that.

stepping_button_callback has a couple of extra checks for a button getting pressed, provided by the example, in order to make sure it was a simple “press”:

stepper.c

  // stepping_button_callback

  // Only request the view switch if the user short-presses the Center button.
  if (button_type == GuiButtonTypeCenter && input_type == InputTypeShort) {
    // Request switch to the Submenu view via the custom event queue.
    view_dispatcher_send_custom_event(context->view_dispatcher,
                                      ViewIndexSubmenu);
  }

Finally, the big one. stepper_event_callback actually has a few points worth mentioning.

Back to Stepping

Here’s the issue: if we just block like before, we no longer have the same loop structure with the event queue to check what events are handled. Instead, the view dispatcher handles events for us. So, once we enter, how would we exit? If we just naively loop, we won’t leave.

We’re already being somewhat imprecise about timing (namely by putting a lot of logic in the loop), so we aren’t going to get the exact wait time that we want. Because of this, we have some freedom here to not reinvent anything. We’ll do this with another callback!

The best reference I found for this is in the Javascript documentation. In that example, the Javascript code creates an event that fires a second after creation:

// create an event source that will fire once 1 second after it has been created
let timer = eventLoop.timer("oneshot", 1000);

// subscribe a callback to the event source
eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) {
    print("Hello, World!");
    eventLoop.stop();
}, eventLoop); // notice this extra argument. we'll come back to this later

We’re doing something similar here, but in C. In our custom event, we want to set a timer that runs every N ticks. Since we don’t really care about absolute precision here, we’ll use furi_event_loop_tick_set. This can be done like so:

stepper.c

    furi_event_loop_tick_set(
        view_dispatcher_get_event_loop(context->view_dispatcher), 3,
        stepper_tick_callback, context);

And you can stop the timer in the same function with:

stepper.c

    furi_event_loop_tick_set(
        view_dispatcher_get_event_loop(context->view_dispatcher), 0, NULL,
        context);

Which one gets called depends on the event - if it’s ViewIndexStepping, then we want to start stepping. If not, we want to stop stepping.

The callback stepper_tick_callback just ticks the motor once:

stepper.c

static void stepper_tick_callback(void *ctx) {
  furi_assert(ctx);
  StepperContext *context = ctx;
  furi_hal_gpio_write(context->step_pin, true);
  furi_delay_us(10);
  furi_hal_gpio_write(context->step_pin, false);
}

With all of this, we have… a button that says “GO!” - if you press it, it steps the motor, if not, it doesn’t. Incredible.

Choosing the Pin

Now, we want two more submenus, one for each pin (step and dir). Those are pretty easy, I just made an enum like this which will change the pin in each case in a callback:

stepper.c

// Each pin that can be used
typedef enum {
  PinA7,
  PinA6,
  PinA4,
  PinB3,
  PinB2,
  PinC3,
  PinC1,
  PinC0,
} UsablePins;

Okay, I avoided using the GPIO examples. It’s simply annoying - if we started from the example, it would have been straightforward, but I like learning about this myself. Instead, we get more submenus, my favorite!

I won’t actually go through the submenu process again, but we did it twice more: once for the step pin, once for the direction pin. These pins don’t have extra validation - they may be the same. I don’t want to know what happens if they are.

Here’s what the app looked like after the two extra submenus:

Home

Pin select

UI Frustration

Developing for the Flipper Zero is enjoyable. I really liked using micro Flipper Build Tool. GPIO worked as I would expect. But the UI development just stood in my way at every step. I wanted to do more - this was supposed to be a legitimate way for me to test stepper motors under various conditions! It’s just tiring. I’m tired.

If you want to make a real application, go for it! I’m just not great at making anything pretty, so I ended up getting pretty frustrated. Feel free to try Javascript as well! It wasn’t an option when I started this, and I know C better.

All in all, the Flipper Zero was fun to mess with. Making an application was rewarding. But, the UI just kept getting in my way. I don’t think I’m cut out for screens.

Remember, the source code for all of this is on my Github. Be warned, it should not be used as an example for how to do things right.