User Tools

Site Tools


eeros_architecture:sequencer:define_sequence

Define your own Sequence or Step

A sequence or step must be carefully designed in order to run as expected.

Write your own Class

Extend the class Sequence or Step, respectively. A step comprises a single operation and never runs concurrently to its caller. A sequence typically includes two or more steps and can run in parallel to its caller.

class MyStep : public Step {
public:
  MyStep(std::string name, Sequencer& seq, BaseSequence* caller) : Step(name, seq, caller) {...}
  ...
};
 
class MySequence : public Sequence {
public:
  MySequence(std::string name, Sequencer& seq, BaseSequence* caller) : Sequence(name, seq, caller, false) {...}
  ...
};

Your constructor usually includes a name for the sequence or step, a reference to the sequencer, and a reference to the caller of this object. The latter point is very important, because every step or sequence must know its caller or owner.
You then initialize the sequence in the initializer list by writing Sequence(name, seq, caller, false). The last parameter defines whether your sequence is blocking or nonblocking. Setting to blocking (false makes this sequence block its calling sequence, setting to nonblocking (true) starts a new thread which runs the sequence in parallel to its calling sequence. A step is always blocking.

In the body of your constructor you define the attributes of your sequence. This includes:

  • monitors with conditions: you can add several monitors which check for various conditions.
  • monitor property: every monitor can have its own exception behavior. This behavior determines how the sequence behaves after a monitor fires.
  • exception sequence: A special exception sequence can run after a monitor fires.

The following example shows how these attributes can be set.

class MySequence : public Sequence {
public:
  MySequence(std::string name, Sequencer& seq, BaseSequence* caller) : Sequence(name, seq, caller, false) {
    // this sequence will run in its own thread and concurrently to its caller
    setTimeoutTime(2.5);                          // the built-in timeout monitor has its timeout condition set to 2.5s
    setTimeoutExceptionSequence(excSeq);          // when the monitor fires, this exception sequence will run
    setTimeoutBehavior(SequenceProp::abort);      // after exception handling the remaining steps of this sequence will be aborted
  }
};

If you define a main sequence which has no calling sequence and which must be nonblocking per default, you can make use of a simpler constructor by calling

class MainSequence : public Sequence {
public:
  MainSequence(std::string name, Sequencer& seq) : Sequence(name, seq) {...}
  ...
};

Define the Action

Implement the function action(). This comprises the work which should be done. In case of a sequence this might be a sequence of steps. In case of a step it might be setting a new set position. Please make sure that this function does not block! Never wait for some condition to be reached nor wait for some time to elapse (no sleeping). Waiting for conditions must be done with the help of Conditions and Monitors.

public:
  int action() {
    step1();
    step2();
    step3();
  }

Its mandatory to implement this function. If not, no work is done and the step or sequence terminates immediately.
There might be sequences which should never stop. This must be done as shown below:

public:
  int action() {
    while (Sequencer::running) step1();
    // while (true) step1();  // WRONG!
  }

This guarantees that the sequence could be stopped by the main program due to some external event such as a signal.

Define Preconditions

You may want to start a sequence or step only if a certain precondition is met. Override the function checkPreCondition(), e.g.:

  bool checkPreCondition() {
    return robot.buttonPressed();
  }

If the check returns false the sequence or step will be omitted and control will return to its caller.

Add Parameters

It is possible to pass parameters into sequences or steps. This can be very convenient and allows a sequence handle a given task in a flexible way. Consider the following examples:

  move(1.0, 2.0);  // calls a step which sets the position to x=1.0/y=2.0 meters 
  move(2.0, 2.5);  // calls the same step which now sets the position to x=2.0/y=2.5 meters

Parameter passing is possible by implementing the ()-operator with a given set of parameters. For the above example this will be done with:

  int operator() (double x, double y) {
    this.x = x; // store the first parameter into a local variable for further use
    this.y = y; // store the second parameter into a local variable for further use 
    return start();  // this will start the step or sequence
  }

Do not forget to call start() at the end. If you don't need parameters you simply use the default ()-operator which already includes the call to start().
It is possible to implement more than one ()-operator if you wish to pass different sets of parameters.

Define Exit Conditions

A sequence or step only ends after its exit condition is met. Typically you wait for a position be be reached or a time to be met. Override the function checkPreExitCondition(), e.g.:

  bool checkExitCondition() {
    return robot.x >= 13.8 && robot.y >= 2.4;
  }

If the check returns false the sequence or step will remain in running state and the check is periodically repeated until it returns true.
Here again, please make sure not to write something like

  bool checkExitCondition() {
    while (robot.x < 13.8 || robot.y < 2.4); // wrong, do not wait here until condition is met!
    return true;
  }

Never wait while checking for a condition. The control must be returned immediately to the sequencer. The sequencer itself will make sure that it continues only after the exit condition is met.

Waiting in Sequences or Steps

As mentioned before you should never wait by using sleep in a action method. However, quite often it is desirable to wait for some time to pass when running sequences. How to do properly? Study the following example:

class StepA : public Step {
public:
  StepA(std::string name, Sequencer& seq, BaseSequence* caller) : Step(name, seq, caller) { }
  int action() {time = std::chrono::steady_clock::now();}
  bool checkExitCondition() {
    return ((std::chrono::duration<double>)(std::chrono::steady_clock::now() - time)).count() > 3.2;
  }
private:
  std::chrono::time_point<std::chrono::steady_clock> time;
};

This step simply takes a time stamp when running its action method. Its exit condition becomes true as soon as a waiting time of 3.2s has elapsed. However, contrary to a simple sleep it does not block the sequencer and the checking of monitors of this step or sequence continues unhindered.

eeros_architecture/sequencer/define_sequence.txt · Last modified: 2018/08/07 11:14 by graf