How to create a user menu structure

Hello,

I'm still working on my Arduino trip computer project using an ESP32, and right now, I am trying to figure out how to best create a user selectable menu structure in C code.

Here's what the user menus will look like, see the image on the right:

I would like to implement the following menu structure:

  • four main menu categories: fuel, engine, speed, user settings
  • each of these main menu categories will have (up to) three screens, i.e. submenus, numbered from I to III.
  • each of these submenus will show two values, which can be int or float. (With the exception of the "settings" category, which will have various different things that the user can modify).

So far, I was thinking maybe go with a struct that defines the contents of each menu screen, like so:

typedef struct {
  const uint16_t*  header_left_part;
  const uint16_t*  menu_category_icon;
  const uint16_t*  submenu_number_icon;
  const uint16_t*  first_displayed_value_icon;
  const uint16_t*  second_displayed_value_icon;
  const char* firstValue;
  const char* secondValue;

} menuStruct;

I would then create menuStruct arrays for each possible submenu screen, in which the icons will be listed that are to be displayed on a particular submenu screen (all of them stored im PROGMEM), plus the two values that I want shown on that menu page. Because these values can be either float or int depending on the screen that is shown, maybe convert either values into char arrays and then have them printed on the screen that way.

Any thoughts on that approach?

carguy:
Any thoughts on that approach?

const uint16_t*  first_displayed_value_icon;

the icon is stored how?

const char* firstValue;

Depending on how your data is structured, using a string for the values can be easier to work with and you may be able to overload a displayfunction depending on wether that particular spot on the display is a reference to a float or to an int

footnote:

typedef struct

this is C++; you don't need the typedef keyword, enums structs and unions are automatically types.

Here's an example of an icon:

const uint16_t l100_d_mki[700] = {

  0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xce79, 0x31a6, 0x18e3, 0xffdf, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
  0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x8c51, 0x0000, 0x39e7, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,

// yadayadayada...

  0xffff, 0xb596, 0x4228, 0x1082, 0x0000, 0x2124, 0x4228, 0xef5d, 0xffff, 0xffff, 0x4a49, 0x0000, 0x2124, 0x5acb, 0x0861, 0x0000, 0x9492, 0xffff, 0xffff, 0x4a49, 0x0000, 0x2124, 0x5acb, 0x0861, 0x0000, 0x9492, 0xffff, 0xffff,
  0xffff, 0x9492, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xdefb, 0xffff, 0xffff, 0xf79e, 0x52aa, 0x0000, 0x0000, 0x0861, 0x8c51, 0xffff, 0xffff, 0xffff, 0xf79e, 0x52aa, 0x0000, 0x0000, 0x0861, 0x8c51, 0xffff, 0xffff, 0xffff
};

The icons are all in 16-bit color. I have found that only anti-aliased icons in 16-bit color give me the kind of display quality that I want. And with the ESP32's comparatively vast memory, there aren't many constraints to this approach.

BulldogLowell:
Depending on how your data is structured, using a string for the values can be easier to work with and you may be able to overload a displayfunction depending on wether that particular spot on the display is a reference to a float or to an int

Let's see... the "big numbers" on the screen will all have between one and four digits. Things like engine oil temperature will have a range between -20°C and +170°C (no plus sign for positive temperatures) and will be gathered from the sensors as integers (tapping into the car's own oil temp sensor, for example, which is a simple thermistor, via analogRead).

Fuel consumption will be given either as miles per gallon or liters per 100 km. Both will probably be displayed as floats with one decimal, and that decimal will be rounded to the nearest 0.5.

I am using a custom made text printing function which handles any and all text on the screen. It allows some basic text formatting functionalities, like align (left, center, right), margins, letter spacing and defining the width of empty spaces:

#ifndef FONT_LIBRARY_H
#define FONT_LIBRARY_H

#include <stdint.h>
#include <string.h>

#include "fonts_def.h"

