Skip to content

State Machine Usage

Franz Miltz edited this page Sep 13, 2021 · 6 revisions

Since STM does not have any hardware interfaces, it can be run without any prior steps. Therefore, we don't need to document how to use the code in that regard. However, code can be used in other ways. In particular, the behaviour needs to be modified and features need to be added to adjust to changing requirements.

Changing transition conditions

If you want to change a transition condition, you should follow these rough steps:

  1. Find sources for the required information
  2. Update data function signatures
  3. Change the implementation logic
  4. Update the documentation
  5. Update tests

Example

Let's pretend we are not happy with the current way of checking whether the pod has stopped. Instead of completely relying on navigation and their velocity values, we want to add a user command that allows us to manually confirm that the pod has indeed stopped. Further, assume that Telemetry have already done their part by implementing the GUI and adding a stopped_command field to the data::Telemetry section of the CDS. It is now up to us to incorporate this value into the transition logic.

Step 1 is easy because we have already mentioned that the value we require lives in data::Telemetry.

Step 2 is to add it to the function signatures in transitions.hpp and transitions.cpp. So we change

// src/state_machine/transitions.hpp

bool checkPodStopped(utils::Logger &log, const data::Navigation &nav_data);

to

// src/state_machine/transitions.hpp

bool checkPodStopped(Logger &log, const data::Navigation &nav_data, const data::Telemetry &telemetry_data);

The same has to be done in transitions.cpp. Further, we need to make sure that all the checkTransition implementations in state.cpp call the function with the right set of arguments. For example, we change

// src/state_machine/state.cpp

State *NominalBraking::checkTransition(utils::Logger &log)
{
  updateModuleData();

  // ... other conditions ...

  bool stopped = checkPodStopped(log, nav_data_);
  if (stopped) { return Finished::getInstance(); }
  return nullptr;
}

to

// src/state_machine/state.cpp

State *NominalBraking::checkTransition(utils::Logger &log)
{
  updateModuleData();

  // ... other conditions ...

  bool stopped = checkPodStopped(log, nav_data_, telemetry_data_);
  if (stopped) { return Finished::getInstance(); }
  return nullptr;
}

This needs to be done in all places where checkPodStopped is being referenced.

In Step 3 we need to modify the actual logic. Currently, we have this:

bool checkPodStopped(utils::Logger &log, const data::Navigation &nav_data, const Telemetry &telemetry_data)
{
  if (nav_data.velocity > 0) return false;

  // ... logging code ...

  return true;
}

To incorporate the command, we can change this to

// src/state_machine/transitions.cpp

bool checkPodStopped(utils::Logger &log, const data::Navigation &nav_data, const data::Telemetry &telemetry_data)
{
  if (nav_data.velocity <= 0) {
      // ... logging code ...
      return true;
  } else if (telemetry_data.stopped_command) {
      // ... logging code ...
      return true;
  }

  return false;
}

This behaves as intended and also allows us to log which of the conditions was met.

In Step 4 we need to change the comment in transitions.hpp to match the behaviour. We could end up with something like

// src/state_machine/transitions.hpp

/*
 * @brief   Returns true iff the pod is close enough to the end of the track or confirmation has been received that the pod has stopped.
 */
bool checkPodStopped(utils::Logger &log, const data::Navigation &nav_data, const data::Telemetry &telemetry_data);

You have now successfully changed the transition conditions for checkPodStopped. However, if it's not tested, it doesn't work. This leads us to the final step. If you try and run make test now, the tests will not compile and if they do, they shouldn't pass.

Firstly, we need to change all the references to checkPodStopped in test/src to use the new signature as we have already done in step 2. In our case, we then need to make sure that the right preconditions are guaranteed so that the existing tests still work. For example, consider this test:

// test/src/state_machine/transitions.test.cpp

TEST_F(TransitionFunctionality, handlesPositiveVelocity)
{
  data::Navigation nav_data;

  constexpr int max_velocity = 100;  // 100 m/s is pretty fast...
  constexpr nav_t step_size  = static_cast<data::nav_t>(max_velocity) / static_cast<data::nav_t>(TEST_SIZE);

  for (int i = 1; i <= TEST_SIZE; i++) {
    nav_data.velocity = step_size * static_cast<data::nav_t>(i);

    nav_data.acceleration               = static_cast<data::nav_t>(rand());
    nav_data.displacement               = static_cast<data::nav_t>(rand());
    nav_data.braking_distance           = static_cast<data::nav_t>(rand());
    nav_data.emergency_braking_distance = static_cast<data::nav_t>(rand());
    enableOutput();
    ASSERT_EQ(false, checkPodStopped(log, nav_data));
    disableOutput();
  }
}

I would change this as follows:

// test/src/state_machine/transitions.test.cpp

TEST_F(TransitionFunctionality, handlesPositiveVelocity)
{
  Navigation nav_data;
  data::Telemetry telemetry_data;
  telemetry_data.stopped_command      = false;

  constexpr int max_velocity = 100;  // 100 m/s is pretty fast...
  constexpr data::nav_t step_size  = static_cast<data::nav_t>(max_velocity) / static_cast<data::nav_t>(TEST_SIZE);

  for (int i = 1; i <= TEST_SIZE; i++) {
    nav_data.velocity = step_size * static_cast<data::nav_t>(i);

    nav_data.acceleration               = static_cast<data::nav_t>(rand());
    nav_data.displacement               = static_cast<data::nav_t>(rand());
    nav_data.braking_distance           = static_cast<data::nav_t>(rand());
    nav_data.emergency_braking_distance = static_cast<data::nav_t>(rand());
    enableOutput();
    ASSERT_EQ(false, checkPodStopped(log, nav_data));
    disableOutput();
  }
}

Of course, in most cases, you will have to add and/or remove tests. However, since the way to do this will be different in each case, that part has been omitted from this example.

Adding a state

Of course, often the design changes will require more than simply adapting the transition conditions. Here's what you need to do in that case:

  1. Implement all the transition checks in src/state_machine/transitions.* as discussed above.
  2. Adapt the data::State enum. Note that adding an internal state may not require a new enum value.
  3. Go to state.hpp and use the MAKE_STATE macro to generate all the boilerplate code.
  4. Set the instance_, enum_value_ and string_representation_ fields in state.cpp.
  5. Implement the checkTransition method for your new state.
  6. Adapt existing checkTransition methods of preceding states (i.e. old states that transition into the new one).
  7. Update the documentation
  8. Update tests (this may require a lot of work if you've added something to the enum)
Clone this wiki locally