Avoid step menu from being interrupted by slow tasks

Hi,

I'm a beginner when it comes to arduino and managed with some googeling to build a step menu (stepping through 4 menus with a button) displaying fuel levels etc. for my boat. I provide some pseudo-code, since my code got really big and in multiple files. Hope it makes sense.

I have a menuSwitcher function that listens to changes from a button (pin 4), and updates the current menu whenever that happens and then another function, menuDispatcher, which updates the display every 1 sec according to the current menu. Initially this worked fine and smooth.

Now I also want to add functionality such as posting data to a server and look for unread sms (in slowFunction), this takes time (about 1 sec) and while this is going on, the button must be pressed for about 1 sec. I understand it such that there must be a state change to the pin between the loops, and now that the loops takes 1 sec the button must be held down longer time, which is the problem.

My questions are the following:

  1. Is there a way to get around it with the current setup?
    I don't believe so, since everything is happening serially, right?

  2. I'm currently using an arduino mega, but also have an uno. Could the uno handle the display and the mega handle "slowFunction" and update the variables needed for the menus (fuel level etc. ) on the uno. I.e, connecting the mega to the uno and the uno to the display and letting the mega handle all sensors and doing the posting. There would be a delay in the update of the sensor variables whenever the mega use "slowFunction" but that is fine as long as the menus are responsive to the press of the button.

  3. Is this too complex for an arduino?

#define up 4
unsigned long DisplayTimer = 0;
const int menuSize = 4;

bool upLastState = HIGH;

int currentMenu = 1;

void setup(){

  pinMode(up, INPUT_PULLUP);

// Setting up screen etc.

}

void loop(){

 menuSwitcher();

if(millis()-DisplayTimer > 1000){
      DisplayTimer = millis();
      menuDispatch(currentMenu);
    }

slowFunction();

}

void slowFunction(){
// Posting something to server or listen to sms, which takes about 1 sec.
}


void menuDispatch(int _currentMenu){
  
    switch (_currentMenu) {
      case 0:

        // Display menu 3, e.g fuel level

        break;
      case 1:

        // Display menu 1, e.g water level

        break;
      case 2:

        // Display menu 2
        
        break;
      case 3:

        // Display menu 3

        break;

    }
}

void menuSwitcher(){
  if (digitalRead(up) != upLastState) {
    upLastState = !upLastState;
    if (!upLastState){
      // Just pressed
      if (currentMenu > 0){
        currentMenu--;
      } else {
        // Go around the menu from 0
        currentMenu = menuSize - 1;
      }

      menuDispatch(currentMenu);
      
    }
  }
}

The problem seems to be your loop taking 1 second. That's very long, and that's what you have to fix. Make your tasks take shorter, release control back to loop while waiting for a sensor or whatever - that kind of things. Getting loop to run about 1,000 times a second is needed for responsiveness.

Thank you for your reply!

The tasks lives in functions from libraries. Then I would have to change the libraries, right?
Or am I misunderstanding something?

wasserslasken:
The tasks lives in functions from libraries. Then I would have to change the libraries, right?
Or am I misunderstanding something?

Do you mean libraries that you created or libraries that someone else created?

In either case do you know which functions take more than (say) 100 microsecs to complete?

Can you post a short working example that illustrates your problem?

...R

Or am I misunderstanding something?

What you are missing is that you can't make things happen faster by wishing. Connecting to a web server and executing a GET request takes time. You can't hurry that up nor can you slice it into smaller pieces.

What you can do is to provide visual feedback when the switch has been read, and found to be pressed. At least that way, the user can hold the switch down as long as needed for the Arduino acknowledge that it was pressed.

When it comes to button presses, you can do quite a bit with interrupts.

Robin2:
Do you mean libraries that you created or libraries that someone else created?

It's libraries that someone else created.

Robin2:
In either case do you know which functions take more than (say) 100 microsecs to complete?

Can you post a short working example that illustrates your problem?

...R

Below is an example from the DFRobot_SIM800 library for reading SMS.
I noticed there is a 1000ms delay in the library function, but since I can't really understand the consequences of removing the delay, I'd rather go for a different solution. Even if it could be solved relatively easy for this particular example, as pointed out by PaulS, the GET request will take time.

PaulS:
What you are missing is that you can't make things happen faster by wishing. Connecting to a web server and executing a GET request takes time. You can't hurry that up nor can you slice it into smaller pieces.

What you can do is to provide visual feedback when the switch has been read, and found to be pressed. At least that way, the user can hold the switch down as long as needed for the Arduino acknowledge that it was pressed.

So if it can't be sliced up, what other options are there to get a fluid menu? Would alternative 3 (in my question) work? Pushing the button for an arbitrary long time is not gonna work in this case.