// Main text printing function
void printText(const char *myText, const fontPointer* font, byte align, byte margin, int y_start, int letterSpacing, int emptySpaceWidth, uint16_t blank_color) {

  int textLength = strlen(myText);

  const uint16_t *font_data = font->fontData;
  const fontParam* font_param = font->fontParamData;

  int characterTableLength = font->charsCount;
  int startX = 0;
  int y_height = font->fontHeight;

  /*  y parameter is calculated from bottom pixel row.
      This makes screen layouts with different fonts
      much easier.
  */

  int startY = y_start - y_height;
  uint32_t startOffset;
  uint32_t endOffset;
  bool matchFound = false;
  int xWidthCumul = 0;
  uint32_t charParams[textLength][2];

  for (int i = 0; i < textLength; i++) {
    startOffset = 0;
    endOffset = 0;
    matchFound = false;

    for (int j = 0; j < characterTableLength; j++) {
      //Looking up matching character in character table
      char currentChar = font_param[j].character[0];

      int x_width = font_param[j].characterWidth;
      endOffset += font_param[j].dataSize;

      if (myText[i] == currentChar) {
        // if match found...
        matchFound = true;

        // Calculating start offset to give us starting block of character in font data pane
        startOffset = endOffset - font_param[j].dataSize;

        /*  calculating x-axis "pixel length" of text array and
            putting data for each character in a character parameters array
            We need this in order to calculate the x starting position of a text array
        */
        charParams[i][0] = startOffset;
        charParams[i][1] = x_width;

        break;
      }
      if (!matchFound) {

        charParams[i][0] = -1;
        charParams[i][1] = emptySpaceWidth;
      }
    }
    xWidthCumul += (charParams[i][1] + letterSpacing);
  }

  // Subtracting letter spacing one time for last character of our text array
  xWidthCumul -= letterSpacing;

  // Calculating X starting point based on specified alignment
  // align left
  if (align == 0) startX = margin;
  // align center
  if (align == 1) startX = (int) ((128 - xWidthCumul) / 2);
  // align right
  if (align == 2) startX = 128 - xWidthCumul - margin;

  uint32_t startOffsetNow;
  int k = 0;

  // Draw blank rectangle in specified color first to eliminate remnants of previous text
  tft.fillRect(startX, startY, (startX + xWidthCumul), y_height, blank_color);

  while (k < textLength) {

    // Go through the charParams array one-by-one and print out characters
    startOffsetNow = charParams[k][0];
    if (charParams[k][0] != -1) tft.pushImage(startX, startY, charParams[k][1], y_height, &font_data[startOffsetNow]);

    // Move forward to next character's position
    startX += (charParams[k][1] + letterSpacing);
    k++;

    // Break if no data present
    if (startOffsetNow == NULL) break;
  }
}

// Float printing
void printFloat(float floatValue, const fontPointer* ffont, byte falign, byte fmargin, int fy_start, int fletterSpacing, int femptySpaceWidth, byte fplusminus, uint16_t fblank_color)
{

  char floatSizeBuf[6];
  String floatTmpStr = dtostrf(floatValue, 6, 2, floatSizeBuf);
  floatTmpStr.trim();

  int floatNumLen = floatTmpStr.length();
  char floatBuffer[floatNumLen];

  if (fplusminus) snprintf(floatBuffer, sizeof(floatBuffer), "%+f", floatValue);
  else snprintf(floatBuffer, sizeof(floatBuffer), "%f", floatValue);

  if (fplusminus) snprintf(floatBuffer, sizeof(floatBuffer), "%+f", floatValue);
  else snprintf(floatBuffer, sizeof(floatBuffer), "%f", floatValue);

  printText(floatBuffer, ffont, falign, fmargin, fy_start, fletterSpacing, femptySpaceWidth, fblank_color);
}

// Integer printing
void printInteger(int intValue, const fontPointer* ifont, byte ialign, byte imargin, int iy_start, int iletterSpacing, int iemptySpaceWidth, byte iplusminus, uint16_t iblank_color)
{
  char intSizeBuf[6];
  String intTmpStr = dtostrf(intValue, 6, 1, intSizeBuf);
  intTmpStr.trim();

  int intNumLen = intTmpStr.length();
  char intBuffer[intNumLen];

  if (iplusminus) sprintf(intBuffer, "%+5d", intValue);
  else sprintf(intBuffer, "%5d", intValue);

  printText(intBuffer, ifont, ialign, imargin, iy_start, iletterSpacing, iemptySpaceWidth, iblank_color);
}

#endif FONT_LIBRARY_H

Right now, these text functions turn floats and ints into chars which are then printed out via the main printText() function. But to make sure that my struct can handle either type, maybe it's best to just turn both floats and ints into char arrays, and then let my menu composing function handle the rest. There will essentially be a function that will put all the visual elements on the screen, and that function will tap into whatever struct array I specify for the contents of a particular menu screen.

carguy:
Here's what the user menus will look like, see the image on the right

How is that a menu? It doesn't look like a menu.

What do you intend to use for input? Is it a touchscreen?

As for menu design: your users might not speak "icon". For example, what does your thermometer icon mean? Temperature, yes, but temperature of what?

The bottom left temperature is the inside temperature of the passenger cabin, the right hand side temperature is that of the outside air (as hinted at by the sun and cloud icon).

For input, there will be three push buttons (or capacitive touch buttons, I haven't decided yet). They will serve as "back", "set" and "forward" controls.

I agree that icons have to be mostly self-explanatory. But personally I think I could have done worse in designing these icons. You should be able to figure out most of my icons within three to five seconds of seeing them for the first time.

"rem" stands for "remain", which in this case will be the remaining fuel in the tank in liters (there might be a "mile and gallon" mode where you will get an MPG icon instead of l/100. I haven't decided yet if I want to go to that length).

If this trip computer really ever goes into mini-production beyond the specimen that I am building for my own car, then there will almost certainly also be a leaflet explaining all the trip computer's functions and features from beginning to end. Modern-day cars with all their interactive touchscreen gadgetry also tend to come with quite sizeable manuals explaining in detail every single icon... :wink:

When I created my menu structures, I used a double linked list so I could scroll up and down through a dynamically created list. I also had a “in” pointer so I could step down to a sub menu.

Each menu item had a code that identified that it was either a menu item that pointed to a sub menu, or a “bottom” level.

I used hardware buttons to scroll up and down my list and a “click” button to return a value on the bottom level items or step in or out of menu levels. By having each menu item including a pointer to its peers and to any sub menu, moving through the menues was easy and the code did not have to know anything about the structure of the menu in advance.

The menu was dynamically created in setup() and by storing pointers to various parts if the structure that were of particular interest, I could also “right click” to short cut menus.

1 Like

I was thinking maybe make a configuration plain-text file and store it in spiffs which will look about like this (just pulling this off the top of my head right now):

appearance.showWelcomeScreen=true;
appearance.showAnalogClockOnIgnitionOff=true;
appearance.dayAndNightMode=auto;
appearance.screenBrightness=230;
appearance.soundOn=true;

setMode.milesOrKm=miles;
setMode.clock=24h;
setMode.MGFcarType=MkI;
setMode.language=EN;

sensors.connected.windscreeenWash=true;
sensors.connected.lowCoolant=true;
sensors.connected.crankshaft=true;

I would then in my code fetch the configuration settings from the txt file in spiffs and use them to display my data on my screen. If I then decide to toggle different settings via my settings menu screen, the corresponding variables in the settings.txt file will be overwritten.

It's been a long time since I've done anything remotely to do with fread and fwrite (I used it a bit of it back in my days of coding web pages in php as a side job), but I'm sure there are neat ways of doing this in C.

carguy:
but I'm sure there are neat ways of doing this in C.

and even more neat ways in C++, which you are using.

So, how about a Display class that defines panel area objects? That class can contain functions and variables that operate on references to your data.

ok let's say I actually want to store the trip computer's configuration in a SPIFFS file using a syntax like

appearance.screenBrightness=230;
appearance.soundOn=true;

How do I go about doing that, in a roundabout way?

As I said, I'm somewhat familiar with file access functions, but haven't done anything like this in C or even C++. The kind of php coding I did with plain text files mainly involved analyzing server logs or other flatfile data with the use of php.

carguy:
I was thinking maybe make a configuration plain-text file and store it in spiffs which will look about like this (just pulling this off the top of my head right now):

appearance.showWelcomeScreen=true;

appearance.showAnalogClockOnIgnitionOff=true;
appearance.dayAndNightMode=auto;
appearance.screenBrightness=230;
appearance.soundOn=true;

....

Woah, woah, woah, woah!
Try something more like:

1
1
2
230
0
....

if you don't want to make life too difficult for yourself.

But what do I know. Maybe other people here will beg to differ.

carguy:
So far, I was thinking maybe go with a struct that defines the contents of each menu screen, like so:

typedef struct {

const uint16_t*  header_left_part;
  const uint16_t*  menu_category_icon;
  const uint16_t*  submenu_number_icon;
  const uint16_t*  first_displayed_value_icon;
  const uint16_t*  second_displayed_value_icon;
  const char* firstValue;
  const char* secondValue;

} menuStruct;




I would then create menuStruct arrays for each possible submenu screen, in which the icons will be listed that are to be displayed on a particular submenu screen (all of them stored im PROGMEM), plus the two values that I want shown on that menu page. Because these values can be either float or int depending on the screen that is shown, maybe convert either values into char arrays and then have them printed on the screen that way. 

Any thoughts on that approach?

Are you sure that the elements of that struct should be constant? Including the character arrays for the numbers?

Probably better to think of a menu screen as a "how" rather than as a "what". Make a function that takes, let's say, nine arguments: the five graphics and two numbers you wish to display, along with two more numbers showing how many decimal places for each displayed number. That way, you could call it with different arguments for each menu screen.

But then, what about the settings screens? How do you intend to handle those?

In one or two of my own projects, I found it most convenient to base my design on the settings screen. Then, I could just treat a normal, "display" screen as a settings screen, but with nothing to actually set.

Consider an "old fashioned" digital watch: the kind many of us used to track the time and date before everyone got smartphones. How do you set the time on such a watch? And what does the time setting screen look like? How exactly does the time setting screen differ from a normal, time display screen?

In all seriousness, it might make more sense to "go all the way" with icons: that is, make a "number 0" icon, a "number 1" icon, and so forth, up to "number 9", and use those to display numbers. (To split a number into its digits is simple arithmetic, and requires no text manipulation.) You might also need an "infinity" icon: how many liters per 100 km when your car is idling? Once you have those icons, then you can do other things (color change? maybe a red box? I don't know how to do graphics handling) to indicate which digit and/or non-digit icon is currently being set. (Remember my "digital watch" example.)

odometer:
Are you sure that the elements of that struct should be constant? Including the character arrays for the numbers?

Right... I sort of copied and pasted that struct together as I went along, and didn't really give consideration to what I was typing there down to the last detail. But that's of course right, if I want to display variables that are actually variable, they should probably not be constant.

odometer:
In all seriousness, it might make more sense to "go all the way" with icons: that is, make a "number 0" icon, a "number 1" icon, and so forth, up to "number 9", and use those to display numbers.

That's exactly what I am doing already. All the numbers are 16-bit color PROGMEM images that are displayed using custom font libraries and the code that I posted at reply #2 further up in this thread.

Given that the ESP32 has oodles of memory, why not use that memory for stuff like that :wink:

Here's a live test screen of my trip computer from the other night... time and date are fully functional, but the "big" numbers are dummy values and don't show any actual sensor readings as yet. The issue with the temperature decimals at the bottom is fixed now.

I've fiddled with GIMP a little the last half hour, and I am thinking maybe this is a good way to structure the settings screens:

I am taking some inspiration here from my car radio (Kenwood BT-92SD), which has slightly similar looking settings menus. At least they behave in a similar way, with the arrows and the negative text.

At the moment, I am thinking that the negative effect of the characters as seen in the picture can maybe be done simply by swapping out red and white pixels when drawing the numbers. But I'll have to see how that looks. My Kenwood radio apparently uses 1 bpp font libraries, so that's naturally a lot easier for swapping out pixel colors.

I will probably keep the settings menu hidden in normal operation, i.e. when you skip through the different main menu categories "engine - fuel - speed", then "settings" will be omitted, and will only be entered if you keep the "set" button pressed for one second or something. You will probably not want to navigate through configuration settings in 16-pt font at 70 mph on the motorway :smiley:

odometer:
Woah, woah, woah, woah!
Try something more like:

1

1
2
230
0
....



if you don't want to make life too difficult for yourself.

But what do I know. Maybe other people here will beg to differ.

No, I totally see your point. All I need to store in a configuration file is a handful of int or bool values. As I wrote, I was making that up off the top of my head, and maybe I was inspired a little by the about:config tab in Mozilla Firefox. :wink:

I will probably have another file in SPIFFS where temporary things will be stored, especially last known sensor values before the ignition is shut off. But they, too, should be no more than float, int and bool. The trip computer will get permanent +12V from the battery, but I want it to go into deep sleep when the ignition is turned off (after showing the analog clock from my first post in this thread for a while), so there will be a pin on which the ESP32 senses if the ignition is HIGH or LOW.