Set a sequence of servo positions

Hi. Sorry to say I'm not a programmer but am using some excellent code posted by user 'Photo-Tom' 8 years ago.
The code works perfectly well and I have no problem with my mechanics, circuit etc, it's all working nicely for a daft halloween project I'm building. The PIR sensor triggers my servo, moves it to position one, pauses, moves it to position 2 and returns to idle position to await the next 'HIGH' from the PIR.
To really make it special I'd like to add more servo positions to the sequence, I thought it would be easy (I've dabbled a bit with Basic, DOS etc many years ago) but I'm struggling to make any sense of it.

Here's the original code, I've since modified the timing a bit to suit my needs, but my question is 'how can I add more servo positions to the sequence?'
Apologies in advance if I've made any 'faux pas' in what I'm posting or how I've posted it, post #1 for me on this forum. Hoping someone can help me out.
Thanks, Rob

#include <Servo.h>


#define S_IDLE 1
#define S_MAXA 2
#define S_MID 3
#define S_MAXB 4
#define S_WAITING 5
#define S_FINISH 6

Servo myservo;
int PIR_SensorPin = 8;
unsigned long duration;
unsigned long lastMillis;
int calibrationTime = 30;
int state = S_IDLE;

void setup()
{
  Serial.begin(9600);
  pinMode (PIR_SensorPin, INPUT);   // Set pinMode
  digitalWrite(PIR_SensorPin, LOW);
  myservo.attach(9, 900, 2100);  // servo.attach(pin, min, max)
  myservo.writeMicroseconds(1500);  // set servo to mid-point

  Serial.print("calibrating sensor ");
  for (int i = 0; i < calibrationTime; i++) {
    Serial.print(".");
    delay(1000);
  }
}

void loop() {

  if (millis() - lastMillis > duration) { // Step in sequence finished - time to take action!
    lastMillis = millis();
    switch (state) {
      case S_MAXA:
        myservo.writeMicroseconds(2100); // turn servo to new position
        duration = 200; // Duration of this position
        state = S_MID; // Next step of the sequence - runs when duration has passed.
        break;

      case S_MID:
        myservo.writeMicroseconds(2000); // turn servo to new position
        duration = 200; // Duration of this position
        state = S_MAXB;  // Move to the next state
        break;

      case S_MAXB:
        myservo.writeMicroseconds(2100); // turn servo to new position
        duration = 200; // Duration of this position;
        state = S_WAITING; // Move to the next state
        break;

      case S_WAITING:
        myservo.writeMicroseconds(1500); // return servo to netural position
        duration = 10000; // Wait for moving on.
        state = S_FINISH;  // Move to the next state
        break;

      case S_FINISH:
        state = S_IDLE; // Done - time to look at the PIR sensor again.
        break;
    }

    // General state: only look at the sensor when IDLE.
    if (state == S_IDLE) {
      if (digitalRead(PIR_SensorPin) == HIGH) {
        state = S_MAXA;
        duration = 0;
        lastMillis = millis();
      }
    }
  }

Welcome to the forum

The first thing that I would do is to add a case for S_IDLE into the switch/case. As you are already using switch/case it makes no sense to use an if to test for a particular state outside of it

As to adding more servo positions, add more states to the switch/case with the appropriate values written to the servo

To add positions, follow your format...

Add a "#define" (you should read about "enum" for easy "re-numbering" if needed)

#define S_WHAT 7

Then make another case...

      case S_WHAT:
        myservo.writeMicroseconds(1000); // WHAT position
        duration = 1000; // Duration of this position
        state = S_MAXB;  // Move to the next state <== decide which is next state
        break;

And copy/paste into your sketch.

By the way: this does nothing more than print dots for thirty seconds:

  Serial.print("calibrating sensor ");
  for (int i = 0; i < calibrationTime; i++) {
    Serial.print(".");
    delay(1000);
  }

@xfpd clarification - S_WHAT will need to be substituted for S_MAXB at the end of case S_MID as well.

The code as posted needs one more } (total of 4) at the end, in order to compile.

