Guru help to improve code ?

Hi All

I am in the final throws of finishing my lock-down project of a spa controller to replace my basic time switch on or off.

The last major part of the project is an error checking to make sure the times, when the spa is at a comfort heat level, entered do not overlap or conflict.

Premise is that there is typedef that sets a structure to capture on, off and freq. Frequency being Everyday, Weekends, Weekdays, and off (not set). time is captured in bytes of hrs and mins. (I should note these are set by an admin user and should rarely be changed) There are 6 comfortTimes in an array to store them.

The hrs and min are converted to seconds past midnight as that was the east way I could think of to check if at any point in time the spa should be at comfort level or not (((hrs *60) + min) * 60). These are stored in two arrays, Comfort_on_times and comfort_off_times.

The code below works and tests out correctly but my questions is could I have done this a more betterer :slight_smile: way rather than nested for and if statement as i have done?. I tried making the function a bool to avoid another global variable but I couldn't get it to work so i made it a void and put bool "conflict" as a global.

One of the reasons I am asking is the overall spa program (attached but not completed) is running out of space so I may have to investigate progmem but don't know which things I should put into progmem (other than they should be static). Should I be putting all of my LCD display text (menu options and messages) in progmem and just calling it, will that help?

The full spa code reports as (before I add the error checking code)
"Sketch uses 17706 bytes (54%) of program storage space. Maximum is 32256 bytes.
Global variables use 1589 bytes (77%) of dynamic memory, leaving 459 bytes for local variables. Maximum is 2048 bytes....Low memory available, stability problems may occur."

Serial.prints are only for testing and will be removed

// structure to store comfort times
typedef struct {
  byte On_hrs;
  byte On_min;
  byte Off_hrs;
  byte Off_min;
  byte Freq;  // Weekdays, Weekends, Everyday
}__attribute__((packed)) Comfort;

// 0 = off
// 1 = Weekends
// 2 = weekdays
// 3 = everyday

Comfort comfortTimes[] =
{
// on        off    
// hr  min  hr min freq      LCD display starts at comfort 1
  {13, 30, 14, 30, 3},    // Comfort 0 On Everyday
  {8,   0, 9,  0,  1},    // Comfort 1 On Weekends only
  {8,  30, 10, 0,  2},    // Comfort 2 On Weekdays only
  {19, 30, 22, 30, 1},    // Comfort 4 On weekends only
  {18, 30, 21, 30, 2},    // Comfort 3 On Weekends only
  {0, 00, 9, 30,   0},    // Comfort 5 not set
}; // array of 6 items

// convert the on and off times to seconds past midnight
                                    // 0       1       2     3      4      5   
unsigned long comfort_on_sec[6] =  { 48600, 28800, 30600, 70200,  66600,     0};
unsigned long comfort_off_sec[6] = { 50400, 32400, 36000, 79200,  75600,     0};
bool conflict = false;

void CheckConflict()
{
  byte i; //local variable for item to be tested
  byte x; //local variable for item to be tested against 
 
 for(i = 0; i < 6; i++){ // cycle through the list 0 - 6
  Serial.print("i=");
  Serial.println(i);

   if(comfort_on_sec[i] > comfort_off_sec[i]) // test the start time is before the end time
   {
    conflict = true; // if true no need to test further.
   }

   else if(conflict == false){  // continue testing
    
      for (x = i+1 ; x < 6; x++ ) {  // for loop with reducing tests per loop ie 1 against 2-5, 2 against 3-5
        Serial.print ("x=");
        Serial.println(x);
        
        // test only freqof  everyday or if freq is the same and is neither are set to off
        if((comfortTimes[i].Freq == 3 || comfortTimes[i].Freq == comfortTimes[x].Freq) && comfortTimes[i].Freq != 0)
           {
             if((comfort_on_sec[i] > comfort_on_sec[x]) && (comfort_off_sec[x] < comfort_on_sec[i])){ 
              conflict = false; // no conflict exists so move on to next number in for loop
             }
             else if((comfort_on_sec[i] < comfort_on_sec[x]) && (comfort_on_sec[x] > comfort_off_sec[i])){
              conflict = false; // no conflict exists so move on to next number in for loop
             }
             else// if neither test is true return false and break for loop 
              {Serial.println("conflict exists");
               conflict = true;  // return true to the main for loop.
               break;            // break the sub [x] for loop as a conflict was found don't test any further
               }
           }//end if  
        } // end sub [x] for loop      
    } // end if conflict test
    else break; // break the main [i] for loop as a conflict was found and conflict returned true
 } // end main [i] for loop

   if(conflict == true){ 
    Serial.println("conflict exists");
   }
   else Serial.println("no conflict");    
        
}// end function



