Notifications
Clear all

How to write interactive menus while being responsive

11 Posts
6 Users
0 Likes
1,857 Views
adrian
(@adrian)
Member
Joined: 4 years ago
Posts: 12
Topic starter  

This may take a bit to explain, so sit comfortably, grab a glass of your elixir of choice and read on.

I've spent too many years in the high abstraction lands of functional and later web programing, away from low level code (15 years since that job when I wrote C/C++ for set top boxes) and that may be creating some cognitive dissonance in my brain about the best way to handle concurrent tasks in an Arduino or similar micro controllers where no OS is involved. 

What do I want to do? Simply (is anything ever really "simple"?) have a matrix keypad drive a menu on an LCD screen while making sure that timed events (e.g. "do X at 5:35") still happen regardless of which menu or task the user is doing. Now, that may sound like a no brainer, but I can't figure out a clean way to write certain sub-menus without looping inside the menu function, never returning to the main loop() function until done. Here's an example:

void updateDurationMenu(int start_pos, int sch) {
char duration[2];
String d = String(schedules[sch].duration);
int idx = 0;
char key_press = NULL;

lcd.setCursor(start_pos, 1);
lcd.blink();

while ((idx < DURATION_LENGTH) && !(key_press == BACK || key_press == CONFIRM)) {
key_press = key_pad.getKey();
if (key_press <= '9' && key_press >= '0') {
duration[idx] = key_press;
lcd.print(key_press);
idx++;
}
}
if (idx == 1) { // only first digit entered
duration[1] = d.charAt(d.length() - 1);
}
schedules[sch].duration = String(duration).toInt();
lcd.noBlink();
}

As you can see, as long as the user is not done updating the duration field, the code will stay within this function never going back to loop() to handle any event that may be happening. Even if the event is driven via interrupts, setting a flag in the interrupt handler to deal with it later means that I will not deal with it until this is done, which is not acceptable for my purposes. 

I tried to rewrite this in a way that the loop() function calls into updateDurationMenu() instead, but then I have to keep the state (idx, d) in the main program, as well as a way to know in which of the many menus (with sub menus) I am and how to transition from one to the next. Right now, transitions are relatively straight forward because each function knows where to go. For instance, this is how you get to updateDurationMenu() from its parent menu:

void updateScheduleMenu(int sch) {
int cur = 0;
printCursor(cur);
lcd.setCursor(1,0);
lcd.print("Time: ");
printTime(schedules[sch].date);
lcd.setCursor(1,1);
lcd.print("Duration: ");
printDuration(schedules[sch].duration);
lcd.print("min");

char key_press = NULL;
while (key_press != BACK) {
key_press = key_pad.getKey();
switch(key_press) {
case CONFIRM:
if (cur == 0) {
updateTimeMenu(7, sch);
} else{
updateDurationMenu(11, sch);
}
break;
case DOWN:
cur = (cur + 1) % LCD_ROWS;
printCursor(cur);
break;
}
}
}

 

If I were to write this somewhere else I'd just use a thread to do this while the main thread is available to deal with timer interrupts, but I'm not quite sure how to handle this here. 

If you've gotten this far, pour yourself another drink, I've made you suffer enough already.

Cheers!


   
Quote
robotBuilder
(@robotbuilder)
Member
Joined: 5 years ago
Posts: 2043
 

I've spent too many years in the high abstraction lands of functional and later web programming, away from low level code (15 years since that job when I wrote C/C++ for set top boxes) and that may be creating some cognitive dissonance in my brain about the best way to handle concurrent tasks in an Arduino or similar micro controllers where no OS is involved. 

Opposite to my problem. I spent all my years low level programming and find high level abstractions leave me without any clear idea of what is going on!

It must be a very complicated piece of code you are working on. 

It is not exactly clear to me what the problem is as I thought an interrupt would do exactly what you seem to require.  A main loop can poll everything including keyboard inputs.  There is no need to have a closed loop waiting for a key press?

 

 


   
ReplyQuote
adrian
(@adrian)
Member
Joined: 4 years ago
Posts: 12
Topic starter  

