Blackboard Remapping (Python)

This tutorial demonstrates how to use blackboard remapping in YASMIN to share data between states using different variable names. Remapping allows you to connect state outputs to different blackboard keys, enabling flexible data flow in your state machine. This is a powerful feature for creating reusable, modular states that can work with different data sources without code modification - essential for building maintainable and scalable robot behaviors.

Background

The Blackboard is a shared data structure that allows states to read and write data. Sometimes you want to:

YASMIN provides remappings to achieve this without changing the state's code, making your state machines more modular and maintainable.

1. Create the Foo State

This state reads from foo_data and writes to foo_out_data in the blackboard. Notice how the state is designed generically - it doesn't know or care what the actual data represents. It simply performs a transformation: read input, process it (in this case just logging), and write output. This generic design is what makes remapping powerful: we can reuse this exact same state class multiple times with different actual data by remapping the keys. The state accesses the blackboard using Python's dictionary syntax (blackboard["key"]), making it intuitive and readable.

class Foo(State):
    """
    Represents the Foo state in the state machine.
    """

    def __init__(self):
        """
        Initializes the FooState instance, setting up the outcomes.

        Outcomes:
            SUCCEED: Indicates the state should continue to the next state.
        """
        super().__init__(outcomes=[SUCCEED])

    def execute(self, blackboard: Blackboard):
        """
        Executes the logic for the Foo state.

        Args:
            blackboard (Blackboard): The shared data structure for states.

        Returns:
            str: The outcome of the execution, which can be SUCCEED.

        Raises:
            Exception: May raise exceptions related to state execution.
        """
        data = blackboard["foo_data"]
        yasmin.YASMIN_LOG_INFO(f"{data}")
        blackboard["foo_out_data"] = data
        return SUCCEED

2. Create the Bar State

This state reads from bar_data and logs it. Like the Foo state, this is designed generically to work with remapping. It doesn't care where bar_data comes from or what it contains - it simply reads and processes it. This demonstrates a consumer state that can be connected to any producer state's output through remapping. The separation between the state's internal key names (bar_data) and the actual blackboard keys (which we'll set via remapping) is what enables flexible composition of state machines from reusable components.

class BarState(State):
    """
    Represents the Bar state in the state machine.

    """

    def __init__(self):
        """
        Initializes the BarState instance, setting up the outcomes.

        Outcomes:
            SUCCEDED: Indicates the state should continue to the next state.
        """
        super().__init__(outcomes=[SUCCEED])

    def execute(self, blackboard: Blackboard):
        """
        Executes the logic for the Bar state.

        Args:
            blackboard (Blackboard): The shared data structure for states.

        Returns:
            str: The outcome of the execution, which can be SUCCEED.

        Raises:
            Exception: May raise exceptions related to state execution.
        """
        data = blackboard["bar_data"]
        yasmin.YASMIN_LOG_INFO(f"{data}")
        return SUCCEED

3. Setup the State Machine with Remapping

Initialize the blackboard with different message keys and use remapping to connect them. We create a blackboard with two messages: msg1="test1" and msg2="test2". Then we add three states using the remappings parameter in add_state(). In STATE1, we use the same Foo class but remap foo_data to msg1, so when the state reads foo_data, it actually gets msg1. In STATE2, we reuse Foo again but remap to msg2, processing different data with the same code. In STATE3, we use BarState and remap bar_data to foo_out_data (the output from STATE2), creating a data pipeline where the output of one state becomes the input of the next. This demonstrates the power of remapping: write states once, compose them flexibly. Try running this and you'll see "test1", "test2", and "test2" logged, showing how data flows through the remapped connections.

rclpy.init()
set_ros_loggers()

bb = Blackboard()
bb["msg1"] = "test1"
bb["msg2"] = "test2"

sm = StateMachine(outcomes=[SUCCEED])

# STATE1: remap foo_data to msg1
sm.add_state(
    "STATE1",
    Foo(),
    transitions={SUCCEED: "STATE2"},
    remappings={"foo_data": "msg1"},
)

# STATE2: remap foo_data to msg2
sm.add_state(
    "STATE2",
    Foo(),
    transitions={SUCCEED: "STATE3"},
    remappings={"foo_data": "msg2"},
)

# STATE3: remap bar_data to foo_out_data (output from STATE2)
sm.add_state(
    "STATE3",
    BarState(),
    transitions={SUCCEED: SUCCEED},
    remappings={"bar_data": "foo_out_data"},
)

4. Execute the State Machine

Execute the state machine with the initialized blackboard. Observe how remapping allows the same state class to work with different data without code modification.

    # Launch YASMIN Viewer publisher for state visualization
    viewer = YasminViewerPub(sm, "YASMIN_REMAPPING_DEMO")

    # Execute the FSM
    try:
        outcome = sm(bb)
        yasmin.YASMIN_LOG_INFO(outcome)
    except KeyboardInterrupt:
        if sm.is_running():
            sm.cancel_state()
    finally:
        viewer.cleanup()
        del sm

        # Shutdown ROS 2 if it's running
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == "__main__":
    main()

5. Run the Demo

Run the remapping demonstration to see how blackboard key remapping enables state reuse with different data sources.

ros2 run yasmin_demos remap_demo.py

Expected Output

The state machine will:

  1. STATE1: Read msg1 (remapped as foo_data) → prints "test1"
  2. STATE2: Read msg2 (remapped as foo_data) → prints "test2"
  3. STATE3: Read foo_out_data (remapped as bar_data) → prints "test2"