Monday, July 2, 2018

Tutorial: Working with Menus, Buttons, Encoders

Way back in the Hello World post, like three weeks ago, I promised that I'd cover menu generation. Since then, I've decided that my own apps wouldn't use menus. But, you know, a deal's a deal, and using O_C's native menu generation system is an important part of development.

In this post, I'll be referring back to the last tutorial, about saving app settings. The reason for this is that the menu system dovetails very nicely with the setting system. The work that you do to declare data types, ranges, potential values, and names of settings also goes toward generating the menu pretty much automatically. If you need to whip some CV processing up quickly, but don't want to mess around with building UI from scratch, you're in luck.

Please pull the Tutorial repo at the usual place, and follow along in the new APP_LOGIC.ino file.

The App

The application is a simple 4-channel logic app. Each channel takes input from two digital inputs, processes it with one of six logic gates (and, or, xor, nand, nor, xnor), and outputs the result to the corresponding channel.

For each output channel (A, B, C, D), the user chooses a logic gate operation and selects which digital inputs are used for the calculation. The selection follows conventional O_C functionality: the right button switches between selection of the parameter and editing, while the right encoder scrolls through the parameters or selects values. The underlying framework asks you to code the functions of the controls, but the data management and screen handling (switching, scrolling, etc.) is handled for you.

Step 1: Set up the enum containing the settings

Don't want to dwell on the first step too much, because we've seen this stuff before. The enum is a list of labels for settings, with a last setting marker:


See also the LOGIC class definition, which uses the SettingsBase parent class.

Step 2: Set some value lists

In the last tutorial, I used settings strings from OC::Strings. This time, I'm defining my own settings strings, which will be displayed on the screen and used in Step 3:

const char* const gates[6] = {
  "AND", "OR", "XOR", "NAND", "NOR", "XNOR"

const char* const input_pairs[3] = {
  "1,2", "2, 3", "3,4"

Step 3: Declare the settings

In the last tutorial, the settings declaration was only used to identify what would be saved. This time, the declarations will actually be used for displaying and validating menu values, so it's important to set ranges and names up as you want them to function:

    { 0, 0, 5, "Logic Gate A", gates, settings::STORAGE_TYPE_U8 },
    { 0, 0, 2, "Source for A", input_pairs, settings::STORAGE_TYPE_U8 },
    { 0, 0, 5, "Logic Gate B", gates, settings::STORAGE_TYPE_U8 },
    { 0, 0, 2, "Source for B", input_pairs, settings::STORAGE_TYPE_U8 },
    { 0, 0, 5, "Logic Gate C", gates, settings::STORAGE_TYPE_U8 },
    { 0, 0, 2, "Source for C", input_pairs, settings::STORAGE_TYPE_U8 },
    { 0, 0, 5, "Logic Gate D", gates, settings::STORAGE_TYPE_U8 },
    { 0, 0, 2, "Source for D", input_pairs, settings::STORAGE_TYPE_U8 }

Step 4: The Cursor

menu::ScreenCursor<menu::kScreenLines> cursor;

You'll provide this as a public property in your app's class somewhere. Some native apps put it in a struct. The idea is that it needs to be available, because it handles lots of stuff, as we'll soon see.

In this case, it's in the LOGIC class, and will be referred to via logic_instance as logic_instance.cursor. If you want to hide it in a private property and create a getter for it, knock yourself out.

Step 5: Initialize the cursor in Init()

This tells the cursor which parameter to start with (from the enum generated in Step 1), and which parameter to end with. For a simple menu, you can just provide the starting and ending points of the entire list of settings. But your app may have different needs.

void LOGIC_init() {
    logic_instance.cursor.Init(LOGIC_SETTING_OPERATION_A, LOGIC_SETTING_LAST - 1);

Step 6: Drawing the menu

To draw the menu, create a SettingsList and then iterate through its available() method until you've reached the end of the settings. The pattern below will get you a conventional O_C menu, and you really don't need to do much else, other than provide logic that might show or hide settings conditionally. But you don't need me for that.

void LOGIC_menu() {

    menu::SettingsList<menu::kScreenLines, 0, menu::kDefaultValueX - 1> settings_list(logic_instance.cursor);
    menu::SettingsListItem list_item;
    while (settings_list.available())
        const int current = settings_list.Next(list_item);
        const int value = logic_instance.get_value(current);
        list_item.DrawDefault(value, LOGIC::value_attr(current));


Step 7: Setting the button and encoder behaviors

You should have some idea about how to handle button and encoder events, as this topic has been covered in the Pong Game tutorial. However, looking back, I sort of glossed over that, so we'll talk about it a bit here. First, here's the button event handler:

void LOGIC_handleButtonEvent(const UI::Event &event) {
    if (event.control == OC::CONTROL_BUTTON_R) {
        if (event.type == UI::EVENT_BUTTON_PRESS) {

Whenever a button is pressed, and the screensaver is not active, the APPNAME_handleButtonEvent() function is called, passing a UI::Event reference.

An Event has a few readable properties, two of which are relevant to button presses:

event.control: Indicates which button was pressed. OC::CONTROL_BUTTON_R and OC::CONTROL_BUTTON_L are the encoders, and OC::CONTROL_BUTTON_DOWN and OC::CONTROL_BUTTON_UP are the up/down buttons next to the screen.

event.type: Indicates whether the button was pressed (UI::EVENT_BUTTON_PRESS), or long-pressed (UI::EVENT_BUTTON_LONG_PRESS). Note that a long press of the up button activates the screensaver, and a long press of the right encoder goes to the app selection menu. These events will never be intercepted by an app because they are overridden by the framework.

In the handler above, when the right button is pressed, it toggles the editing mode of the app instance's cursor, between parameter selection and editing. You don't need to do anything special to handle the new state.

Encoder handling works in a similar way:

void LOGIC_handleEncoderEvent(const UI::Event &event) {
    if (event.control == OC::CONTROL_ENCODER_R) {
        if (logic_instance.cursor.editing()) {
            logic_instance.change_value(logic_instance.cursor.cursor_pos(), event.value);
        } else {

Here, the event.control is set to either OC::CONTROL_ENCODER_R or OC::CONTROL_ENCODER_R, indicating which encoder was turned.

Instead of event.type, an encoder event's behavior is read with

event.value: Indicates whether the encoder was turned clockwise (1) or anti-clockwise (-1). The O_C system has the ability to fire multiple encoder events when the encoder is turned fast. Your code doesn't need to worry about this. You can just assume that you're handling changes one increment at a time.

The handler above checks the cursor.editing() mode to decide what to do. In editing mode, it changes the value at the cursor position. This is nice and automatic: it checks the appropriate range (from SETTINGS_DECLARE() in Step 3) and updates the values in the app class.

If editing mode is off, it scrolls through the menu items. Again, you don't need to specifically handle the scrolling. The cursor and SettingsList (Step 6) handle all that for you.

That's it for today!

A Word about Hemisphere

It was nice to see the support of the Hemisphere project. The Hemisphere introduction dwarfed readership of the next most popular post by over 5,000 views. At this point, Hemisphere is ready for beta testing. So if you'd like to try it out, let me know by IM'ing me at MuffWiggler (I'm chysn) and I'll send you a link to the hex file.

In the coming days, I'll be producing a series of one-minute videos, one for each applet. That rollout will be my next blog post.

No comments:

Post a Comment

Pitch Calculation and Output

"If pitches were horses, we'd all be eatin' steak." --Jayne Cobb And where have I been for almost two months? I was busy...