State machine design?

this is what your state machine looks like today:

https://dreampuf.github.io/GraphvizOnline/#digraph%20G%20{%20 %20%20graph%20[ %20%20%20%20label%3D"Finite%20State%20Machine" %20%20%20%20labelloc%3D"t" %20%20%20%20fontname%3D"sans-serif" %20%20] %20%20node%20[ %20%20%20%20style%3Dfilled %20%20%20%20fillcolor%3Dgray95 %20%20%20%20fontname%3D"sans-serif" %20%20] %20%20 %20%20MIX%20->%20IDLE%20[label%3D"mixingIndex%20<%207"%20fontsize%3D10]%3B%20%20 %20%20MIX%20->%20DOSE_START%20[label%3D"mixingIndex%20>%3D%207"%20fontsize%3D10]%3B%20%20 %20%20DOSE_START%20->%20DOSE_END%20[label%3D"after%20currentDelay"%20fontsize%3D10]%3B%20%20 %20%20DOSE_END%20->%20REST%20[label%3D""%20fontsize%3D10]%3B%20%20 %20%20REST%20->%20MIX%20[label%3D"after%20currentRest"%20fontsize%3D10]%3B%20%20 %20%20DELAY%3B%20%20 %20%20IDLE%3B%20%20 }

IDLE and DELAY are dead ends.
I don't think this is what you really need.

As mentioned above ... start drawing a diagram.

Outside of the state machine, you have to model the parameters of your process somehow in profiles. A struct may well suit.
It looks like you are controlling a number of pumps, and these maybe operate in a cycle and are on for a specified time in a particular order depending on what "Growth Rate" is set (I guess this is a hydroponics system or similar). Get that right before designing the state machine model to implement it.

I do want the machine to have an "idle" state. It is idle 90%+ of the time. An RTC checking loop would kick off different 'formulas', or user interaction via buttons also can.

perfectly fine.
But you will need a condition to leave the idle state (e.g. at a specific time) and conditions in other states to fall back into IDLE (e.g. at the end of Rest?)

sure the idle-state checks for has the time to "take action" come.

as pseudo-code

case IDLING:
  if (time_for_action_has_come) {
   STATE = .......
  }
  break; // VERY important to have the break

Exactly what a state transition diagram is about.

That has nothing to to with state machines. It's a detail of C coding. If you look at state machines coded in other languages, there won't be any break statements.

Yes. At the end of a mixing cycle (processing a formula) the state could become the idle state, but more likely it was kicked off by checks from the water level sensors and the next state would be the routine to fill them, which doesn't exist yet.

This project is by far the largest and most complex thing I have ever attempted to create. It is basically an entire automated factory with pumps, valves and 15 relays and counting and I really appreciate the help.

Yes and you should do it in steps.

First step: getting a single pump to work through the steps of
switch on pump
check if pump has been switched on for the right amount of time
....

THIS is really cool. I wish I had time to learn to create something like what you made here. Maybe I can play with this example someday.

The link is the source - so it is up to you to make modifications.
Not someday.
Now - before you continue to write lines of code.

I have a different file that does almost everything I want the machine to do and it works. It is also a state machine but a weirdly coded one. I have not included that code because it is 500 lines of mess and I am sparing you lol. I am trying to rewrite and learn more and do more better.

Not sure about that one. If the machine spends zero time in the state, should it really be a state?

I would consider modelling that as two states. One state is waiting for the value to rise above the threshold. When that happens, go to the second state. In the second state, if the value falls below the threshold, return to the first state. But if the time period is reached while in that second state, move on to some third state.

Perhaps if it is a common entry point? But this is well taken, a clear statement about how to make choices.

It may well be that a clean table which describes all the options and processes is a better starting point than a formal state diagram.
If the process exist in code somehow, then look at reverse engineering that out of the code. After that, look at the engine to implement it.

This feels like what I have here. Setting the delay via the custom Delay function and setting NEXT_STATE when using Delay. Is there a better arrangement? I think this is going to work for me.

don't use a "delay()" but a currentMillis-previousMillis > interval construct

To be clear I claim that code mess lol, I developed a working prototype as I built the machine and also at the same time developed the capabilities.
Basically, I was tired of hauling and mixing nutrient water and I could have just run an R/O line to the grow room and bought a new teaspoon set but I never know when to say when so now I have this machine.
I am quite happy with the results so far, I fell in a few pits along the way but was able to resolve issues like flyback and wiring.
So now I am faced with taking my 800+ (did I say 500?) lines of working code and adding the RTC time checking routine to it someday (for now I need to do it all via buttons and LCD to learn what I want that to be) and also adding more pumps like the emptying pumps to it and handling for all that...
...so it was time for a rewrite and to again make a strong effort at improving structure and clarity and maintainability.

For example, the struct I pasted replaced this unmaintainable garbage in the prototype;

void updateDispensingCriteria(CA mt) { // 600 = 10, 300 = 5 , 180 = 3, etc
  if (mt == CA{"SEEDLING"}) {
    DISPENSE_COUNT = 8; // number of pumps to dispense
    dispensingOrder[0] = CA{"PUMP_NUT_1"}; dispTimes[0] = 6;  dispDelays[0] = 300; dispRests[0] = 600; // ARMOR SI
    dispensingOrder[1] = CA{"PUMP_NUT_2"}; dispTimes[1] = 3;  dispDelays[1] = 180; dispRests[1] = 300; // SUPERTHRIVE
    dispensingOrder[2] = CA{"PUMP_NUT_3"}; dispTimes[2] = 6;  dispDelays[2] = 180; dispRests[2] = 300; // MICRO
    dispensingOrder[3] = CA{"PUMP_NUT_4"}; dispTimes[3] = 4;  dispDelays[3] = 180; dispRests[3] = 300; // BLOOM
    dispensingOrder[4] = CA{"PUMP_NUT_5"}; dispTimes[4] = 6;  dispDelays[4] = 180; dispRests[4] = 300; // GROW
    dispensingOrder[5] = CA{"PUMP_NUT_6"}; dispTimes[5] = 15; dispDelays[5] = 180; dispRests[5] = 300; // NECTAR
    dispensingOrder[6] = CA{"PUMP_NUT_7"}; dispTimes[6] = 2;  dispDelays[6] = 180; dispRests[6] = 300; // WETTING
    dispensingOrder[7] = CA{"PUMP_NUT_8"}; dispTimes[7] = 3;  dispDelays[7] = 300; dispRests[7] = 300; // PEROXIDE SEE NOTES

a state machine typically has a number of states and stimuli. various stimuli in each state can cause transitions to different states from a state.

a sequencer has only a single transition from each state to the next state. the transition occurs when some event occurs: button press, timer expires.

it seems that you really have a sequencer. there typically isn't a generic DELAY state unless NEXT_STATE is a variable. so if some processing needs to occur for some specified amount of time a state would transition to a state where a timestamp is captured and the next state waits for the timer to expire and then transitions to some other state

i don't see what causes your code to exit the IDLE state or is that some End state?

also .... only Capitalize Constants, not variables

Simply put, the Delay() and NEXT_STATE are intended for use between 2 other states, i.e. within a transition from IDLE to STATE to DELAY to STATE ... ... ... to IDLE.

The entry state would be changed by an RTC timer routine or a user button press being scanned.

In the prototype, you select a (here Formula) growing cycle with one button and there is also a start/stop cycle button.

verbal description of this pseudo-code