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:
- Reuse the same state class with different data keys (avoiding code duplication)
- Connect outputs from one state to inputs of another with different names (data pipelining)
- Maintain clear data flow without modifying state implementation (separation of concerns)
- Create generic states that work with any data by remapping at configuration time
- Build state libraries that can be composed flexibly in different state machines
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:
-
STATE1: Read
msg1(remapped asfoo_data) → prints "test1" -
STATE2: Read
msg2(remapped asfoo_data) → prints "test2" -
STATE3: Read
foo_out_data(remapped asbar_data) → prints "test2"
YASMIN