Timing for many different solenoid valves - fountain show

Hi,

I am undertaking a few projects at the moment for a personal art installation in my backyard, and need help controlling multiple solenoid valves. Hardware wise everything is working, but need help with my code for the timing switching solenoids. I have a water feature project which has 5 different solenoid valves that I want to go off in a sequence, lasting for maybe 15-20 seconds. For the testing and assembly I've been using the delay() function, but the code is extremely long and I would like to add some lights (with a special timing aswell aswell) in the future so I would like to understand how to do this without delay(). I understand millis() may be the way to go, or potentially a timer library, but it seems quite complicated so far. The solenoids will mostly be off, with a small jet (roughly 40ms) when they shoot.

At least to start, activating solenoids 1 through 5 in order will be fine. I would like to set it up so I can play around with the timing and make something that looks cool with as little fiddling around as possible. I'm going to outline an example of what variables and parameters I want to include, I have not been able to make my visions come true yet, but I'm hoping it makes sense. If my way isn't good, I'm very open to any other way to tackle this. I want to be able to just adjust the timing every round that jets 1-5 activate, for 4-5 rounds. To activate a solenoid I basically just need to digitalWrite HIGH for the 40ms.

static const uint8_t VALVE_ARRAY[] = {8, 9, 10, 11, 12};    //array for the valve pins 
onTime = 40                         //milliseconds on for the jet to activate 
timeBetweenJets1 =  1000         //milliseconds between the different solenoids during round 1 of the jets 
timeBetweenJets2 =  800         //milliseconds between round 2 of the jets 
timeBetweenJets3 =  600         //milliseconds between round 3 of the jets 
timeBetweenJets4 =  400         //milliseconds between round 4 of the jets 

Where do I go from here? I've tried many things but I get confused. I am using an arduino nano.

Thanks a lot!

Do you really mean "off" or do you mean "on" for 15 - 20 seconds? Very confusing.

Read about millis(), array[]#s, and possibly state-machines.

structs{} might help you tie things together for an easier to read program.

Not hard, but you need to be disciplined!

Sorry, let me clarify. I want them to "activate" in a cool visually appealing sequence. Solenoid 1 on for 40ms and then off, wait for 500ms, solenoid 2 on for 40ms and then off, wait 500ms, solenoid 3 on for 40ms and then off, wait 500ms, solenoid 4 on for 40ms and then off, wait 500ms, solenoid 5 on for 40ms and then off, wait 500ms. This is round 1. Round 2 will have the same sequence but slightly different timing.

Very doable. This requires:

  • non-blocking timing based on function millis();
  • using arrays: once you have understood what arrays are writing the code becomes much more effective
  • state-machines: once you have understood how state-machines work writing the code becomes much more effective

Depending on your C++-Programming knowledge it will take you estimated 5 to 50 hours to learn it.

Take a look into this tutorial:

Arduino Programming Course

It is easy to understand and has a good mixture between explaining important concepts and example-codes to get you going. So give it a try and report your opinion about this tutorial.

best regards Stefan

I would divide the problem into "tracks", one for each solenoid, and then pass them data that looked like (ontime, offtime) pairs.
Each "track" would then be driven by a millis()-based non-blocking code with its own running timer.

(BTW, 40ms sounds really fast for either solenoids or water, unless you've bought Disney-quality, really expensive, valves and high-pressure supply stuff. I don't know what the limits of more ordinary solenoid valves are. (let us know if you find out!) Ordinary electric sprinkler valves can take multiple seconds to turn off.)

Hi,

Thank you so much for your resources. I knew a lot of the information in the general arduino tutorial already but it filled in some holes. I have spent the last day learning about these things.

I would like to ask if I am going about this correctly. I could write a state machine with 5 states/cases, one state for each solenoid. Each state would check if a specific timing (on-off) timestamp has been reached for a specific solenoid, and act accordingly. This way if I want to expand and include the timing of other things (such as lights) its as simple as adding more cases/states. Is this the right way to break it up?

Hi West. What is the benefit of using (ontime,offtime) pairs instead of just an array with wait times between changing between on-off?

40ms gives a nice small squirt of water. The solenoids with a transistor for switching works fine at 40ms for the cheap chinese solenoids that I have. It is high pressure water, but I can always play around with this later to see what looks good. Cheers

I gave it a good go, and its "mostly" working. I've been debugging for hours and I can't seem to figure out whats wrong with it. I've started by writing the code just for 2 valves.
The array is written to represent wait times between every change of state for the valve (either turning on, or turning off), each valve gets its own array. Each case/state in the state machine part of the code covers a different solenoid. At the moment it's only 2.