void setup() {
  // put your setup code here, to run once:
Serial.begin(9600);
CheckConflict();
Serial.println(conflict);

}

void loop() {
  // put your main code here, to run repeatedly:

}

spa_controller_V6.ino (61.3 KB)

You have to reset conflict at the begin of CheckConflict(). I'd suggest to omit the global variable and let CheckConflict() or better hasConflict() return true/false for the test result.

I cannot identify any memory hog in your code, perhaps it's in some library code?
An excess println("conflict exists") can be removed.

What bothers me most is the 2 units time system.

There are 1440 minutes a day. 1440 / 8 = 180.. I can store 1 bit for every minute of the day in 180 bytes.

If I were to write a schedule editor, it would keep conflicts from being entered in the first place.
If the admin/user is not perfect then the editor must be.

So with a 180 byte map of minutes, T if comfort and F if not, it becomes easy to check what was already set or not.

Hour * 60 + minute = number of any particular minute = which minute.

Which minute / 8 = byte number
Which minute % 8 = bit number

The user enters a start hour and minute, that can be error checked to never start on a comfort minute.
The user enters an end hour and minute, that can be error checked and every minute-bit between to never conflict.

If you have an RTC through an interrupt every 1 minute, a minute based system could run on 1440 bits.

If you keep one 180 byte array for weekdays and another for weekends and fill in the bits of good entries then those 2 arrays are the schedule. Every minute is it off or on and every minute tick tock, that simple.

That's how I might do it. I'd want to be real sure it's synch with real time somehow, just show the hour would do fine.

@DrDiettrich

Resetting the "conflict", yip definitely would have found that eventually :slight_smile: I will make sure that is in there.

Just so you know I only have been looking a Arduino since last Christmas and really only seriously since the New Zealand Lockdown 7 weeks ago.

I cannot seem to get it to work as a "bool". Every time I do it fails because the variable "conflict" is not declared, so it is a scope issue that I haven't got my head around yet. If I declare Conflict as a local variable I end up in a loop where it resets itself all of the time and I can't "return" the result to the calling function. As you can see my knowledge of some of the fundamentals is lacking but my ambition has overtaken my ability :slight_smile:

@GoForSmoke
Thanks for that, that has given me something to think about. currently I have in the main program a very basic system where the use enters the hours (as a byte) and then minutes (as a byte) so it is not a time. but then is compiled via a mathematical formula to work in seconds. (see my comment above, still learning)

I am using a RTC and the Timelib.h so i can calculate seconds past the previous midnight. That was my rational. Interups are not something I have looked at yet.

The check for a conflict would be run at the time a comfort time was changed which would bounce them back to try again (or maybe an option to accept the error as the next time will be edited, I would just need to work out a timeout to revert to original if the error wasn't corrected)

Cheers
Al

The interrupt happens when the RTC toggles an input pin if a pin-change interrupt is used. The interrupt code runs, setting a variable and then the regular sketch resumes and eventually comes around to if (flag==1) and the sketch knows another minute just rolled in so it checks the array bit for that minute and changes the system if it doesn't match, ie if the comfort is off and should be on then turn it on but if it should be off then do nothing.

Leave the RTC to tell the sketch when it's time every minute and you don't have to calculate time, just wait for the signal.

I'm not entirely sure what you mean.

The spa is setup using a series of four "dumb" bytes for "on hrs" "on min", "off hrs" "off min" which are then converted to a seconds past midnight (and I know that means the program won't work if a time spans midnight). If I converted it to an actual time via the time lib I couldn't work out how to test if a particular point in any given time was between to other points in time using HH:MM:SS. Converting to seconds past midnight was the best I could come up with after days of trawling the forum. This is governed by if the spa is "in use" and then goes into "exit delay" once the exit delay is completed it has to determined if it should be in comfort mode or economy mode.

The sketch (in the full attached file) uses the RTC to determine the actual time in seconds and then see if it is within the limits of the comfort time in seconds, if not it is in economy mode. Guaranteed it is not the most efficient code, but I'm not sure what direction you are trying to point me in. I want to make efficiencies in the code but I am a newbie to programming so I might need some examples or a keyword to google or post thread to read. The code I posted was more about how to improve that code to see if that the times entered via the four "dumb" bytes and then converted to seconds and then checked could be improved upon (within reason for someone trying to learn better ways)

below is the code of how the full program checks if the time entered is within the actual time using the RTC.

// Check if current time is within Comfort Time

void Check_for_Times()
{
  time_t curTime = now();

  unsigned long curSec = elapsedSecsToday(curTime);  // set variable to current secs

  for (byte i = 0; i < 6; i++)
  {
    //is the comfort period set > 0
    if (comfortTimes[i].Freq != 0) // if Freq is not 0 - 0 being off/not set
    {
      if (weekday() == 1 || weekday() == 7 && comfortTimes[i].Freq == 1)
      {
        if (curSec > Comfort_on_Sec[i] && curSec < Comfort_off_Sec[i])
        {
          StandbyFlag = true;
          break;
        }
        else StandbyFlag = false;
      }

      else if (weekday() > 1 && weekday() < 7 && comfortTimes[i].Freq == 2)
      {
        if (curSec > Comfort_on_Sec[i] && curSec < Comfort_off_Sec[i])
        {
          StandbyFlag = true;
          break;
        }
        else StandbyFlag = false;
      }
      else
      {
        if (curSec > Comfort_on_Sec[i] && curSec < Comfort_off_Sec[i])
        {
          StandbyFlag = true;
          break;
        }
        else StandbyFlag = false;
      }
    }

  }// end For loop

  if (StandbyFlag == true)
  {
    mState = m_StandBy;
  }
  else mState = m_Economy;

}// end Check_for_Times

You can get some memory back easily by using the F macro to put some string literals in progmem.

e.g. this:

              lcd.print(" Edit Comfort 1 ");

becomes:

              lcd.print(F(" Edit Comfort 1 "));

Another minor thing - seconds since midnight will fit in an unsigned int.
Edit: ignore this last. Throughly debunked by Awol below.

wildbill:
Another minor thing - seconds since midnight will fit in an unsigned int.

...but only until about ten past six in the evening.

TheMemberFormerlyKnownAsAWOL:
...but only until about ten past six in the evening.

Hmmm, apparently there was a low coffee warning that I ignored - thanks!

However, now that I'm thinking about it again, minutes since midnight will fit in two bytes and for spa timing, I wouldn't think the seconds are relevant :slight_smile:

Thanks Wildbill I'll explore the F macro - that's a new one for me.

You are right seconds are irrelevant for this purpose. I could tweak everything to be in minutes.

I also think I could move some of my UL timingMillis in the full code to be local rather than global. (and check all of my variables are actually used as the code has "evolved" over time)

Another Question. I am new to strings, I have defined a few in the global variables.
char st_DispL1[17];
char st_DispL2[17];
char st_Time[17];
char st_Date[17];
char st_EditValue[3]; // use to make numbers being edited blink

I did have a lot more but am I correct that if I have a single char string with a length of 17 I can just reuse that char to display what is needed at the time. Example.

above I have st_DispL1 and st_DispL2

and then just used it as needed

sprintf (st_DispL1, ("EcoSpa %2d:Set %2d"), CurWaterTemp, comfTemp);
lcd.print(st_DispL1);

or
sprintf(st_DispL1, ("Freq %s "), FreqTxtFull[editFreq]);
lcd.print(st_DispL1);

Cheers
Al

F() macro wasn't as hard as I thought it would be. Worked a treat :slight_smile:

"Sketch uses 17744 bytes (55%) of program storage space. Maximum is 32256 bytes.
Global variables use 1087 bytes (53%) of dynamic memory, leaving 961 bytes for local variables. Maximum is 2048 bytes."

What I did find though is if I remove 2 unsigned long variables it made no difference to the size of Global variables. I was expecting to gain 8 bytes ?

With the 2 variables in it is 1087, if I comment them out it is still 1087 ?

//unsigned long questionMillis;
//unsigned long switchMillis;

Maybe you weren't using them

Thanks FormerlyAWOL

Thats good to know, if you declare a whole lot of variables only use 1 the compiler works out they are not used. As you can see I'm a newbie to programming.

Allan_Pritchard:
I'm not entirely sure what you mean.

The spa is setup using a series of four "dumb" bytes for "on hrs" "on min", "off hrs" "off min" which are then converted to a seconds past midnight (and I know that means the program won't work if a time spans midnight).

As long as the next morning picks up at midnight your time should be able to span.

You have data stored in two user units that you convert to second-of-the-day to compare ranges for conflicts.

You should store and work with one unit that you convert for the user to read or enter. The work may all be in fast dependable integers while the results show and read floating point.

Why go to seconds when minutes are good? The user schedule is hours and minutes. That's when I calculated number of minutes and knew the whole day to the minute could be stored in 180 easy-change bytes.

Thanks GoForSmoke

I'll need to research a lot more on that but you have given me some key words to trawl the forum with, I only mastered blink without delay in March so I have a long way to go.

Yes you are absolutely right minutes would have been fine, I only chose to work in seconds because it seems most programming works at milliseconds and 1000 is a nice decimal number. Plus the timeLib had "seconds past midnight" so I put my blinkers on and just worked with seconds rather than thinking about it properly. I'm going to go back through and change it.

I have integrated in the checking function and finally have it showing two LCD screens with a delay to say there was an error followed by what times were in error. (Comfort 2 and Comfort 5 for example) Thanks to WilBill my global variable space is down.

Attached is where I got to on that function which seems to work within the full code. I feel there would be a far cleaner and elegant way to achieve the same result (true or false) using the current data and methodology provided to the function. Like trying to speak German but you only know a few words and constructs.

bool CheckConflict()
{
  byte i;
  byte x;
 
 for(i = 0; i < 6; i++){ // cycle through the list 0 - 6


  if(conflict == false){
    
      for (x = i+1 ; x < 6; x++ ) {  // for loop with reducing tests per loop

        // check to see if the times are conflicting freq either is an every day or the same wekkkds or weekdays
        if((comfortTimes[i].Freq == 3 || comfortTimes[i].Freq == comfortTimes[x].Freq) && comfortTimes[i].Freq != 0)
           {
             if((Comfort_on_Sec[i] > Comfort_on_Sec[x]) && (Comfort_off_Sec[x] < Comfort_on_Sec[i])){ 
              conflict = false; // no conflict exists so move on to next number in for loop
             }
             else if((Comfort_on_Sec[i] < Comfort_on_Sec[x]) && (Comfort_on_Sec[x] > Comfort_off_Sec[i])){
              conflict = false; // no conflict exists so move on to next number in for loop
             }
             else// if neither test is true return false and break for loop 
              {
              //Serial.println("conflict exists");
              previousMillis = currentMillis;
               conflict = true;  // return true to the main for loop.
               break;            // break the sub [x] for loop as a conflict was found don't test any further
               }
           }//end if  
        } // end sub [x] for loop      
    } // end if conflict test
    else break; // break the main [i] for loop as a conflict was found and conflict returned true
             
 } // end main [i] for loop

 if(conflict == true){

  byte Off1 = i-1; // Off1 has to be less one due to the main loop always running one extra cylce
  byte Off2 = x;

  //Serial.print("i=");
  //Serial.println(Off1);
  //Serial.print ("x=");
  //Serial.println(Off2);
  
    for(byte LCD = 0; LCD < 2;)
    { 
     if (LCD == 0) // first time through the loop
      {
        if(currentMillis < previousMillis + readLcdDelay) // delay to read LCD text
           {
            lcd.setCursor(0,0);
            lcd.print(F(" Conflict Exists "));
            lcd.setCursor(0,1);
            lcd.print(F(" Between Times "));
            //Serial.println("first loop");
            currentMillis = millis(); 
           }
           else 
           {
            previousMillis = currentMillis;   // reset the timer
            LCD++;    // advance the counter for the next LCD screen
           }
      } // end first if
      
     else if(LCD == 1)                                  // second time through the loop
      {
        if(currentMillis < previousMillis + readLcdDelay)
           {
            lcd.setCursor(0,0);
            sprintf(st_DispL1, "Comfort %1d is off", (Off1+1)); // add one as comfort times for user are 1-6
            lcd.print(st_DispL1);
            lcd.setCursor(0,1);
            sprintf(st_DispL2, "Comfort %1d is off", (Off2+1));
            lcd.print(st_DispL2);
            //Serial.println("second loop");
            currentMillis = millis();
           } 
           else
           {
            previousMillis = currentMillis;   // reset the timer
            LCD++;  
           }
      }// end second if
    }// end for loop
    
    comfortTimes[Off1].Freq = 0;  // set the conflicting times to off
    comfortTimes[Off2].Freq = 0; 
    conflict = false;             // set conflict to false as the times are off       
      
    //Serial.println("conflict Removed");
    //Serial.println(comfortTimes[Off1].Freq);
    //Serial.println(comfortTimes[Off2].Freq);

    return true;                // return true to calling state which will terminate that state back to mState
   }
   else
   {
    //Serial.println("no conflict");  
    return false;              // no conflict, allow calling state to finish saving the data and exit
        
   }
}// end function

Allan_Pritchard:
Yes you are absolutely right minutes would have been fine, I only chose to work in seconds because it seems most programming works at milliseconds and 1000 is a nice decimal number. Plus the timeLib had "seconds past midnight" so I put my blinkers on and just worked with seconds rather than thinking about it properly. I'm going to go back through and change it.

With the RTC interrupting once a minute there is no need for less accurate millis.

And your endpoints code is working?

It is good you learned to not block. I had my equivalent in 1980 or 81, I'm a little more comfy with it.

Do you know bits and bit-math? You can stack up to 8 T/F conditions in a byte, 32 in an unsigned long, and operate on the bits with logic in 1 cycle.
I have a debounce routine that uses 8 bits as pin state history (instead of byte histry[8]) where each read is 500us apart for 4ms pin state history. Every read, all history bits are moved up 1 and the top bit is lost.. and the read goes into bit 0. And that's like a blink task set to happen twice a millisec.

The sketch looks for the history to be 128 to see a stable button press, 127 to see a release, 0 when held down, 255 up. That's how easy it is to use, it is very light on cycles and fast. Note that debounce is 4ms -after- the last bounce but at least it's not waiting 20ms to check. (using a byte array would mean checking every byte, if (x[0] == 0 & x[1] == 0 &&... x[7] == 1) { press detected } else if (yup, all over again but bits are so hard!) { dontcha knowit }.

Really, I try to give you these tools to save you time on sketches!

When you get bits it's time to get into addresses with pointers. You already use them, the name of every array is a pointer and you can index from that name. Pointers are array power tools, very direct one.

PS - Arduino Playground - BitMath

Thanks

I know "of" bitmath but now how to use it in a sketch, I have examples of bit manipulation in some of the code I have used. It is on my to do list to find examples that I can apply.

Here is an example of where my learning falls over, and you can see my thought process.

Part of my sketch blinks a value on screen. This was taken from an LCD clock that blinked hrs, min, secs when being edited and works great. But the blink only blinks 2 characters. I know that in theory I can blink any length of value, possibly by determining the length of the value passed to the blink function and possibly by using bitmath to turn that value into the same length of null characters.

Then the hours of research begins on how to do it, of which I may come up with an ugly but working solution or stumble across a bit of working code I can pull apart and learn (I have already glanced through some posts). For you, you have probably already solved it in your head. :slight_smile:

Cheers
Al

//////////////////////////////////////////////////////////////////////////
// Make value blink on screen
//////////////////////////////////////////////////////////////////////////
void flashValue(byte val, byte column, byte line)
{
static unsigned long lastTime = 0; // Static variables preserves data between function calls.
static bool flashOn = false;
if (millis() - lastTime > 500UL)
{
if (flashOn)
{
lcd.setCursor(column, line);
sprintf(st_EditValue, ("%02d"), val);
lcd.print(st_EditValue);

}
else
{
lcd.setCursor (column, line);
lcd.print(" "); //<<<<------------- need to make this the same size as val, and change val to an int rather than a byte to be more versatile.
}
flashOn = !flashOn;
lastTime = millis();
}
}

Allan_Pritchard:
Thanks

I know "of" bitmath but now how to use it in a sketch, I have examples of bit manipulation in some of the code I have used. It is on my to do list to find examples that I can apply.

Here is an example of where my learning falls over, and you can see my thought process.

Part of my sketch blinks a value on screen. This was taken from an LCD clock that blinked hrs, min, secs when being edited and works great. But the blink only blinks 2 characters. I know that in theory I can blink any length of value, possibly by determining the length of the value passed to the blink function and possibly by using bitmath to turn that value into the same length of null characters.

Then the hours of research begins on how to do it, of which I may come up with an ugly but working solution or stumble across a bit of working code I can pull apart and learn (I have already glanced through some posts). For you, you have probably already solved it in your head. :slight_smile:

Cheers
Al

It only blinks 2 digits at a time so you know which value you are adjusting. The rest should stay onscreen.

You forgot the [code] [/code] tags for your code.

//////////////////////////////////////////////////////////////////////////
// Make value blink on screen
//////////////////////////////////////////////////////////////////////////
void flashValue(byte val, byte column, byte line)
{
  static unsigned long lastTime = 0;     // Static variables preserves data between function calls.
  static bool flashOn = false;
  if (millis() - lastTime > 500UL)
  {
    if (flashOn)
    {
      lcd.setCursor(column, line);
      sprintf(st_EditValue, ("%02d"), val);  //  <<== But the blink only blinks 2 characters. Here's the code!
      lcd.print(st_EditValue);                      //  So what's your point? You need more code to print more, not bitmath. 

    }
    else
    {
      lcd.setCursor (column, line);
      lcd.print("  ");        //<<<<---- This erases 2 chars because only 2 are printed.  
    }
    flashOn = !flashOn;
    lastTime = millis();
  }
}

Do you have trouble with sprintf()? The reference is in AVR libc, in the stdio.h library. LibC is the standard C libraries that Arduino uses. That site is the doc.

oops I'm usually very careful to use code tags. Sorry

sprintf is something I have only just learned and have converted the code from Strings() to strings. I will admit I am struggling a little bit but have found some good c++ sites tonight. Aurduino site is a bit light on how to use sprintf properly.

Yes the code only blinks 2 values and the rest stay the same (works for my lcd clock). I want to adapt it to blink any value from 1 to 16 characters (length of my current LCD) so it becomes a helper function that I can use anywhere.

I literally have just learned tonight that %02d is (and I hope I am right) is print a minimum of 2 characters and pad by zero.

So I need to work out the if a value is 1 or 12345678 then how the sprintf can adapt between the two lengths. I also need to to take the value of 1 or 123456 and turn it to _ or ________ for the blink. Which as i intimated, my beginner brain is thinking should I be looking at bitmath to turn the same length value printed to all 0's. Maybe I'm looking at the wrong approach. Whats the right approach?

I'm not looking to be spoon fed answers but any examples are helpful and keywords help me research.

Cheers
Al