#include <DFRobot_sim808.h>
#include <SoftwareSerial.h>

#define MESSAGE_LENGTH 160
char message[MESSAGE_LENGTH];
int messageIndex = 0;

char phone[16];
char datetime[24];

//#define PIN_TX    10
//#define PIN_RX    11
//SoftwareSerial mySerial(PIN_TX,PIN_RX);
//DFRobot_SIM808 sim808(&mySerial);//Connect RX,TX,PWR,

DFRobot_SIM808 sim808(&Serial);

void setup() {
  //mySerial.begin(9600);
  Serial.begin(9600);

  //******** Initialize sim808 module *************
  while(!sim808.init()) {
      Serial.print("Sim808 init error\r\n");
      delay(1000);
  }
  delay(3000);  
  Serial.println("Init Success, please send SMS message to me!");
}

void loop() {
  //*********** Detecting unread SMS ************************
   messageIndex = sim808.isSMSunread();
    Serial.print("messageIndex: ");
    Serial.println(messageIndex);

   //*********** At least, there is one UNREAD SMS ***********
   if (messageIndex > 0) { 
      sim808.readSMS(messageIndex, message, MESSAGE_LENGTH, phone, datetime);
                 
      //***********In order not to full SIM Memory, is better to delete it**********
      sim808.deleteSMS(messageIndex);
      Serial.print("From number: ");
      Serial.println(phone);  
      Serial.print("Datetime: ");
      Serial.println(datetime);        
      Serial.print("Recieved Message: ");
      Serial.println(message);    
   }
}

wvmarle:
When it comes to button presses, you can do quite a bit with interrupts.

I'll look into it, sounds promising!

wasserslasken:
Below is an example from the DFRobot_SIM800 library for reading SMS.
I noticed there is a 1000ms delay in the library function, but since I can't really understand the consequences of removing the delay, I'd rather go for a different solution.

I have had a quick look at the sim808.cpp file and it seems to be littered with delay()s.

Unfortunately many library creators take no account of the possibility that users might want to use their creation alongside other activities.

No doubt all the delay()s could be replaced by non-blocking code using millis() but it would not be a trivial exercise.

Normally I would argue against using interrupts for button presses, but it may be the simplest solution in this situation.

...R

So I tried the interrupt function and its really good for capturing the press of the button, but since it can't update the menu (cause it takes too long for the interrupt) there will be a lag until the menu reacts. I have now made a small prototype where the mega handles all sensors and the uno handles the lcd and menu. They communicate through serial connection. I put in delays up to 20 sec in the mega and this does not affect the menu update time, i.e the menu changes immediately when I press the button. However it affects the update of the values that are displayed in the menu of course but this is not so critical, I don't need it updated so often. Many things are copied from other people, so thanks for that! Its still work in progress and I will tidy it up down the road, but feel free to suggest improvements.

This is on the display arduino (uno). Pin 2 is the button.

#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x3F, 2,1,0,4,5,6,7,3,POSITIVE);
int i, s, MessageLength, Message0[100], Message1[100], tempChar[1];
byte low, high;
const byte up = 2;
const int menuSize = 4;
bool upLastState = HIGH;
int currentMenu = 1;
float fuel_percent;
char var_menu0[10];
char var_menu1[10];
char var_menu2[10];
char var_menu3[10];
unsigned long DisplayTimer = 0;
unsigned long SerialTimer = 0;
void setup(){
  pinMode(up, INPUT_PULLUP);
 lcd.begin(16,2); 
 Serial.begin(115200);
  
}


void loop(){

  if(millis()-SerialTimer<30000){
  menuSwitcher();


    // How often the display should be updated
  if(millis()-DisplayTimer > 1000){
      DisplayTimer = millis();
      menuDispatch(currentMenu);
      
    }

  }else{
    lcd.setCursor(0,1);
    lcd.print("Lost connection");
  }

  // Here we wait until all n bytes has arrived
  if(Serial.available()>=14){
  if(Serial.read() == '

This is on the sensor arduino (mega)

byte counter;

float testFloat = 10.23545466;
char * testChar;

void setup(){
pinMode(3, OUTPUT);
Serial2.begin(115200);
Serial.begin(9600);
}


void loop(){

testChar = float2Ascii(testFloat);

Serial.print(testChar);	


delay(20000);


// Make sure that the message is not longer than 100 characters, else we need to change it int the LCD_arduino sketch
Serial2.print("$");
Serial2.print(float2Ascii(random(0,100)));
Serial2.print(",");
Serial2.print(float2Ascii(random(0,100)));

Serial2.print('#');
// this can probably be removed
counter++;





}

char* float2Ascii(double temp)
{
  char ascii[32];
  int frac;
  frac=(unsigned int)(temp*1000)%1000;  //get three numbers to the right of the deciaml point

  itoa((int)temp,ascii,10);
  strcat(ascii,".");
  itoa(frac,&ascii[strlen(ascii)],10); //put the frac after the deciaml

  
  return ascii;
}

){
    SerialTimer = millis();
    int varCounter = 0;
    s = 0;
    for(i=0; i<100; i++){
      delay(1);
      tempChar[0] = Serial.read();

// Check if we should enter it in the next variable
    if(tempChar[0]==','){
        s = 0;
      varCounter++;
      tempChar[0] = Serial.read();
      delay(1);
    }

switch(varCounter){
      case 0:
      Message0[s] = tempChar[0];
      var_menu0[s] = char(tempChar[0]);
      s++;

break;

case 1:
      Message1[s] = tempChar[0];
      var_menu1[s] = char(tempChar[0]);
      s++;

break;

case 2:
      Message1[s] = tempChar[0];
      var_menu2[s] = char(tempChar[0]);
      s++;

break;

case 3:
      Message1[s] = tempChar[0];
      var_menu3[s] = char(tempChar[0]);
      s++;

break;
    }
   
   
    if(tempChar[0]=='#')
    break;
    }
  }
  }

MessageLength = i;

}//loop

void menuSwitcher(){
  if (digitalRead(up) != upLastState) {
    upLastState = !upLastState;
    if (!upLastState){
      // Just pressed
      if (currentMenu > 0){
        currentMenu--;
      } else {
        // Go around the menu from 0
        currentMenu = menuSize - 1;
      }
      //SerialMon.println("Menu changed");
      //SerialMon.println(currentMenu);
      lcd.clear();
      menuDispatch(currentMenu);
     
    }
  }
}

void menuDispatch(int _currentMenu){
 
    switch (_currentMenu) {
      case 0:
      menu0(atof(var_menu0));
     
       
        break;
      case 1:
     
        menu1(atof(var_menu1));
     
       
        break;
      case 2:
     
        lcd.setCursor(0, 0);
        lcd.print("Menu 2");
       
        break;
      case 3:
     
        lcd.setCursor(0, 0);
        lcd.print("Menu 3");

break;
      default:
        lcd.setCursor(0,0);
        lcd.println("Default");
    }
}

void menu0(float waste_percent){
  lcd.print("Waste: ");
  if(waste_percent<10){
    lcd.setCursor(13, 0);
    lcd.print(" ");
    lcd.setCursor(14, 0);
    lcd.print(round(waste_percent));
    lcd.print("%");
  }else{
    lcd.setCursor(13, 0);
    lcd.print(round(waste_percent));
    lcd.print("%");
}
  progressBar(waste_percent, "");

}

void menu1(float fuel_percent){
  lcd.print("Fuel: ");
  if(fuel_percent<10){
    lcd.setCursor(13, 0);
    lcd.print(" ");
    lcd.setCursor(14, 0);
    lcd.print(round(fuel_percent));
    lcd.print("%");
  }else{
    lcd.setCursor(13, 0);
    lcd.print(round(fuel_percent));
    lcd.print("%");
}
  progressBar(fuel_percent, "");

}

void menu2(float fuel_percent){
  lcd.print("Menu 2");

}

void menu3(float fuel_percent){
  lcd.print("Menu 3");

}

void progressBar(double percent, String textIn){

#define lenght 16.0
  unsigned char b;
  unsigned int peace;

// custom charaters for the progress bar
  // each one is a vertical line

byte p1[8] = {
    0x10,
    0x10,
    0x10,
    0x10,
    0x10,
    0x10,
    0x10,
    0x10};

byte p2[8] = {
    0x18,
    0x18,
    0x18,
    0x18,
    0x18,
    0x18,
    0x18,
    0x18};

byte p3[8] = {
    0x1C,
    0x1C,
    0x1C,
    0x1C,
    0x1C,
    0x1C,
    0x1C,
    0x1C};

byte p4[8] = {
    0x1E,
    0x1E,
    0x1E,
    0x1E,
    0x1E,
    0x1E,
    0x1E,
    0x1E};

byte p5[8] = {
    0x1F,
    0x1F,
    0x1F,
    0x1F,
    0x1F,
    0x1F,
    0x1F,
    0x1F};

lcd.createChar(0, p1);
    lcd.createChar(1, p2);
    lcd.createChar(2, p3);
    lcd.createChar(3, p4);
    lcd.createChar(4, p5);
   
    lcd.setCursor(0, 0);

// calculate time until reached target

//percent = (millis() - startTime)/(1000*targetTime)*100.0;

lcd.print(textIn);
   
   
    lcd.setCursor(0,1);

double a=lenght/100*percent;

// drawing black rectangles on LCD

if (a>=1) {

for (int i=1;i<a;i++) {

lcd.write(byte(4));

b=i;
      }

a=a-b;

}

peace=a*5;

// drawing charater's colums

switch (peace) {

case 0:

break;

case 1:
      lcd.write(byte(0));

break;

case 2:
      lcd.write(byte(1));
      break;

case 3:
      lcd.write(byte(2));
      break;

case 4:
      lcd.write(byte(3));
      break;

}

//clearing line
    for (int i =0;i<(lenght-b);i++) {

lcd.print(" ");
    }
}

void pressedButton(){

lcd.clear();
lcd.println("Button pressed");

//menuDispatch(3);

}


This is on the sensor arduino (mega)

§DISCOURSE_HOISTED_CODE_1§

If you use two Arduinos, it would probably make sense to dedicate one to the (slow) gsm module and only use that for sending and receiving.

Thats a good point, I'll consider that. As of now Im using the Uno for "frontend" stuff and the mega for "backend", which guarantees that the menu will remain responsive no matter what gets connected connected to it later on.

I wonder what is more work: setting up properly working communication between the two and keeping two code bases in sync, or finding non-blocking ways to read those sensors and do everything on the Mega.

wasserslasken:
Thats a good point, I'll consider that. As of now Im using the Uno for "frontend" stuff and the mega for "backend", which guarantees that the menu will remain responsive no matter what gets connected connected to it later on.

I have a guaranteed method to un-delay whole sections of code that works every time. But I have no SMS and there are pros who should be getting PAID to write professional quality libraries in the first place. Even with the understanding to do it there's likely hours of tedious editing to make non-blocking functions for someone else's product.

I recommended to someone else in 2014 to dedicate a stand-alone 328P chip as a SIM800 controller. He was getting > 30 seconds of wait wait wait just for the SIM begin() to run. That way the main controller could still run the greenhouse.

How many pins do you need? Serial ports? RAM?

The under $6 ATmega1284P has 32 user IO pins, 2 serial ports, 16KB RAM, 128KB flash and 4KB EEPROM (save data through power-offs). It does not take much to make that run.

GoForSmoke:
I recommended to someone else in 2014 to dedicate a stand-alone 328P chip as a SIM800 controller. He was getting > 30 seconds of wait wait wait just for the SIM begin() to run. That way the main controller could still run the greenhouse.

That sounds like a sensible and economic solution. A nano or micro could be dedicated to the job if the OP does not wish to make a circuit board with an Atmega 328.

...R

I'd be thinking a Mini might be better, It has no USB (costs less). It can be programmed with an FTDI cable or other Arduino that has USB. It can be treated the same as a breadboard Duino as far as programming, they all can.

Crossroads sells a 1284 on a breakout board (very small, surface mount) and some DIP 1284 boards/breakouts.

If the 1284 isn't enough then it's Mega2560 and up time. The AVR's have stronger pins and lower price and many come in hobby-friendly DIP format that makes them rather choice for rapid dev work. They can fill so many niches cheaply, I expect they'll be made for a good while yet.

int i, s, MessageLength, Message0[100], Message1[100], tempChar[1];

I abhor using a type (char) in the name of a variable when the type in the name is not the type of the variable (int).

I abhor code that has piss-poor indenting. Tools + Auto Format would correct that in a flash.

Probably another 200 bytes are to be saved by making Message0 and Message1 a string of char instead of a string of int. Even on a Mega that makes a significant difference.

I know I've posted this before, but it is possible to develop an asynchronous SMS driver around a state-machine....
(Mine is a bit of a tangled mess derived from the FONA library, but it works.)

I use simple states as below

enum modem_states { MODEM_IDLE,
    MODEM_SMS_WAITPROMPT, MODEM_SMS_SENDBODY, MODEM_SMS_WAITFOROK, 
    MODEM_CSQ_PENDING, 
    MODEM_CCLK_PENDING,
    MODEM_CALLBACK };

This allows me to initiate each phase of 'sending' an SMS, then returning to the main code.
Depending on the current 'state', and any message returned from the modem, we can parse, or change state as needed.

It doesn't make the actual SMS sending any faster, but it doesn't block the rest of your code while other things are happening.
I have an interactive serial console, the modem doing other things, various blinking LEDs, multiple input sensing, four output relays, and a scrolling LCD display all on 20mS or better response time. It can be done.

lastchancename:
It doesn't make the actual SMS sending any faster, but it doesn't block the rest of your code while other things are happening.

Are you using the same SMS library as the OP?

As I mentioned in Reply #8 the library he is using seems to be littered with delay()s. No amount of states is going to get around them.

...R