Skip to content

State Machine Usage

Franz Miltz edited this page Aug 7, 2021 · 6 revisions

How to work with the State Machine code

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, asumme 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(Logger &log, Navigation &nav_data);

to

// src/state_machine/transitions.hpp

bool checkPodStopped(Logger &log, Navigation &nav_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(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(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(Logger &log, Navigation &nav_data, 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(Logger &log, Navigation &nav_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(Logger &log, Navigation &nav_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)
{
  Navigation nav_data;

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

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

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

I would change this as follows:

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

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

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

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

    nav_data.acceleration               = static_cast<nav_t>(rand());
    nav_data.displacement               = static_cast<nav_t>(rand());
    nav_data.braking_distance           = static_cast<nav_t>(rand());
    nav_data.emergency_braking_distance = static_cast<nav_t>(rand());
    enableOutput();
    ASSERT_EQ(false, checkPodStopped(log, nav_data)) << pod_stopped_false_positive_error;
    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 ommitted from this example.

Clone this wiki locally