Thanks so much for the replies. To be perfectly honest I'd tried @xfpd's suggestion as a sort of 'cookie cutter' solution, this is about the limit of my coding ability, but it didn't change the sequence of sweeps. Thanks for the tips re the unneeded code BTW.
I've defined some additional 'BUMP's (the servo is supposed to bump a lid open and bang it shut again) and added them to the sequence with a unique 'WAITING' period between each that should return the servo to its start position.
Do the numbers have any significance when adding definitions? i.e. 'S_IDLE 1', is the number arbitrary? Also, does the sequence of defining have any effect or can definitions be in a disordered list? I'm just trying to rule a few things out. I'll tidy things up once I know I'm on the right track.
The revised code makes no difference to the servo movement. I purposely added long delays to make the sequence more obvious. It seems to skip over my added positions (BUMP and WAITING), almost as though my changes aren't updating the board, is this even possible? Weird.
Thanks again, Rob

#include <Servo.h>


#define S_IDLE 1
#define S_BUMPA 2
#define S_WAITINGA 3
#define S_MAXB 4
#define S_WAITING 5
#define S_FINISH 6
#define S_MAXA 7
#define S_BUMPB 8
#define S_BUMPC 9
#define S_MID 10
#define S_WAITINGB 11

Servo myservo;
int PIR_SensorPin = 8;
unsigned long duration;
unsigned long lastMillis;
int calibrationTime = 15;
int state = S_IDLE;

void setup()
{
  Serial.begin(9600);
  pinMode (PIR_SensorPin, INPUT);   // Set pinMode
  digitalWrite(PIR_SensorPin, LOW);
  myservo.attach(9, 900, 2300);  // servo.attach(pin, min, max)
  myservo.writeMicroseconds(1500);  // set servo to mid-point

  Serial.print("calibrating sensor ");
  for (int i = 0; i < calibrationTime; i++) {
    Serial.print(".");
    delay(1000);
  }
}

void loop() {

  if (millis() - lastMillis > duration) { // Step in sequence finished - time to take action!
    lastMillis = millis();
    switch (state) {
      case S_BUMPA:
        myservo.writeMicroseconds(1650); // turn servo to new position
        duration = 1000; // Duration of this position
        state = S_WAITINGA; // Next step of the sequence - runs when duration has passed.
        break;

      case S_WAITINGA:
        myservo.writeMicroseconds(1500); // return servo to netural position
        duration = 1000; // Wait for moving on.
        state = S_BUMPB;  // Move to the next state
        break;

      case S_BUMPB:
        myservo.writeMicroseconds(1750); // return servo to netural position
        duration = 20000; // Wait for moving on.
        state = S_WAITINGB;  // Move to the next state
        break;

      case S_WAITINGB:
        myservo.writeMicroseconds(1500); // return servo to netural position
        duration = 5000; // Wait for moving on.
        state = S_MAXA;  // Move to the next state
        break;
  
      case S_MAXA:
        myservo.writeMicroseconds(1900); // turn servo to new position
        duration = 200; // Duration of this position
        state = S_MID; // Next step of the sequence - runs when duration has passed.
        break;

      case S_MID:
        myservo.writeMicroseconds(2000); // turn servo to new position
        duration = 2000; // Duration of this position
        state = S_MAXB;  // Move to the next state
        break;

      case S_MAXB:
        myservo.writeMicroseconds(2300); // turn servo to new position
        duration = 1000; // Duration of this position;
        state = S_WAITING; // Move to the next state
        break;

      case S_WAITING:
        myservo.writeMicroseconds(1500); // return servo to netural position
        duration = 2000; // Wait for moving on.
        state = S_FINISH;  // Move to the next state
        break;

      case S_FINISH:
        state = S_IDLE; // Done - time to look at the PIR sensor again.
        break;
    }

    // General state: only look at the sensor when IDLE.
    if (state == S_IDLE) {
      if (digitalRead(PIR_SensorPin) == HIGH) {
        state = S_MAXA;
        duration = 0;
        lastMillis = millis();
      }
    }
  }
}