My first problem was that for some reason the valve would keep switching more than the length of the array, and Valve2Count would go much higher than the length of the array. I would think that since I have an array with 8 values inside, it wouldn't be able to switch on and then off again more than 4 times. But it did, so I added a small if() statement at the top of each case to stop this phenomenon.

My problem now is that Valve 2 only executes the first 3 pairs of on-off. It never gets to the 4th one.

Can anybody help me with this? Or at least point me in the next direction to fix this? My code is as follows:

#include <Arduino.h>

// define pins for valves

#define VALVE_1 8
#define VALVE_2 9
#define VALVE_3 10
#define VALVE_4 11
#define VALVE_5 12
#define PILOT_VALVE 7 


// variable to switch between cases
byte myStateVar;
int ValveState = 0;

// variables to toggle between on-off states of various solenoids
// Effect 1
int Valve1State = LOW; //valve is switched off to start
int Valve1Count = 0;     //where are we in the Valve1Sequence array? Starts at zero and goes up as the valve switches on-off
unsigned long Valve1Timer = 0;    //timing

// Effect 2
int Valve2State = LOW;
int Valve2Count = 0;
unsigned long Valve2Timer = 0;


// show sequence timing
// MAKE SURE THERE IS EVEN NUMBER OF ENTRIES OR VALVE WILL STAY ON
unsigned long Valve1Sequence[] = {1000, 80, 2000, 80, 2000, 80, 2000, 80};
unsigned long Valve2Sequence[] = {2000, 80, 2000, 80, 2000, 80, 2000, 80};


// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver(unsigned long startOfPeriod, unsigned long TimePeriod)
{
  unsigned long currentMillis = millis();
  if (currentMillis - startOfPeriod >= TimePeriod)
  {
    // more time than TimePeriod has elapsed since last time if-condition was true
    // startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else
    return false; // actual TimePeriod is NOT yet over
}


// function that switches the valve on or off when a specific timestamp is reached
void switchValve(int ValvePin)
{
  ValveState = digitalRead(ValvePin);
  if (ValveState == LOW) // if its already off, turn it on
  {
    // Serial.print("valve Effect:");
    // Serial.println(ValvePin);
    ValveState = HIGH;
  }
  else // if its already on, turn it off
  {
    ValveState = LOW;
  }
  // set the valve to its new state
  digitalWrite(ValvePin, ValveState);
}

void setup()
{
  // put your setup code here, to run once:

  // turn on delay for initiation
  delay(1000);

  Serial.begin(9600);
  Serial.println("Setup-Start");

  // setting pin modes 
  pinMode(VALVE_1, OUTPUT);
  pinMode(VALVE_2, OUTPUT);
  pinMode(PILOT_VALVE, OUTPUT);


  //turn on pilot valve and wait a couple seconds for pressure to build
  digitalWrite(PILOT_VALVE, HIGH);
  delay(5000);

  //grab current time as main code is about to run
  Valve1Timer = millis();
  Valve2Timer = millis();

}

void main_case_loop()
{

  switch (myStateVar)
  {

  case 0: // for valve number 1
    if (Valve1Count >= 8) // line of code added to stop counter going above the length of the array
    {
      myStateVar++;
    }
    if (TimePeriodIsOver(Valve1Timer, Valve1Sequence[Valve1Count])) // if the next time-stamp is reached, do something
    {
      Serial.print("Valve 1 Count:");
      Serial.println(Valve1Count);

      switchValve(VALVE_1); // calls function to change the state of the valve
      Valve1Count++;       // looks for the next time-stamp in the timing array
      Valve1Timer = millis();  //grabs current time in order to compare for next time-stamp
    }
    myStateVar++; // goes to the next valve
    break;

  case 1: // for valve flame effect number 2
    if (Valve2Count >= 8)
    {
      myStateVar = 0;
    }
    if (TimePeriodIsOver(Valve2Timer, Valve2Sequence[Valve2Count])) // if the next time-stamp is reached, do something
    {
      Serial.print("Valve 2 Count:");
      Serial.println(Valve2Count);

      switchValve(VALVE_2); // calls function to change the state of the valve
      Valve2Count++;       // looks for the next time-stamp in the timing array
      Valve2Timer = millis();
    }
    myStateVar = 0; // goes back to the first valve
    break;

  }
}

void loop()
{

  // put your main code here, to run repeatedly:
  main_case_loop();
}

array-indexes start at zero
so the index-number must count from 0 up 7
your if condition

if (Valve1Count >= 8)

allow to go up to 8

You coded Valve1Count >= 8 which means 8 is included
it must be
either

if (Valve1Count == 8) //  is 8

or

if (Valve1Count > 7) // bigger than 7

bigger than 7 catches all numbers 8,9,10 etc.

== 8 just catches 8

You never reset your variables Valve1Count, Valve2Count
this means with

Valve1Count++;

your increasing the value over all limits

to automatically ensure that you stop at the last element you can code

if (Valve1Count > (sizeof(Valve1Sequence) / sizeof(Valve1Sequence[0] ) )  )// check if last array-element is reached

this allows to just modify the number of array-elements and stopping always at the last element.

here is a code-version with three macros that make it comfortable to add debugoutput

dbg: print everytime

dbgi: print only after "X" milliseconds have

dbgc: print only once on each change of the variable specified

it does compile but I did not test it

// MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START *
// a detailed explanation how these macros work is given in this tutorial
// https://forum.arduino.cc/t/comfortable-serial-debug-output-short-to-write-fixed-text-name-and-content-of-any-variable-code-example/888298

#define dbg(myFixedText, variableName) \
  Serial.print( F(#myFixedText " "  #variableName"=") ); \
  Serial.println(variableName);
// usage: dbg("1:my fixed text",myVariable);
// myVariable can be any variable or expression that is defined in scope

#define dbgi(myFixedText, variableName,timeInterval) \
  do { \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  } while (false);
// usage: dbgi("2:my fixed text",myVariable,1000);
// myVariable can be any variable or expression that is defined in scope
// third parameter is the time in milliseconds that must pass by until the next time a
// Serial.print is executed
// end of macros dbg and dbgi
// print only once when value has changed
#define dbgc(myFixedText, variableName) \
  do { \
    static long lastState; \
    if ( lastState != variableName ){ \
      Serial.print( F(#myFixedText " "  #variableName" changed from ") ); \
      Serial.print(lastState); \
      Serial.print( F(" to ") ); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  } while (false);
// MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END *


#include <Arduino.h>

// define pins for valves

#define VALVE_1 8
#define VALVE_2 9
#define VALVE_3 10
#define VALVE_4 11
#define VALVE_5 12
#define PILOT_VALVE 7 


// variable to switch between cases
byte myStateVar;
int ValveState = 0;

// variables to toggle between on-off states of various solenoids
// Effect 1
int Valve1State = LOW; //valve is switched off to start
int Valve1Count = 0;     //where are we in the Valve1Sequence array? Starts at zero and goes up as the valve switches on-off
unsigned long Valve1Timer = 0;    //timing

// Effect 2
int Valve2State = LOW;
int Valve2Count = 0;
unsigned long Valve2Timer = 0;


// show sequence timing
// MAKE SURE THERE IS EVEN NUMBER OF ENTRIES OR VALVE WILL STAY ON
unsigned long Valve1Sequence[] = {1000, 80, 2000, 80, 2000, 80, 2000, 80};
unsigned long Valve2Sequence[] = {2000, 80, 2000, 80, 2000, 80, 2000, 80};


// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver(unsigned long startOfPeriod, unsigned long TimePeriod)
{
  unsigned long currentMillis = millis();
  if (currentMillis - startOfPeriod >= TimePeriod)
  {
    // more time than TimePeriod has elapsed since last time if-condition was true
    // startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else
    return false; // actual TimePeriod is NOT yet over
}


// function that switches the valve on or off when a specific timestamp is reached
void switchValve(int ValvePin)
{
  ValveState = digitalRead(ValvePin);
  if (ValveState == LOW) // if its already off, turn it on
  {
    // Serial.print("valve Effect:");
    // Serial.println(ValvePin);
    ValveState = HIGH;
  }
  else // if its already on, turn it off
  {
    ValveState = LOW;
  }
  // set the valve to its new state
  digitalWrite(ValvePin, ValveState);
}

void setup()
{
  // put your setup code here, to run once:

  // turn on delay for initiation
  delay(1000);

  Serial.begin(9600);
  Serial.println("Setup-Start");

  // setting pin modes 
  pinMode(VALVE_1, OUTPUT);
  pinMode(VALVE_2, OUTPUT);
  pinMode(PILOT_VALVE, OUTPUT);


  //turn on pilot valve and wait a couple seconds for pressure to build
  digitalWrite(PILOT_VALVE, HIGH);
  delay(5000);

  //grab current time as main code is about to run
  Valve1Timer = millis();
  Valve2Timer = millis();

}

void main_case_loop()
{
  dbgc("0:",myStateVar); /// whenever variable myStateVar changes print 
  switch (myStateVar)
  {

  case 0: // for valve number 1
    //if (Valve1Count >= 8) // line of code added to stop counter going above the length of the array
    if (Valve1Count > (sizeof(Valve1Sequence) / sizeof(Valve1Sequence[0] ) )  )// check if last array-element is reached
    {
      Valve1Count = 0; // reset to first array-element
      myStateVar++;
    }
    if (TimePeriodIsOver(Valve1Timer, Valve1Sequence[Valve1Count])) // if the next time-stamp is reached, do something
    {
      dbg("1",Valve1Count);

      switchValve(VALVE_1); // calls function to change the state of the valve
      Valve1Count++;       // looks for the next time-stamp in the timing array
      // not nescessary this is done automatically by function TimePeriodIsOver()
      //Valve1Timer = millis();  //grabs current time in order to compare for next time-stamp
    }
    //increasing myStateVar here means the valves are timed in parallel
    // is this your intention?
    myStateVar++; // goes to the next valve
    break;

  case 1: // for valve flame effect number 2
    if (Valve2Count >= 8)
    {
      myStateVar = 0;
    }
    if (TimePeriodIsOver(Valve2Timer, Valve2Sequence[Valve2Count])) // if the next time-stamp is reached, do something
    {
      Serial.print("Valve 2 Count:");
      Serial.println(Valve2Count);

      switchValve(VALVE_2); // calls function to change the state of the valve
      Valve2Count++;       // looks for the next time-stamp in the timing array
      Valve2Timer = millis();
    }
    myStateVar = 0; // goes back to the first valve
    break;

  }
}

void loop()
{

  // put your main code here, to run repeatedly:
  main_case_loop();
}

Your valvle-switch-function can be boiled down to

void switchValve(int ValvePin) {
  digitalWrite(ValvePin,!digitalRead(ValvePin) );
}

digitalRead: read the state of the IO-pin,

"!" the attention-makr is the not-operator !LOW = HIGH !HIGH = LOW

"!" invert this state

digitalWrite:: set IO-pin to the inverted state.

best regards Stefan

consider

struct Valve {
    const byte    Pin;
    unsigned long MsecOn;
    unsigned long MsecOff;
    const char   *desc;

    unsigned long msecLst;
    unsigned long msecPeriod;
};

Valve valves [] = {
    { 10, 500, 500, "V1" },
    { 11, 100, 300, "V2" },
    { 12, 250, 100, "V3" },
    { 13, 450, 200, "V4" },
};
#define Nvalve  (sizeof(valves)/sizeof(Valve))

enum { Off = HIGH, On = LOW };

// -----------------------------------------------------------------------------
void
loop (void)
{
    unsigned long msec = millis ();

    Valve *v = valves;
    for (unsigned n = 0; n < Nvalve; n++, v++)  {
        if (msec - v->msecLst > v->msecPeriod)  {
            v->msecLst = msec;

            Serial.println (v->desc);
            if (On == digitalRead (v->Pin))  {
                v->msecPeriod = v->MsecOff;
                digitalWrite(v->Pin, Off);
            }
            else  {
                v->msecPeriod = v->MsecOn;
                digitalWrite(v->Pin, On);
            }
        }
    }
}

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

    for (unsigned n = 0; n < Nvalve; n++)  {
        pinMode (valves [n].Pin, OUTPUT);
    }
}

@mr_nerdy_gadget_man the way you have divided things for you state machine is not optimal.

You have five identical objects, an FSM shoulb be written to control one object that can be used on all five.

So the state is simply if the solenoid is on or off, and how long it is until it should be changed. And where it is in its own sequence of steps. And maybe other things not yet though of.

Any time you have variable names like

#define VALVE_1 8
#define VALVE_2 9
#define VALVE_3 10
#define VALVE_4 11
#define VALVE_5 12

you should thinking of arrays. I used arrays below to store and track all the info for all the solenoids. So we can talk about solenoid #2 (or variable x, an index);

Here's your last program with as much as I can do before heading out to the beach. It doesn't have any logic yet informing the action, but it is set up with arrays and if you read through it you will see where I would be heading if I wasn't heading elsewhere. She's on her way and must not be kept waiting.

You can see it do nothing here: simulation version

The wokwi is free, you can change the sketch. I do most early development this way.

// https://forum.arduino.cc/t/timing-for-many-different-solenoid-valves-fountain-show/1048846/9

#include <Arduino.h>

// define pins for valves

# define NVALVES  5
# define MAXSEQUENCE  10

const unsigned char valvePin[NVALVES] = {
  8, 9, 10, 11, 12, };

unsigned long valveTimer[NVALVES];
unsigned char valveProgramCounter[NVALVES];
unsigned char valveState[NVALVES];


#define PILOT_VALVE 7 

unsigned long now; // always the current value of millis() if we are doing thins right

// show sequence timing
// MAKE SURE THERE ARE AN EVEN NUMBER OF ENTRIES OR VALVE WILL STAY ON
const unsigned char valveLength[NVALVES] = {8, 8, 8, 8, 8, }; // or use n/2?
unsigned long valveSequence[NVALVES][MAXSEQUENCE] = {
  
  {500, 80, 500, 160, 500, 160,  500, 500},
  {700, 80, 700, 80, 700, 80, 15, 780},
};

// easy to use helper-function for non-blocking timing
// well it's a long way to go for one line of code - eliminate after it is working
boolean TimePeriodIsOver(unsigned long startOfPeriod, unsigned long TimePeriod)
{
  unsigned long currentMillis = now;
  return currentMillis - startOfPeriod >= TimePeriod;
}

// function that switches the valve on or off
// that's all! according to its current state
void switchValve(unsigned char theValve)
{
  digitalWrite(valvePin[theValve], valveState[theValve]);
}

void setup()
{
  delay(777);
  Serial.begin(9600);
  Serial.println("solenoid control unit 0\n");

  for (unsigned char ii = 0; ii < NVALVES; ii++) {
    pinMode(valvePin[ii], OUTPUT);
    valveState[ii] = 0;   // OFF
    digitalWrite(valvePin[ii], LOW);
    valveProgramCounter[ii] = 0;
  }

  //digitalWrite(PILOT_VALVE, HIGH);
  Serial.println("delay(5000)? Life too short, here we go!\n");
}

void main_case_loop()
{
  for (unsigned char theValve = 0; theValve < NVALVES; theValve++) {

    switch (valveState[theValve]) {
    case 0 : // OFF. has its timer expired? turn on

    // advance / reset the program counter
    // get the ON time, set the timer
    // go to state = 1 (ON)

    break;

    case 1 : // ON. has its timer expired? turn off 

    // get the OFF time, set the timer
    // go to state = 1 (ON)

    break;

    }
  }
}

void loop()
{
  now = millis();   // everyone set your clocks by this
  main_case_loop();
}

Read it, see if it makes sense this way. Use the wokwi! to share your progress and we can play with it, too. Only less wet.

a7

@gcjr
what is the reason for you to not add any comments?

This code is very compact. The smallest code requires the biggest knowledge in C++-coding.
using

  • structs
  • arrays
  • enum
  • pointers
  • the arrow-operator
  • a lot of experience in combining expressions in logical expressions

Word.

But @mr_nerdy_gadget_man has used millis(), arrays and the C/C++ switch statement.

For this project, nothing more advanced is required. Anything more advanced is a distraction.

Even if 40 line expert sketch is desirable, it still should come after mastery of some language basics, and the basic ideas of using logic in programs.

a7

aside from the unusual, it's never obvious to me what needs to be comments.

what 3 comments would you add? by limiting it to 3, i'm interested in knowing the 3 things least obvious

Comments can be saved if you use sensible names for the constants and variables.

Hello mr_nerdy_gadget_man
Try this simple sketch using a strutured array for the timing.
Check it, test it and have fun.

 /* BLOCK COMMENT
  ATTENTION: This Sketch contains elements of C++.
  https://www.learncpp.com/cpp-tutorial/
  Many thanks to LarryD
  https://europe1.discourse-cdn.com/arduino/original/4X/7/e/0/7e0ee1e51f1df32e30893550c85f0dd33244fb0e.jpeg
  https://forum.arduino.cc/t/timing-for-many-different-solenoid-valves-fountain-show/1048846
  Tested with Arduino: Mega[ ] - UNO [X] - Nano [ ]
*/
#define ProjectName "Timing for many different solenoid valves - fountain show "
// HARDWARE AND TIMER SETTINGS
// YOU MAY NEED TO CHANGE THESE CONSTANTS TO YOUR HARDWARE AND NEEDS
constexpr byte ValvePins[] {2, 3, 4, 5, 6}; // portPin o---|220|---|LED|---GND
constexpr unsigned long Blink512Hz {9};     // blinkrate for heartbeat function
// VARIABLE DECLARATION AND DEFINITION
unsigned long currentTime;
// -- objects -----------------------------------------
struct TIMER {              // has the following members
  unsigned long duration;   // memory for interval time
  unsigned long stamp;      // memory for actual time
  int onOff;               // control for stop/start/repeat
};
struct TIMING {
  TIMER flash;
  int pattern[5];
};
TIMING timings[]
{
  {40, 0, HIGH, {0, 0, 0, 0, 0}},
  {40, 0, LOW, {1, 0, 0, 0, 1}},
  {40, 0, LOW, {1, 1, 0, 1, 1}},
  {40, 0, LOW, {0, 1, 1, 1, 0}},
  {40, 0, LOW, {0, 0, 1, 0, 0}},
  {40, 0, LOW, {0, 1, 1, 1, 0}},
  {40, 0, LOW, {1, 1, 0, 1, 1}},
  {40, 0, LOW, {1, 0, 0, 0, 1}},
  {40, 0, LOW, {0, 0, 0, 0, 0}},
  {40, 0, LOW, {1, 0, 0, 0, 0}},
  {40, 0, LOW, {0, 1, 0, 0, 0}},
  {40, 0, LOW, {0, 0, 1, 0, 0}},
  {40, 0, LOW, {0, 0, 0, 1, 0}},
  {40, 0, LOW, {0, 0, 0, 0, 1}},
  {40, 0, LOW, {0, 0, 0, 1, 0}},
  {40, 0, LOW, {0, 0, 1, 0, 0}},
  {40, 0, LOW, {0, 1, 0, 0, 0}},
  {40, 0, LOW, {1, 0, 0, 0, 0}},

};
// -- services  -----------------------------------------
void heartBeat(int rate)
{
  bool myBlink {currentTime & bit(rate)};
  digitalWrite(LED_BUILTIN, myBlink);
}
// -------------------------------------------------------------------
void setup() {
  Serial.begin(9600);
  Serial.println(F("."));
  Serial.print(F("File   : ")), Serial.println(__FILE__);
  Serial.print(F("Date   : ")), Serial.println(__DATE__);
  Serial.print(F("Project: ")), Serial.println(ProjectName);
  pinMode (LED_BUILTIN, OUTPUT);  // used as heartbeat indicator
  //  https://www.learncpp.com/cpp-tutorial/for-each-loops/
  for (auto ValvePin : ValvePins) pinMode(ValvePin, OUTPUT);
}
void loop () {
  currentTime = millis();
  heartBeat(Blink512Hz);
  for (auto &timing : timings)
  {
    if (currentTime - timing.flash.stamp >= timing.flash.duration && timing.flash.onOff)
    {
      static int arrayControl;
      timing.flash.onOff = false;
      if (++arrayControl == (sizeof(timings) / sizeof(timings[0]))) arrayControl = 0;
      timings[arrayControl].flash.onOff = true;
      timings[arrayControl].flash.stamp = currentTime;
      int elements = 0;
      for (auto ValvePin : ValvePins) digitalWrite(ValvePin, timings[arrayControl].pattern[elements++]);
    }
  }
}

Wow, thank you! It makes more sense the way you've done it. However, I have a question about doing it like this. As I mentioned, I may want to expand on this project to include lights or who knows what else yet. However, you have put the case-switch statements inside of a for() loop. Say (as an example) I want to include a distance sensor that turns lights on and then changes the color of light as a person approaches. Could I simply add another switch statement after the for() loop, inside the main_case_loop? Is it possible to have two state machines operating like this or would this slow down my code a detrimental amount? I'm brand new to the state machine concept so I'm trying to understand its limitations.

Thanks so much for your help!

@alto777 I have finished your code on wokwi and it works! I'm going to expand and change it a bit. I want to have the fountain dormant and waiting, with a hidden sensor to see when someone is approaching. When movement is detected (and someone is nearby), the show will start. I'm thinking I could just put this in the loop function, and when activated it calls upon main_case_loop, and then goes back to being dormant.

Thanks everybody (especially @StefanL38 for getting me started) for all of your help, I will continue to update as I progress.

Hi,

That would work, then when you want the sequence to stop, you have a function to turn ALL the valves off.

Like pseudo code..

Is trip device ON?
If Yes... Display function.
else
Shutdown valves function.

Tom.... :smiley: :+1: :coffee: :australia: