Blackboard Remapping (C++)

This tutorial demonstrates how to use blackboard remapping in YASMIN to share data between states using different variable names in C++. Remapping is a powerful feature that allows you to reuse states with different data without modifying the state code. This is particularly valuable when you have generic, reusable states (like data processors or validators) that you want to apply to different blackboard variables. Remapping promotes code reuse and makes state machines more modular and maintainable.

1. Create the FooState

Define the FooState that reads from foo_data and writes to foo_out_data. This state is designed to be generic - it doesn't know or care what actual data it's processing. It simply reads from a blackboard key named foo_data, logs it, and writes it to foo_out_data. The beauty of this design is that we can use the same state class multiple times with different actual data by using remapping, avoiding code duplication and making the state more flexible and testable.

class FooState : public yasmin::State {
public:
  FooState() : yasmin::State({yasmin_ros::basic_outcomes::SUCCEED}) {};

  std::string
  execute(std::shared_ptr<yasmin::blackboard::Blackboard> blackboard) override {
    std::string data = blackboard->get<std::string>("foo_data");
    YASMIN_LOG_INFO("%s", data.c_str());
    blackboard->set<std::string>("foo_out_data", data);
    return yasmin_ros::basic_outcomes::SUCCEED;
  };
};

2. Create the BarState

The BarState is similar to FooState but reads from bar_data instead. This state demonstrates how different states can work with their own blackboard keys, which will later be remapped to share data between them without modifying the state classes themselves.

class BarState : public yasmin::State {
public:
  BarState() : yasmin::State({yasmin_ros::basic_outcomes::SUCCEED}) {}

  std::string
  execute(std::shared_ptr<yasmin::blackboard::Blackboard> blackboard) override {
    std::string data = blackboard->get<std::string>("bar_data");
    YASMIN_LOG_INFO("%s", data.c_str());
    return yasmin_ros::basic_outcomes::SUCCEED;
  }
};

3. Setup with Remapping

In the main function, we create a blackboard with two messages: msg1="test1" and msg2="test2". Then we add three states to the state machine, each using remapping (the fourth parameter in add_state). In STATE1, we remap foo_data to msg1, so when FooState reads foo_data, it actually gets msg1 from the blackboard. In STATE2, we reuse the same FooState class but remap foo_data to msg2, so it processes different data. In STATE3, we use BarState and remap bar_data to foo_out_data (the output from the previous FooState), creating a data pipeline. This demonstrates how remapping enables: state reuse, data flow between states with incompatible key names, and clear separation between state logic and data sources.

yasmin_ros::set_ros_loggers();

auto blackboard = std::make_shared<yasmin::blackboard::Blackboard>();
blackboard->set<std::string>("msg1", "test1");
blackboard->set<std::string>("msg2", "test2");

auto sm = std::make_shared<yasmin::StateMachine>(
    std::initializer_list<std::string>{yasmin_ros::basic_outcomes::SUCCEED});

  // Cancel state machine on ROS 2 shutdown
  rclcpp::on_shutdown([sm]() {
    if (sm->is_running()) {
      sm->cancel_state();
    }
  });

// STATE1: remap foo_data to msg1
sm->add_state("STATE1", std::make_shared<FooState>(),
              {
                  {yasmin_ros::basic_outcomes::SUCCEED, "STATE2"},
              },
              {
                  {"foo_data", "msg1"},
              });

// STATE2: remap foo_data to msg2
sm->add_state("STATE2", std::make_shared<FooState>(),
              {
                  {yasmin_ros::basic_outcomes::SUCCEED, "STATE3"},
              },
              {
                  {"foo_data", "msg2"},
              });

// STATE3: remap bar_data to foo_out_data
sm->add_state("STATE3", std::make_shared<BarState>(),
              {
                  {yasmin_ros::basic_outcomes::SUCCEED,
                   yasmin_ros::basic_outcomes::SUCCEED},
              },
              {
                  {"bar_data", "foo_out_data"},
              });

4. Execute

Execute the state machine and observe how the same state classes process different data through remapping. The viewer tool helps visualize the state transitions and data flow in real-time.

yasmin_viewer::YasminViewerPub yasmin_pub(sm, "YASMIN_REMAPPING_DEMO");

try {
    std::string outcome = (*sm.get())(blackboard);
    YASMIN_LOG_INFO(outcome.c_str());
} catch (const std::exception &e) {
    YASMIN_LOG_WARN(e.what());
}

rclcpp::shutdown();

5. Run the Demo

Compile and execute the remapping demonstration to see how states can be reused with different data sources.

ros2 run yasmin_demos remap_demo