I hope it is not too complicated and it is just the way I'm framing the problem. The problem I encounter when trying to make the main loop() call into the menus is that I can't find a good way to track which menu should I be invoking and with which parameters. Each menu can have sub menus and some menus allow you to edit a numeric field while you're in it.

I've extracted the relevant menu code from my program and shared it here: https://gist.github.com/adrianbn/6d833e594e70348e931d937e5f573342

I've also uploaded a video of the menu operation in case it helps:

This may make it more clear what I'm doing with the menu. The things that need to happen while the menus are being operated include events driven by an RTC module and other sensors (removed from the code for simplicity).

Hopefully someone can nudge me in the right direction here. 

Cheers!


   
ReplyQuote
robotBuilder
(@robotbuilder)
Member
Joined: 5 years ago
Posts: 2043
 

As you can see, as long as the user is not done updating the duration field, the code will stay within this function never going back to loop() to handle any event that may be happening.

What other events are there that might be happening?

I suggest you would have a run mode and an edit mode.

The key event should interrupt the main loop or the keys be monitored by the main loop and if in the edit mode a key event in the main loop would call the edit routine to execute the key event  (move cursor, change character) and then immediately return to the main loop which can continue to monitor all the other events as well.

So let's see if I understand what the program does.  You have two schedules. You can select one of two schedules and edit its start time and how long the motor is to run?

You can't really edit the schedules while they are being executed. Also how do you avoid one schedule ever overlapping another schedule?

Duration could also be a stop and start time.

IF schedule1_start_time then turn on motor.
IF schedule1_stop_time then turn off motor.
IF schedule2_start_time then turn on motor.
IF schedule2_stop_time then turn off motor.

 


   
ReplyQuote
adrian
(@adrian)
Member
Joined: 4 years ago
Posts: 12
Topic starter  
Posted by: @robotbuilder

What other events are there that might be happening?

Right now, only RTC interrupts to check time and determine what to do based on the schedules. Eventually, I want to add sensor readings for temperature to trigger additional events. I don't want any of these to not happen because someone was at the keyboard or left the device in a menu screen.

Posted by: @robotbuilder

You can't really edit the schedules while they are being executed. Also how do you avoid one schedule ever overlapping another schedule?

You're correct. Editing the schedules while running may throw the system into a loop. It'll figure itself out eventually, but I'd rather not keep the motor running for that long. I will probably stop the motor when done editing the schedules if it was running. As for the overlap, it falls into the realm of acceptable human error for version 1, but I will add a check after editing to see if the new value and the other schedules (I'll add more) overlap.

Posted by: @robotbuilder

The key event should interrupt the main loop or the keys be monitored by the main loop and if in the edit mode a key event in the main loop would call the edit routine to execute the key event  (move cursor, change character) and then immediately return to the main loop which can continue to monitor all the other events as well.

I get the general idea of this, having the main loop dispatch a call to the appropriate screen to do what it needs to. I just can't seem to write the logic for it. I started it and it becomes a real nightmare. For instance, when you're on the main menu, pressing "*" should move you into whatever menu is being pointed at by the chevron cursor. Then, inside that menu, pressing "*" will do different things depending on which menu you're on. I started on this path (see below) but I stopped because it felt wrong the way I was going. The idea was that loop() would check for key press, if it is CONFIRM/BACK it will have to update its internal "current menu" state, to know which menu to dispatch the next key press.

void menuBack() {
switch(curr_menu) {
case "MAIN":
break;
case "PUMP_ON":
case "PUMP_OFF":
case "UPDATE_SCHEDULE":
curr_menu = "MAIN";
break;
case "UPDATE_DURATION":
case "UPDATE_TIME":
curr_menu = "UPDATE_SCHEDULE";
break;
}
}

