Author Topic: HMI - input menu, how do you handle the code?  (Read 1738 times)

0 Members and 1 Guest are viewing this topic.

Offline rvalenteTopic starter

  • Frequent Contributor
  • **
  • Posts: 726
  • Country: br
HMI - input menu, how do you handle the code?
« on: December 10, 2019, 02:14:08 am »
Hello mates,

I'm starting a development (STM32, C GCC, CubeIDE) with a menu structure similar to a motor drive, with parameters from P000 to P100... e.g.
There will be an rotary encoder with button as "ok/sel" and an esc button near and a 4 digit 7 seg led display



How do you guys handle the code to work the menus?
For every holding each parameter, I'm thinking something like this:

typedef struct
{
   float value; //Holds the parameter value
   float min; //Holds the maximum allowed parameter
   float max; //Holds the minimum allowed parameter
   unsigned char parameter; //Holds the parameter number, to display
   unsigned char notSaved :1; //Has the parameter changed in the last interaction and not saved? Yes, save to eeprom.

   struct
   {
      unsigned long value1;
      unsigned long value2;
      unsigned long value3;
      unsigned int value4;
   };
}PARAMETER;

For controlling it, how do you handle the code? A gigantic switch case?

Please, share your knowledge.
 

Online MarkF

  • Super Contributor
  • ***
  • Posts: 2550
  • Country: us
Re: HMI - input menu, how do you handle the code?
« Reply #1 on: December 10, 2019, 04:47:40 am »
I use the encoder push button to toggle between "item selection" and "data input" modes.
From which, the user can select a item (highlighted by a cursor) --> toggle the switch --> change the item.

I use a fixed 500Hz interrupt to read the encoder knob and save the number of detents changed (positive or negative).
In my example, variables en0 and sw0 are set in the ISR() function.

Code: [Select]
#define mITM 1        // item_select mode
#define mDAT 0        // data_input mode
#define NUM_ITEMS 4   // number of items in menu

  int en0=0;  // encoder_0 detent change count
  int sw0=0;  // encoder_0 switch depressed and released

  int mode=mITM;  // current mode
  int curItem=0;  // current item
  int val_0=0;
  int val_1=0;
  int val_2=0;
  int val_3=0;

//============================================================
void main(void)
{
  int ev;  // encoder value

  while (1) {

    if (sw0 != 0) {  // user pressed the switch
      sw0 = 0;
      if (++mode > 1) mode = 0;
    }

    ev = en0;        // get number of counts from ISR function
    en0 = 0;         // reset encoder count to restart counting in ISR
    if (ev != 0) {   // user rotated the knob

      if (mode == mITM) {
         curItem =+ ev;
         if (curItem >= NUM_ITEMS) curItem = 0;
         if (curItem < 0) curItem = NUM_ITEMS - 1;
      }

      else if (mode == mDAT) {
        switch (curItem) {
          case 0:   
            val_0 += ev;    // note:  You still need to check the limits for each value
            break;
          case 1:   
            val_1 += ev;
            break;
          case 2:   
            val_2 += ev;
            break;
          case 3:   
            val_3 += ev;
            break;
         }
      }

    }

    drawDisplay();  // draw display and highlight the current item

    // TODO:  any more processing needed
  }
}

//============================================================
void ISR(void)    // 500 Hz interrupt service routine
{
  // read encoder and its switch
}

« Last Edit: December 10, 2019, 05:14:28 am by MarkF »
 

Offline ggchab

  • Frequent Contributor
  • **
  • Posts: 276
  • Country: be
Re: HMI - input menu, how do you handle the code?
« Reply #2 on: December 10, 2019, 07:23:49 am »
Will you have an array of "PARAMETER"? Then, why a gigantic switch case ? Can't the index of the array simply be the parameter number ?
If there are unassigned parameter numbers and you  don't want to have empty array entries, why not adding a new "paramNumber" member in your structure to identify each parameter ?
 

Offline Jeroen3

  • Super Contributor
  • ***
  • Posts: 4078
  • Country: nl
  • Embedded Engineer
    • jeroen3.nl
Re: HMI - input menu, how do you handle the code?
« Reply #3 on: December 10, 2019, 09:58:57 am »
For controlling it, how do you handle the code?
I'd happen to be exploring this right now. It's quite complex to standardize and keep it extensible.
And implement in C. (c++, much better)

But for implementation I've settled on something like this:
Code: (c) [Select]
struct PARAMETER {
    enum Type type;
    int number;
    union {
      int (*integer)(int number, const int * const data);
      /* other types */
    } setter;
    union {
      int (*integer)(int number, int * const data);
      /* other types */
    } getter;
}
Infinitely scalable across many files and extendable in many ways. (more types, special conversions, range checks, condition checks)
Usage:
Code: [Select]
int value;
if(0 < parameterlist[i].getter(number, &value))
    printf("%d",value)
else
    printf("failure")
yes, searching trough the table is slow. But accessing the menu on the screen isn't very real-time, or is it?
Basically I'm doing something that inheritance should have solved in C++.

Also, good recommendation. Limit the amount of types to the smallest amount possible. Integer and Float are enough. Maybe text. But that's it. It saves you from having to create dozens of viewers/editors for your model.

And try a test environment and TDD. You should make this menu run on your PC with any normal C compiler and a shell. This makes your life easy.
« Last Edit: December 10, 2019, 10:14:40 am by Jeroen3 »
 

Offline senso

  • Frequent Contributor
  • **
  • Posts: 951
  • Country: pt
    • My AVR tutorials
Re: HMI - input menu, how do you handle the code?
« Reply #4 on: December 10, 2019, 11:56:11 am »
Will you have an array of "PARAMETER"? Then, why a gigantic switch case ? Can't the index of the array simply be the parameter number ?
If there are unassigned parameter numbers and you  don't want to have empty array entries, why not adding a new "paramNumber" member in your structure to identify each parameter ?

Maybe easier to handle each parameter as an isolated case with a gigantic switch case, because one parameter might go between 50 or 60(Hz, relative to input AC), another might be 0-100 for percent something, another might be Hz again but its 0-300(output Hz, maybe with a decimal place), other parameter might be just 0 or 1 to turn on/off, others will be "random number", like 0 do nothing, 10 re-set some settings, 99 factory reset.
Also, then you can blink units at maybe 0.5Hz so the user can kinda know wtf is going on, because I hate to set VFD's with those crappy interfaces.

If you only have one place to handle all those conditions you will also end up in a gigantic switch case, but messier.
 

Offline ggchab

  • Frequent Contributor
  • **
  • Posts: 276
  • Country: be
Re: HMI - input menu, how do you handle the code?
« Reply #5 on: December 10, 2019, 12:38:57 pm »
You already have a min/max in your structure. Why not adding a step value and a code for the unit ?
Of course, I don't know how different might be your units and if there is a nice way to handle them all with the same code.

Or you might have a set of functions and a pointer to the right function in your structure, depending on the unit. Most probably, you wont' need as many functions as parameters in your array.
 

Offline rstofer

  • Super Contributor
  • ***
  • Posts: 9890
  • Country: us
Re: HMI - input menu, how do you handle the code?
« Reply #6 on: December 10, 2019, 02:30:38 pm »
Personally, I would code the gigantic case statement with transitions - a Finite State Machine.

OTOH, I could add 2 fields to the structure so that I could have a linked list of structs with 'previous' and 'next' pointers.  This would work well but gets to be awkward when there are many structs in the list.  Adding a new struct in the middle would take some care.
 

Online T3sl4co1l

  • Super Contributor
  • ***
  • Posts: 21698
  • Country: us
  • Expert, Analog Electronics, PCB Layout, EMC
    • Seven Transistor Labs
Re: HMI - input menu, how do you handle the code?
« Reply #7 on: December 10, 2019, 04:20:29 pm »
I like to encode the menu structure in the data structure.  So you might have:

Code: [Select]
typedef struct menu_s {
menu_func_rtn_t (*func)(int* this, int* params);
int* this;
menu_t prev;
menu_t next;
menu_t esc;
} menu_t;
(give or take syntax errors?)

The core loop being:
- Point to a given menu_t
- Call its function with its common data parameter this, and any other operands as needed (and implying how this might be syntactically simplified in C++!)
- The function performs menu-specific actions, when applicable
- The function returns a value determining the next action in the loop, e.g., an enum selecting the next menu_t

This echoes the above recommendation of an FSM; it's just encoded differently.  Whereas you might implement one with a switch-case (the cases being effectively unnamed functions for each action), this has a core machine which operates on data; the data is written into a graph of objects.

There is a size-functionality tradeoff, and also a verbosity tradeoff (everything in the switch-case is just, well, in the switch-case, it's a huge pile; not that this can necessarily be organized any better, granted!).  The switch-case (or even simpler, if-else) will win for short state machines; this format wins for generality, when you have a fair number of menus and a diversity of data and functionality on the items.

Of course if you need absolute minimum size, you can encode things even more tightly, say down to the level of pointers or offsets for target data and operations; or even writing a simple VM that runs bytecode, allowing extremely powerful (but probably also very cryptic) operations to be done in very little space; you'd want to write a code generator to facilitate such an extreme option).

I'm guessing you aren't pressed for RAM or ROM, so that kind of approach is already well out of scope; but if you get forced into such a corner, there are ways out.


The core loop might be at the level of a menu screen/page, where the function is executed as part of an init or data-fetch (say to put a value of interest into a string), or to change an option (in which case you might add a few fields to reference options where applicable), or on exit (to clean up?).  Or multiple, however you like.

.this should contain some strings or graphics for each screen, or a list of items, or whatever.  .next and .prev could be pages in a given menu, and .esc goes up one level.  Sub-menus would have to be returned from .func, in which case it should have a different type, or a compound type (struct) .

On a limited system like a 4-digit display, each item might be a menu, and prev/next would go between items, since, well, items are screens...


The functions, you might craft a no-op or "dummy" function to handle the default case (e.g., navigation items).  You might write functions specific to a given action, e.g.
"INCREMENT TEMPERATURE" -->
Code: [Select]
{
Temperature++;
updateTemperature();
return 0;
}

Or you might encode simple actions in this data, e.g.
Code: [Select]
{
(this.myPointer*)++;
this.update();
return 0;
}

You can slice it up however you like, and what's best will depend on what you need.

I don't know of any richly-featured systems offhand, but I doubt that there are any that are really general enough to fit anything anyone might want to do and are easy to build and are compact enough for embedded -- though again, if you're really not limited by memory, the overhead of that, plus e.g. STM32 HAL, plus an RTOS or something, might really just not matter.  No idea!

Although there probably is a general and compact system (if not in existence, then possible to write), in C++, with copious use of templating; but setting them up may not be at all obvious, as a result (or maybe tool driven instead).

Tim
Seven Transistor Labs, LLC
Electronic design, from concept to prototype.
Bringing a project to life?  Send me a message!
 

Offline rstofer

  • Super Contributor
  • ***
  • Posts: 9890
  • Country: us
Re: HMI - input menu, how do you handle the code?
« Reply #8 on: December 10, 2019, 06:17:20 pm »
And in each state we need to consider that there are at least 2 exit states (next and previous) plus abnormal/exit branches.  I'm thinking about something like 'page down' to move forward quickly rather than stepping through a bunch of states to get somewhere.  This really argues for the FSM because each state can do whatever it wishes for the next state and each state only deals with some small aspect of the data structure.  Plus I can bury text inside each state for prompts and such.

I'm really a hardware guy at heart.  FSMs are always going to be my preferred solution.
 

Online MarkF

  • Super Contributor
  • ***
  • Posts: 2550
  • Country: us
Re: HMI - input menu, how do you handle the code?
« Reply #9 on: December 10, 2019, 07:09:10 pm »
Consider:  That the item being modified is NOT the entire value, but a digit within the value.

If for example, you want to modify a frequency.  The frequency range is 1Hz to 1000Hz.  The cursor location does NOT indicate the entire frequency but one of three digits (i.e. the ones, tens and hundreds digits).  The switch statement will have a case for each of the three digits that are allowed to be modified.

This example implemented with a generic indexed array for each item would difficult to handle.  Let alone knowing where to draw a cursor.  With a switch statement implementation, the case # indicates the portion/digit of the item being modified and the location to draw a cursor.

Code: [Select]
            switch (mItem) {
               case  3:  ddsF1/=1000000;  ddsF1+=ev;  ddsF1*=1000000;  cF1=1;  break;
               case  4:  ddsF1/=100000;  ddsF1+=ev;  ddsF1*=100000;  cF1=1;  break;
               case  5:  ddsF1/=10000;  ddsF1+=ev;  ddsF1*=10000;  cF1=1;  break;
               case  6:  ddsF1/=1000;  ddsF1+=ev;  ddsF1*=1000;  cF1=1;  break;
               case  7:  ddsF1/=100;  ddsF1+=ev;  ddsF1*=100;  cF1=1;  break;
               case  8:  ddsF1/=10;  ddsF1+=ev;  ddsF1*=10;  cF1=1;  break;
               case  9:  ddsF1+=ev;  cF1=1;  break;
"cF1" is a change flag for a display() function.  Since redrawing the entire display will cause flashing.  So just redraw that one value.
 

Offline ajb

  • Super Contributor
  • ***
  • Posts: 2608
  • Country: us
Re: HMI - input menu, how do you handle the code?
« Reply #10 on: December 10, 2019, 11:14:59 pm »
There are a number of ways to go about this, but as you can see in this thread the general consensus is that you would have some sort of struct that holds an item description, and your menu handling code navigates a tree (or something) of those structs. 

A few things to consider:

- Does the menu structure contain the control value, or does it contain a reference to the control value which is stored somewhere else, or does it contain a copy of that control data?  Is this consistent across your entire menu structure?
- Same question as above, but for min/max.  How do you handle values where the min or max is conditioned on some other variable?
- Should changes to control values be made live, as the user manipulates them, or only when the user somehow commits the value?
- Are you only manipulating values via this menu, or might you want to use the interface to do something else requiring, say, handling the buttons differently? (less of an issue for something as limited as a 4x7 segment display, but definitely an issue in more sophisticated interfaces)

In general I would want to have the menu store references to control values, as this better decouples the menu from the application.  It also means that if your parameters are accessible in another way (say via MODBUS) then you are coupling the communication stack to the application directly rather than having to go through the menu.  (Another option is to have a data structure that describes your configuration data in a more abstract fashion, and both your menu system and your comm stack operate on this abstract structure.)  If your menu needs to access variables of different types (floats, ints, strings, IP addresses, whatever), then you can use a tagged union, where you have a union of the required data types (as pointers or values), and a tag (usually an enum) that tells the menu handling code which member of the union to use. 

When it comes to implementing the code for this, your menu can be organized into a list or tree structure, and your menu handling functions need to have some sort of reference to the current item, and a way to navigate to others.  You can use a linked list, or an array, and you can have arbitrary links from one list to another (ie, for sub menus).  The menu code should then have the ability to do all of the necessary operations (increment, decrement, save, cancel) on each of the data types your menu exposes.  With the right structures, you can use the same objects to store a menu item or a reference to a submenu.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf