Theory: Versatile GUI with a Rotary Encoder

Good programming is, in my opinion, as general as it can be without impacting the performance of your particular application. I haven't found much in the way of versatile libraries for GUI though, so I'm trying to build my own...

I've been grappling with ways to create a rotary-encoder-driven GUI on an atmega328p for the last few weeks. I'm specifically working with the AdafruitGFX library on an SSD1306, intending to make a simple GUI to control a reflow oven (with the ability to create, edit, and name profiles to be saved/loaded from EEPROM) but this discussion isn't really limited to that, and should also be applicable to touchscreens or other input methods.

Here's a short list of frustrations I've unearthed while I work on this:

1: Defining a button as a struct in Progmem is really hard to manage because Arduino does not allow you to use lambda expressions to put function pointers in progmem.

typedef (*funcPtr)();
const funcPtr P PROGMEM= (funcPtr) [] () {delay(1);};

will just yield P = 0x00 instead of any meaningful address, so if you want to put callbacks on buttons and store those buttons in progmem then you need to create a unique, named function for every single button in your menu. That makes your code turn into a really massive, unorganized mess. I have no good solution for this.

2: Defining a button as a struct in ram is much easier to work with but if the buttons have display text of any substantial length, you'll run out of RAM too fast to make anything sophisticated.

2: Singleton objects are really hard to avoid without storing a bunch of redundant pointers in memory. If you want your general-purpose GUI to control physical resources like the digital pins, it looks like the only way to avoid hard-coding that functionality into the GUI is to either pass in functions to global pointers or maybe just create an object describing that physical interface and pass a reference to that object into the GUI. Both of these strike me as inelegant.

What is your ideas or practices on building a general purpose graphical interface? Have you worked with a library you would recommend?

Just grab a touch screen and make it simple for the user, and yourself.

-jim lee

For a specific application, I did once write a navigation bar class with buttons. This would optionally occupy the bottom of a touch screen. Buttons could be added or removed dynamically at run time. It was quite simple in concept. I used an ESP8266 so I was not so sensitive about resource usage.

// For example, Create a button bar object:
NavBar tmNavBar( NAVBARBOTTOM ) ;

// create a button bar with text, callback routines, and visibility settings, then display it :
tmNavBar.UpdateNavBarItem( "Offline" ,      fNetworkMode_cbr ,  0, config.networkMode ) ;
tmNavBar.UpdateNavBarItem( "Online" ,       fNetworkMode_cbr ,  1, ! config.networkMode ) ;
tmNavBar.UpdateNavBarItem( "Return" ,        tmConfMain_root ,  2, true ) ;
tmNavBar.Show() ;

// for a menu heirarchy, just create numbered buttons, with text above with numbers.
// The same callback routine is used, but is passed a parameter equal to the button pressed.

tft.println(F(" Main Config Menu")) ;
tft.println(" ") ;
tft.println(F(" 1. Set Debug Options")) ;
tft.println(F(" 2. Set Online/Offline")) ;
tft.println(F(" 3. Set Online Conf (AP)")) ;
tft.println(F(" 4. View Conf")) ;
tft.println(F(" 5. Restart")) ;
tft.println(F(" 6. Exit")) ;

tmNavBar.UpdateNavBarItem(  " 1" , tmConfMain_cbr , 1, true ) ;
tmNavBar.UpdateNavBarItem(  " 2" , tmConfMain_cbr , 2, true ) ;
tmNavBar.UpdateNavBarItem(  " 3" , tmConfMain_cbr , 3, true ) ;
tmNavBar.UpdateNavBarItem(  " 4" , tmConfMain_cbr , 4, true ) ;
tmNavBar.UpdateNavBarItem(  " 5" , tmConfMain_cbr , 5, true ) ;
tmNavBar.UpdateNavBarItem(  " 6" , tmConfMain_cbr , 6, true ) ;
tmNavBar.Show() ;

I published the whole application here, a telephone caller id system, which includes the NavBar class: https://forum.arduino.cc/index.php?topic=528459.0
(see Screen.h/.cpp and the .ino file for usage examples)

1: Defining a button as a struct in Progmem is really hard to manage because Arduino does not allow you to use lambda expressions to put function pointers in progmem.

don’t understand this comment. using a function pointer suggests that it is a not a constant and needs to be in RAM. code can’t access data directly in PROGMEM, see Progmem - Arduino Reference

i recently created a oled based menu using buttons

menu.h (2.28 KB)

menus.cpp (2.88 KB)

menus.h (66 Bytes)

gcjr:
...using a function pointer suggests that it is a not a constant and needs to be in RAM. code can't access data directly in PROGMEM...

Constant pointers are still useful. In my particular situation, a button was defined as something like:

typedef void (*Callback)()
struct Button {
  Callback cb;
  char text[10];
  byte x;
  byte y;
};

Which means that you could then execute an arbitrary function determined by the button:

Callback cb = (Callback) pgm_read_word( &btn[selIndex]->cb );
cb();
//you could even use memcpy_P to copy the entire button into ram if you need quick access to all of its members

So now instead of having to do some kind of enormous switch statement to figure out which function needs to be executed based off of which button was clicked, we can just specify the callback for each button in that button's definition.

To make the code even more organized, you can use a lambda expression to make the button callbacks point to anonymous functions, so that your button declaration could be this simple:

const Button example PROGMEM = {
  [](){
    /*what the button ought to do*/
  },
  "display",
  10,30
};

But like I mentioned, lambda statements don't work for putting function pointers into progmem so this trick only works if you want to dump a lot of RAM at your problem.

in the code i posted, there are table entries for each menu element that includes pointer to functions for each button. i guess these table could be stored in PROGMEM

// button and display function pointers
typedef struct {
    void (*menu) (void *);
    void (*sel)  (void *);
    void (*up)   (void *);
    void (*dn)   (void *);
    void (*disp) (void *);
} Func_t;

// parameter descripton
typedef struct {
    const char *text;
    int        *param;
    int         min;
    int         max;
    void       *p;
    Func_t  func;
} P_t;

i don't understand why lamda functions are necessary?

gcjr:
i don't understand why lamda functions are necessary?

Syntactic sugar.