@wiebe, I haven't done an OO sequencer before but have wanted to do one. Most of my OO stuff has been HAL type stuff. Do you implement a generic "step" class with some associated functions, then reuse it across your programs? My biggest hurdle in this type of thing has always been maintaining state and test data across steps.
Say you want to do thermal testing of PCB's, do you do a "PCB Thermal Test Step" abstract class that contains the list of steps in your sequence? Or do you have a "Step" class that you use for "PCB Thermal Test", which has children of "Load PCB into test rig" then "Heat to X degrees" or whatever?
Or does each step fully contain its own data- like you initialize the list of steps at the beginning, loading things like temperature targets, COM ports, etc, and throw em into an array to cycle through?
I'd be super interested in hearing more about your architecture, as I still have a lot to learn about OO. I'm sure this would help the OP as well.
Well, all of those options are possible.
I made my sequencers so they acts almost like a GUI. The sequence steps trigger other parts of the program (DAQ loop) to do things.
Note that OP might actually like a OO State Pattern as well, or even better. The Sequencer is more suitable for 'programmable' sequences, the state pattern is fixed. I'll look up my CLD solution I once did with a state pattern.
The sequencers I made have a parent step. It has children to 'do stuff'. Usually, I have a Databus (a class, by value, but FG or DVR would also work). So one step would set a value.
The trick I used is to implement a formula parser. So when a value is set in the "Set Step", the value can actually be "channel_a + sin (channel_b)". That makes it easy to reuse the set, without having to make specific steps.
I also make a composite child. That child contains an array of "Step" objects. The children of the composite can either execute the steps in parallel or sequential. Another Step (or Composite) child would be a Conditional child to enable branching. Now since I have the formula parsing, the branching simply evaluates the formula ("channel_a > 5). If the result is True, it executes branch T, else branch F.
The execute method of Step should return immediately. It returns a "Done?" Boolean, and if it's not done, it will simply be executed next iteration. If it is done, the next step will be executed (by it's Composite owner).
It's good to realize that there are two hierarchies. The class hierarchy, and the object hierarchy. The class hierarchy specifies the individual object\class behavior, the object hierarchy specifies the specific sequence, the execution behavior of the objects.
The trick for State Patterns and Sequencers is to not put state in them! For a State Pattern, pass all the data as an input\output. That way, state transitions are easier. Each state\step can have it's own data, for instance a state\step timer. But this data only lives in that state\step. The sequence time for instance should not live in the states\steps. This timer can be used for instance for a "Ramp" step. The Sequence Engine can pause the timer, and all steps using the timer will automatically stop as well.