I think that in each case where you set a duration to time the switch into the next case you also need to set lastMillis = millis().

More than one state can point at the same state, just be aware to avoid creating an infinite loop.

Also... "cases" do not need to be in numerical order, just keep their values different.

The enum is an example of "keeping their values different." This is the simplest way. The top variable gets the value "0" unless assigned another value, and each successive variable gets assigned the next number. You can start with any number you want or skip numbers... just look up "enum c++" and you will get lots of information.

enum { // automatic numbering (enumeration)
  S_IDLE,   // 0
  S_MAXA,   // 1
  S_MID,    // 2
  S_MAXB,   // 3
  S_WHAT,   // 4 <== added
  S_WAITING,// 5
  S_FINISH  // 6
};

#include <Servo.h>
Servo myservo;
int PIR_SensorPin = 8;
unsigned long duration;
unsigned long lastMillis;
int calibrationTime = 30;
int state = S_IDLE;

void setup()
{
  Serial.begin(9600);
  pinMode (PIR_SensorPin, INPUT);   // Set pinMode
  digitalWrite(PIR_SensorPin, LOW);
  myservo.attach(9, 900, 2100);  // servo.attach(pin, min, max)
  myservo.writeMicroseconds(1500);  // set servo to mid-point

  Serial.print("calibrating sensor ");
  for (int i = 0; i < calibrationTime; i++) {
    Serial.print(".");
    delay(1000);
  }
}

void loop() {

  if (millis() - lastMillis > duration) { // Step in sequence finished - time to take action!
    lastMillis = millis();
    switch (state) {
      case S_MAXA:
        myservo.writeMicroseconds(2100); // turn servo to new position
        duration = 200; // Duration of this position
        state = S_MID; // Next step of the sequence - runs when duration has passed.
        break;

      case S_MID:
        myservo.writeMicroseconds(2000); // turn servo to new position
        duration = 200; // Duration of this position
        state = S_MAXB;  // Move to the next state
        break;

      case S_MAXB:
        myservo.writeMicroseconds(2100); // turn servo to new position
        duration = 200; // Duration of this position;
        state = S_WAITING; // Move to the next state
        break;

      case S_WAITING:
        myservo.writeMicroseconds(1500); // return servo to netural position
        duration = 10000; // Wait for moving on.
        state = S_FINISH;  // Move to the next state
        break;

      case S_FINISH:
        state = S_IDLE; // Done - time to look at the PIR sensor again.
        break;

      case S_IDLE:
        if (digitalRead(PIR_SensorPin) == HIGH) {
          state = S_MAXA;
          duration = 0;
          lastMillis = millis();
        }
        break;

      case S_WHAT:
        myservo.writeMicroseconds(1000); // WHAT position
        duration = 1000; // Duration of this position
        state = S_MAXB;  // Move to the next state <== decide which is next state
        break;
    }
  }
}

Preaching to the choir, we call that.

I wanted OP to see another way to do stuff.

Don't you want to start from the beginning again?
If yes then S_MAXA should be S_BUMPA

That is why it's skipping everything you added.

I think you're right Jim-p. Thanks for the correction.

Hmm. Some things to think about there. I will look at 'enum'.
Jim-p has rightly noted that my sketch will skip past my BUMPs and WAITING cases and go straight to MAXA. It seems to me that the sketch should run through successfully on the first cycle though, only skipping to MAXA on the second and later cycles. It doesn't, it goes straight to MAXA on cycle #1.
I'm at the office so can't test at the moment but my plan for this evening is to wipe out the existing sketch and rebuild it one case at a time, check the result, add the next case etc. Hopefully I can at least see where it falls over.
Thanks for all the help so far.

No, it won't because the initial state is S_IDLE and only changes the first time around when the PIR is activated.

@whatiscode Consider this contribution. I've added comments, Serial output, and cleaned it up a bit. It can also be run in Wokwi, just substitute a button for the sensor input.

#include <Servo.h>

enum { // automatic numbering (enumeration)
  S_IDLE,   // 0
  S_MAXA,   // 1
  S_MID,    // 2
  S_MAXB,   // 3
  S_WHAT,   // 4 <== added, but not used in sequence right now
  S_WAITING,// 5
  S_FINISH  // 6
};

Servo myservo;
int PIR_SensorPin = 8;

int ACTIVE = HIGH;

unsigned long duration;     //=0 by default
unsigned long lastMillis;   //=0 by default
//int calibrationTime = 30;  //irrelevant code at this time
int state = S_IDLE;

void setup()
{
  Serial.begin(9600);
  Serial.println("Hello");
  pinMode (PIR_SensorPin, INPUT_PULLUP);   // Set pinMode
  //  digitalWrite(PIR_SensorPin, LOW);  //serves no purpose

  // because of the way Servo.h works, you should set the position before you enable the pin, but no one tells anyone this

  myservo.writeMicroseconds(1500);  // set servo to mid-point
  myservo.attach(9, 900, 2100);  // servo.attach(pin, min, max)

  //  the following is meaningless, code at this time
  //  Serial.print("calibrating sensor ");
  //  for (int i = 0; i < calibrationTime; i++) {
  //    Serial.print(".");
  //    delay(1000);
  //  }//ends calibration

  Serial.println("S_IDLE");  //announce the starting state
}// ends setup

void loop() {
  if (millis() - lastMillis > duration) { // Step in sequence finished - time to take action!
    lastMillis = millis();
    switch (state) {
      case S_MAXA:
        Serial.println("S_MAXA");
        myservo.writeMicroseconds(2100); // turn servo to new position
        duration = 2000; // Duration of this position
        state = S_MID; // Next step of the sequence - runs when duration has passed.
        break;

      case S_MID:
        Serial.println("S_MID");
        myservo.writeMicroseconds(2000); // turn servo to new position
        duration = 2000; // Duration of this position
        state = S_MAXB;  // Move to the next state
        break;

      case S_MAXB:
        Serial.println("S_MAXB");
        myservo.writeMicroseconds(2100); // turn servo to new position
        duration = 2000; // Duration of this position;
        state = S_WAITING; // Move to the next state
        break;

      case S_WAITING:
        Serial.println("S_WAITING");
        myservo.writeMicroseconds(1500); // return servo to netural position
        duration = 2000; // Wait for moving on.
        state = S_FINISH;  // Move to the next state
        break;

      case S_FINISH:
        Serial.println("S_FINISH");
        state = S_IDLE; // Done - time to look at the PIR sensor again.
        duration = 0; // Wait for moving on.
        break;

      case S_IDLE:
        if (digitalRead(PIR_SensorPin) == ACTIVE) {  //start sequence if active
          state = S_MAXA;
          Serial.println();  //just ends line of ...
          duration = 0;      //move immediately to first state, no delay
          lastMillis = millis();
        }//ends if ACTIVE
        else {   //just cycling, so pump out another .
          Serial.print(".");
          delay(50);
        }//ends else
        break;

        //the following is just a hangover from problem solving in thread
        //      case S_WHAT:
        //        Serial.println("S_WHAT");
        //        myservo.writeMicroseconds(1000); // WHAT position
        //        duration = 1000; // Duration of this position
        //        state = S_MAXB;  // Move to the next state
        //        break;
    }//end of switch statement
  } //end of sequence finished compound
}//end of loop

Wow! Thanks for that. Much cleaner. Will give it a run.

You were right Jim-p (although I'm sure you already knew that) changed start state to BUMPA and it worked as hoped. It seemed illogical to me to have the start state at the end of the sketch, just showing my ignorance I guess.
Great, I can add as many sweeps and waits as I need now.
Thanks! Rob.

You can have the states in any order in the switch/state. Only the code for the current state will be executed within the switch block