void menuForward() {
switch(curr_menu) {
case "MAIN":
switch(cursor_pos) {
case 0:
case 1:
curr_menu = "UPDATE_SCHEDULE" + cursor_pos;
break;
case 2:
curr_menu = "PUMP_ON";
break;
case 3:
curr_menu = "PUMP_OFF";
break;
}
break;
case "UPDATE_SCHEDULE1":
if (cursor_pos == 0) {
curr_menu = "UPDATE_TIME1";
} else {
curr_menu = "UPDATE_DURATION1";
}
break;
case "UPDATE_SCHEDULE2":
if (cursor_pos == 0) {
curr_menu = "UPDATE_TIME2";
} else {
curr_menu = "UPDATE_DURATION2";
}
break;
case "UPDATE_DURATION":
case "UPDATE_TIME":
curr_menu = "UPDATE_SCHEDULE";
break;
}
}

Then there are menus like the edit one that need to know when all input has been entered (they need to keep state), but they can't keep the state internally if they're constantly re-entering, which means we're pushing a lot of different local variables out to loop() that only apply to a specific sub-menu, breaking the little encapsulation functions provide.

For instance, the edit menu from the original post:

void updateDurationMenu(int start_pos, int sch) {
char duration[2];
String d = String(schedules[sch].duration);
int idx = 0;
char key_press = NULL;

lcd.setCursor(start_pos, 1);
lcd.blink();

while ((idx < DURATION_LENGTH) && !(key_press == BACK || key_press == CONFIRM)) {
key_press = key_pad.getKey();
if (key_press <= '9' && key_press >= '0') {
duration[idx] = key_press;
lcd.print(key_press);
idx++;
}
}
if (idx == 1) { // only first digit entered
duration[1] = d.charAt(d.length() - 1);
}
schedules[sch].duration = String(duration).toInt();
lcd.noBlink();
}

So in a simple program where this is the only menu driven by loop() we'd have to pass in duration, idx, and key_press from loop(), plus add another variable "done" that determines whether the code after the while loop runs on this invocation of the function or not. Those variables would need to be global, since they'd need to be accessed from loop() as well as from updateDurationMenu().

If that's the only way to go, I guess I'll go down this path, but it will make a super messy, very hard to read and maintain piece of code, where multiple functions handle local variables.

Thanks for the help!


   
ReplyQuote
robotBuilder
(@robotbuilder)
Member
Joined: 5 years ago
Posts: 2043
 

@adrian

There must be some optimal way to resolve your issue.

Is the requirement for some kind of multitasking?

https://www.electronicshub.org/arduino-multitasking-tutorial/

I would search for solution beyond this forum.

 


   
ReplyQuote
MadMisha
(@madmisha)
Member
Joined: 4 years ago
Posts: 340
 

I am new to this so ignore if it doesn't make sense. This sounds like as good use for an ESP32. Have one core dedicated to your timed events to always run and execute and the other to control the screen and menu.


   
ReplyQuote
(@hakha4)
Member
Joined: 4 years ago
Posts: 2
 

Why not use a hardware interrupt timer for your critical events overriding if you are in the menu loop ?

Lot´s of example, here is one that might give ideas. https://github.com/khoih-prog/TimerInterrupt


   
ReplyQuote
adrian
(@adrian)
Member
Joined: 4 years ago
Posts: 12
Topic starter  

I'm still not done with this problem but I figured I'd provide an update. Upon further research it seems that the menu problem is not a straightforward issue across the board. I've found a couple of libraries that help deal with this and looking at them highlights how complex this issue can be. They both address it, but at a cost:

  • https://github.com/neu-rah/ArduinoMenu - This is the one I'm using for now; the documentation is found wanting and it consumes enormous amounts of memory / storage. It's pushing my nano to its limits. It does, on the other hand, handle a lot of things for you, including the update of fields. I am still figuring out how to edit some fields in a way I am happy with, but it is functional at the moment.
  • https://github.com/VaSe7u/LiquidMenu - This I want to try out this week. It's a lighter weight option, but it is not as flexible (nesting of menus and depth is limited). That's not a problem for my project though. It does not handle field edit for you, which means a bit more work but also perhaps more control over the format and editing, which are areas of pain with the previous library.

