How do you use a struct of configuration variables?

I was drawn into another discussion -- Use another function during the running of Millis -- and wrote some code, but my disparate set of global configuration variables seems sloppy. I think it would be better to use a configuration structure instead, along with an array of different configurations.

#include <limits.h>
typedef struct  config_t {
  int id;
  char * name;
  int delay_interval;
  int angle;
  int wait_interval;
};

config_t configurations[] = {
  {0, "near", 3000, 45, 3000},
  {1, "far", 10000, 90, 3000},
  {2, "zero",0,0,0},
  {3, "ULONG_MAX",ULONG_MAX,0,0}
  //...
};
config_t configuration;

//...
void setup(){
   Serial.begin(115200);
   configuration = configurations[0]; 
   Serial.println(configuration.name);
   //...
}
void loop(){;}

Would you have advice or a reference on how to save, update, and use structured configurations like this? Or a better pattern to learn?

My full code with the sloppy global config variables:

// DaveX 2022-02-16 CC BY-SA 
// For https://forum.arduino.cc/t/use-another-function-during-the-running-of-millis/959087/17
//

#include<Servo.h>
#define maxdistance 100

unsigned long currentMillis = 0;
Servo srv;
int i;

const int ledPin = LED_BUILTIN;
int ledState = LOW;

void setup()

{
  Serial.begin(9600);
  pinMode(6, OUTPUT);
  pinMode(5, INPUT);
  srv.attach(7);
}

// daveX:
enum states {IDLE, DELAY, TURN, STAY, RETURN} sanitizer_state = IDLE;
enum configs {NEAR, FAR} config = NEAR;
unsigned long delay_interval, stay_interval;
int turn_angle, distance;

int read_distance() {
  distance = pulseIn(5, HIGH) / 29 / 2;
  distance = 4;
  return distance;
}

void setNearConfig() {
  config = NEAR;
  delay_interval = 2000;
  turn_angle = 45;
  stay_interval = 3000;
}

void setFarConfig() {
  config = FAR;
  delay_interval = 10000;
  turn_angle = 90;
  stay_interval = 3000;
}

void processSanitizer() {
  unsigned long currentMillis = millis();
  static unsigned long previousMillis = 0;
  switch (sanitizer_state) {
    case IDLE:
      if ( read_distance() <= 5 ) {
        setNearConfig();
      } else {
        setFarConfig();
      }
      srv.write(0);
      sanitizer_state = DELAY;
      previousMillis = currentMillis;
      break;
    case DELAY:
      if ( currentMillis - previousMillis > delay_interval) {
        sanitizer_state = TURN;
      }
     // Serial.println(currentMillis - previousMillis);
      break;
    case TURN:
      srv.write(turn_angle);
      previousMillis = currentMillis;
      sanitizer_state = STAY;
      break;
    case STAY:
      if (currentMillis - previousMillis > stay_interval) {
        sanitizer_state = IDLE;
      }
      break;
    default:
      Serial.println("Default case");
      ;
  }
}

void report() {
  const long interval = 1000;
  static unsigned long prev = - interval;
  if (millis() - prev < interval )
    return;
  prev += interval;
  Serial.print(sanitizer_state);
  Serial.print(config);
  Serial.print(distance);
  Serial.print(srv.read());
  Serial.println();
}  

void loop() {
  processSanitizer();
  report();
  // check for special handling override
  if (config == FAR && sanitizer_state == DELAY) {
    if (read_distance() < 5) { // special handling
      setNearConfig();
      srv.write(turn_angle);
    }
  }
}

Continuing the discussion from Use another function during the running of Millis:

Your example looks more like a sub-state (or child state) than a "configuration" but...

You mention "save" and "update".

I'm struggling to understand the problem you want to solve.

I am also not clear what your intention is. Do you want to keep several parallel configurations for millis() controlled activities?

If that's the case you can create a struct of the general data required for the timing, control variables and status tracking; here an example struct for Leds:

struct LEDType {
  unsigned long LastMillis = 0;
  unsigned long interval   = 500;
  boolean       isOn = false;
  boolean       letBlink = true;
  int           Pin;
};

This is a full running sketch for five LEDs which can be programmed to blink at different delays:

enum {
  IDLE,
  ALLON,
  ALLOFF,
  ZEROONE,
  ZEROTWOFOUR,
  ONETHREE,
  PROGRAMMED,
  TOGGLE1,
  TOGGLE2
} BlinkType;

int BlinkState = IDLE;

struct LEDType {
  unsigned long LastMillis = 0;
  unsigned long interval   = 500;
  boolean       isOn = false;
  boolean       letBlink = true;
  int           Pin;
};

const int NoOfLEDs = 5;

LEDType LED[NoOfLEDs];

void SetLED(int No, int State) {
  LED[No].isOn = State;
  digitalWrite(LED[No].Pin, LED[No].isOn);
}


void BlinkLedNo(int No) {
  if (LED[No].letBlink) {
    if (millis() - LED[No].LastMillis > LED[No].interval) {
      LED[No].LastMillis = millis();
      LED[No].isOn = !LED[No].isOn;
      SetLED(No, LED[No].isOn);
    }
  } else {
    if (LED[No].isOn) SetLED(No, LOW);
  }
}

void BlinkLEDs() {
  for (int i = 0; i < NoOfLEDs; i++) {
    BlinkLedNo(i);
  }
}

void AllLEDs(int Value) {
  for (int i = 0; i < NoOfLEDs; i++) {
    SetLED(i, Value);
  }
}

void StateMachine() {
  switch (BlinkState) {
    case IDLE  :
      break;
    case ALLOFF :
      AllLEDs(LOW);
      BlinkState = IDLE;
      break;
    case ALLON :
      AllLEDs(HIGH);
      BlinkState = IDLE;
      break;
    case ZEROONE :
      AllLEDs(LOW);
      SetLED(0, HIGH);
      SetLED(1, HIGH);
      BlinkState = IDLE;
      break;
    case ZEROTWOFOUR :
      AllLEDs(LOW);
      SetLED(0, HIGH);
      SetLED(2, HIGH);
      SetLED(4, HIGH);
      BlinkState = IDLE;
      break;
    case ONETHREE :
      AllLEDs(LOW);
      SetLED(1, HIGH);
      SetLED(3, HIGH);
      BlinkState = IDLE;
      break;
    case TOGGLE1 :

      break;
    case PROGRAMMED :
      BlinkLEDs();
      break;
    default : break;
  }
}

void AllowAllToBlink() {
  for (int i = 0; i < NoOfLEDs; i++ ) {
    LED[i].letBlink = true;
  };
}

void DisableToBlink(int No) {
  LED[No].letBlink = false;
}


void getSerialCommand() {
  if (Serial.available()) {
    char c = Serial.read();
    switch (c) {
      case 'A' :
        BlinkState = ALLON;
        break;
      case 'a' :
        BlinkState = ALLOFF;
        break;
      case 'P' :
        AllowAllToBlink();
        BlinkState = PROGRAMMED;
        break;
      case 'I' :
        BlinkState = IDLE;  // Leaves LED states as they are at time of key pressure
        break;
      case '0' :
        BlinkState = ZEROONE;
        break;
      case '1' :
        BlinkState = ZEROTWOFOUR;
        break;
      case '2' :
        BlinkState = ONETHREE;
        break;
      case 'T' :
        AllowAllToBlink();
        DisableToBlink(0);
        DisableToBlink(2);
        DisableToBlink(4);
        BlinkState = PROGRAMMED;
        break;
      case 't' :
        AllowAllToBlink();
        DisableToBlink(1);
        DisableToBlink(3);
        BlinkState = PROGRAMMED;
        break;
      default : break;
    }
    c = ' ';
  }

}

void setup() {
  Serial.begin(115200);
  // Just a quick initialization
  for (int i = 0; i < NoOfLEDs; i++ ) {
    LED[i].interval = 300 * (i + 1);
    LED[i].Pin = 9 + i; // Do this only in this case as 9 + 4 = 13 !!!
    pinMode(LED[i].Pin, OUTPUT);
  }
  AllLEDs(HIGH);
  delay(500);
  AllLEDs(LOW);
  Serial.println("Use keys A,a, P, I, 0, 1 ,2 , T, t on Serial to switch LED control");
}


void loop() {
  getSerialCommand();
  StateMachine();
}

Feel free can test it on https://wokwi.com/arduino/projects/323055484656419410

You can use Serial to interact with the state machine.

1 Like

The id field can be removed. It is always equal to the array index.

1 Like

I might think of the "configuration" as a parent state, or as global vector of state variables, on which different subsystems might have their own loosely coupled processes and state machines making use of the global state.

The exercise of typing the typedef/struct/setup/loop into the executable code helped me think about it.

      if ( read_distance() <= 5 ) {
        setNearConfig();
      } else {
        setFarConfig();
      }

Might become

      if ( read_distance() <= 5 ) {
        useDonfig = NEAR;
      } else {
        useConfig = FAR;
      }

if I see where you are headed. That will work and I see it elsewhere and have done the same.

If you do mean update might change the literal character constants in the struct, you should make it instead a character array certain to be big enough instead of a pointer.

Also, yes, the first member is just the index now so can be omitted.

I usually put the index number in a comment next to the line that initialises a particular array element so I can easily see what the index is as well as how many I am jukkling, but no need to have it in there for the code.

All opinion really or a matter of taste.

a7

1 Like

What you're doing is sensible, but could do with a small amount of OOP. Rather than having a configuration variable, which is a copy of one of the available configurations, wrap the whole thing in an object. The caller select WHICH configuration is "active", and then accesses, through setters and getters, the individual elements of the current configuration. This hides the structure itself, allowing it to be changed/updated as needed down the road. For persistence, simply save the configurations array to eeprom, or some other non-volatile storage.

What you have is similar to what I always do for tracking configuration data, with the exceptions noted above. In fact, storage space allowing, I generally store it all in human-readable text format, as key-value pairs, to make it easier to maintain and update manually during development.

1 Like

This helps -- If I were trying to combine a multiple sketches, e.g. your LED state machine with my state machine for the other thread's sanitizer, and BlinkWithoutDelay, there's a set of globals specific for each sketch that you'd need to protect from name-clashes, etc.. For your LED SM, there's {NoOfLEDs, LED[], BlinkState, etc...} and for my sanitizer SM, there's the ones in {config, sanitizer_state}. In a sketch that tried to combine the two, (or N different sketches), you wouldn't want to rewrite everything into one large, flat state machine, but into loosely coupled subsystems that are managed/adjusted by a parent process.

I think I'm looking for advice on how to group the global variables from a sketch/SM into a collection that would make it easy to merge the sketch with other sketches and be managed from a parent process.

Yes. A key idea is to get stuff functioning and then hide it. From yourself, keep the work area uncluttered so to speak.

Unimaginable hair hides in many Arduino libraries.

I like to take care of my own hair, just because I get satisfaction from knowing I controlled everything down to the lowest level possible.

But it is still nice to wrap something up that might never need looking into again.

a7

1 Like

Yes, I think I'm looking for advice and refs on adding a small amount of OOP. I'm an engineer who learned K&R C ages ago, and sort of missed actually learning OOP.

Adapting BWOD to provide a feature and then needing to re-write it as an object or library in order to combine it with other functionality seems a bit daunting. I think I'm aiming at the first step of OOP of identifying and grouping the attributes together, but still leaving them globally accessible for sloppy, stick-built, un-abstracted interaction with other processes.

But a simple application like this is an ideal place to LEARN OOP. And it WILL save you a lot of time and effort in the long term. Start very simple. Create the object, with setters and getters, and nothing more. Get that working. Then add the few functions to fetch/store the configurations to the NV memory. Just those few things will get you well up the curve on the structure of a "library" and the benefits they bring. Once you've written a whole application using non-OOP models, making the change becomes an order or magnitude more work.

1 Like

I have just spent a few minutes with my friend google

   c structure c++

and

  struct vs class c++

both led to a website called geeksforgeeks.org and look reasonably gentle.

It can be hard to find good simple examples of these techniques, dunno why but I guess ppl who can exploit them aren’t writing for us to learn from, nor necessarily to show off, but TBH some of the C++ stuff is intimidating and I know it is close-minded of me, but tots overkill in the context of teeny programs.

Since I did quite a bit with microprocessors having only 1/16 the resources available on even the UNO, I feel like I’m taking a bath in a swimming pool. If I keep to the things I know to do the things I get up to, I’ll never run out of anything.

The ability to express things in a standard OOP manner doesn’t mean you have to write too much differently to old C and K&R, which after all we’re not major impediments to using the same ideas.

a7

1 Like

Thanks. I found that struct vs class in C++ - Embedded Software was interesting:

The only difference between a struct and class in C++ is the default accessibility of member variables and methods. In a struct they are public; in a class they are private.

struct MyStruct {
   int id;
   char * name;
   int delay_interval;
   int angle;
   int wait_interval;
   MyStruct(){};
   MyStruct(int angle, int delay_interval, int wait_interval){
     this->angle = angle;
     this-> delay_interval = delay_interval;
     this->wait_interval = wait_interval;
   };
   printAngle(){Serial.println(angle);}
};

class MyClass {
  public:
   int id;
   char * name;
   int delay_interval;
   int angle;
   int wait_interval;
   MyClass(){};
   MyClass(int angle, int delay_interval, int wait_interval){
     this->angle = angle;
     this-> delay_interval = delay_interval;
     this->wait_interval = wait_interval;
   };
   printAngle(){Serial.println(angle);}
};

//globals
MyStruct config_s;
MyClass config_c;

MyStruct config_s1 = MyStruct();
MyClass config_c1 = MyClass();

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
   config_s.angle = 55;
   config_s.printAngle();

   config_c.angle = 101;
   config_c.printAngle();

   config_s1.angle = 101;
   config_s1.printAngle();

   config_c1.angle = 102;
   config_c1.printAngle();
}

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

}

If one is tempted to use a struct or an array of structs for configuration/state variables, one might as well use a class. The syntax is nearly the same, and the documentation is richer.

There is more to it: A class can include public and private functions, the concept of inheritance and the possibility to override functions and properties ...

Structs usually handle data (of course one can use pointers to functions in a struct also ... inventing the "class wheel" again :wink: ).

Nope. Read what @DaveX wrote:

1 Like

It appears that in C++, a struct can have public and private data and functions. Perhaps they implemented structs by inheritance from class? You could put function pointers in either structs or classes as data, but the compiler seems to allow functions within classes or structs without a storage as data.

I'm by no means an authority on c++, but my sample code in #13 gets the same behavior out of a struct and a class with functions in it. I don't know how to do overrides or inheritance, but those may have the same possibilities with c++ structs as one does with c++ classes.

1 Like

A word of warning: The remaining fields are not initialized. They will have whatever value happens to be on the stack or heap depending on where the instance is placed.

1 Like

Nor am I, as you've seen ;-))

Just learned (and that's what this platform is good for)

The difference that really matters between struct and class boils down to one thing: convention .

from https://www.fluentcpp.com/2017/06/13/the-real-difference-between-struct-class/

So thanks to you and the other "corrective" forces here :slightly_smiling_face:

1 Like

I wonder if an array of classes can be initialized like an array of structs?

Yes.

1 Like