To this point, I've spent inordinate amounts of time trying to write a menu for a project, which was never the centerpiece of the project. I'm looking forward to polish what I have with ArduinoMenu or switch to LiquidMenu and be done with it so I can move to the more interesting parts of the project. It is still a bit surprising that this is not a truly solved problem given how often devices would need to provide menus for users to interact with them.


   
ReplyQuote
byron
(@byron)
No Title
Joined: 5 years ago
Posts: 1122
 

@adrian

Very much a curved ball, but whenever I see this sort of problem I muse on how much easier a menu solution would be if programmed in python and possibly the whole solution could be accomplished on a raspberry pi.  So it struck me that you could easily have two boards in your project, your arduino and also a rpi Zero (to do the python bit).  The communication between to 2 is easy to do.   

But this is just me musing away and probably not a tack you would want to go down.  It seems you are quite far along your path of a solution anyway, so I hope you find a good result.


   
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 3 years ago
Posts: 6982
 
Posted by: @adrian

This may take a bit to explain, so sit comfortably, grab a glass of your elixir of choice and read on.

I've spent too many years in the high abstraction lands of functional and later web programing, away from low level code (15 years since that job when I wrote C/C++ for set top boxes) and that may be creating some cognitive dissonance in my brain about the best way to handle concurrent tasks in an Arduino or similar micro controllers where no OS is involved. 

What do I want to do? Simply (is anything ever really "simple"?) have a matrix keypad drive a menu on an LCD screen while making sure that timed events (e.g. "do X at 5:35") still happen regardless of which menu or task the user is doing. Now, that may sound like a no brainer, but I can't figure out a clean way to write certain sub-menus without looping inside the menu function, never returning to the main loop() function until done. Here's an example:

void updateDurationMenu(int start_pos, int sch) {
char duration[2];
String d = String(schedules[sch].duration);
int idx = 0;
char key_press = NULL;

lcd.setCursor(start_pos, 1);
lcd.blink();

while ((idx < DURATION_LENGTH) && !(key_press == BACK || key_press == CONFIRM)) {
key_press = key_pad.getKey();
if (key_press <= '9' && key_press >= '0') {
duration[idx] = key_press;
lcd.print(key_press);
idx++;
}
}
if (idx == 1) { // only first digit entered
duration[1] = d.charAt(d.length() - 1);
}
schedules[sch].duration = String(duration).toInt();
lcd.noBlink();
}

As you can see, as long as the user is not done updating the duration field, the code will stay within this function never going back to loop() to handle any event that may be happening. Even if the event is driven via interrupts, setting a flag in the interrupt handler to deal with it later means that I will not deal with it until this is done, which is not acceptable for my purposes. 

I tried to rewrite this in a way that the loop() function calls into updateDurationMenu() instead, but then I have to keep the state (idx, d) in the main program, as well as a way to know in which of the many menus (with sub menus) I am and how to transition from one to the next. Right now, transitions are relatively straight forward because each function knows where to go. For instance, this is how you get to updateDurationMenu() from its parent menu:

void updateScheduleMenu(int sch) {
int cur = 0;
printCursor(cur);
lcd.setCursor(1,0);
lcd.print("Time: ");
printTime(schedules[sch].date);
lcd.setCursor(1,1);
lcd.print("Duration: ");
printDuration(schedules[sch].duration);
lcd.print("min");

char key_press = NULL;
while (key_press != BACK) {
key_press = key_pad.getKey();
switch(key_press) {
case CONFIRM:
if (cur == 0) {
updateTimeMenu(7, sch);
} else{
updateDurationMenu(11, sch);
}
break;
case DOWN:
cur = (cur + 1) % LCD_ROWS;
printCursor(cur);
break;
}
}
}

 

If I were to write this somewhere else I'd just use a thread to do this while the main thread is available to deal with timer interrupts, but I'm not quite sure how to handle this here. 

If you've gotten this far, pour yourself another drink, I've made you suffer enough already.

Cheers!

@adrian start your search here https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32/api-reference/peripherals/timer.html

and https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html?highlight=alarm

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
Sure you can learn to be a programmer, it will take the same amount of time for me to learn to be a Doctor.


   
ReplyQuote