XML State Machines
YASMIN Factory allows you to define state machines using XML files, enabling configuration without code changes. This is particularly useful for:
- Rapid prototyping: Test different behaviors quickly
- Non-programmer friendly: Modify robot behaviors without coding
- Version control: Track state machine structures easily
- Runtime flexibility: Load different behaviors dynamically
What is a Plugin?
A plugin is a dynamically loadable component that extends YASMIN's functionality without modifying the core library. In the context of YASMIN, plugins are primarily used for C++ states that can be loaded at runtime from shared libraries. This architecture enables:
- Modularity: Develop and distribute states as separate packages
- Runtime loading: States are loaded when needed, not compiled into the main application
- Extensibility: Third parties can create custom states without access to core YASMIN source
- XML compatibility: C++ states defined as plugins can be referenced in XML state machine definitions
How to Create a Plugin
To create a C++ state plugin for use with YASMIN Factory XML, follow these steps:
1. Create your State Class
Define a class that inherits from yasmin::State.
Implement the required execute() method and register
outcomes.
// my_package/include/my_package/my_custom_state.hpp
#ifndef MY_PACKAGE__MY_CUSTOM_STATE_HPP_
#define MY_PACKAGE__MY_CUSTOM_STATE_HPP_
#include "yasmin/state.hpp"
#include "yasmin/blackboard.hpp"
namespace my_package {
class MyCustomState : public yasmin::State {
public:
MyCustomState();
std::string execute(yasmin::Blackboard::SharedPtr blackboard) override;
};
} // namespace my_package
#endif
2. Implement and Export the Plugin
Implement your state and use the
PLUGINLIB_EXPORT_CLASS macro to register it.
// my_package/src/my_custom_state.cpp
#include "my_package/my_custom_state.hpp"
#include <pluginlib/class_list_macros.hpp>
namespace my_package {
MyCustomState::MyCustomState() : yasmin::State({"success", "failure"}) {}
std::string MyCustomState::execute(
yasmin::Blackboard::SharedPtr blackboard) {
// Your state logic here
return "success";
}
} // namespace my_package
PLUGINLIB_EXPORT_CLASS(my_package::MyCustomState, yasmin::State)
3. Create the Plugin Description XML
Create a plugins.xml file in your package root that
describes your plugin:
<!-- my_package/plugins.xml -->
<library path="my_package">
<class type="my_package::MyCustomState" base_class_type="yasmin::State">
<description>My custom YASMIN state plugin</description>
</class>
</library>
4. Update CMakeLists.txt
Configure your CMakeLists.txt to build the plugin library and export the plugin description:
# Add the library
add_library(my_custom_state SHARED src/my_custom_state.cpp)
target_include_directories(my_custom_state PUBLIC include)
ament_target_dependencies(my_custom_state yasmin pluginlib)
# Export the plugin
pluginlib_export_plugin_description_file(yasmin plugins.xml)
# Install
install(TARGETS my_custom_state
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
5. Use in XML State Machine
Reference your plugin in XML state machine definitions using the
format
package_name/ClassName:
<State id="my_state" type="cpp" class="my_package/MyCustomState">
<Transition outcome="success" target="next_state"/>
<Transition outcome="failure" target="error_handler"/>
</State>
For Python states, no plugin system is needed. Python states are
loaded dynamically using Python's import system. Simply specify the
module and class in the XML definition with type="py".
XML Structure
A YASMIN XML state machine definition includes state declarations, transitions, and optional remapping. The example below shows a simple state machine with two states:
-
StateMachine element: Root container with
idanddefault_outcomeattributes -
State elements: Define individual states with their
type (
pyorcpp), module/class information - Transition elements: Specify how states connect - which outcome leads to which target state
-
Self-loops: Notice
state1can transition back to itself onfailoutcome, useful for retry logic
<StateMachine id="example_sm" default_outcome="finish">
<State id="state1" type="py" module="my_package.states" class="MyState">
<Transition outcome="success" target="state2"/>
<Transition outcome="fail" target="state1"/>
</State>
<State id="state2" type="py" module="my_package.states" class="AnotherState">
<Transition outcome="done" target="finish"/>
</State>
</StateMachine>
State Types
YASMIN Factory supports both Python and C++ states in the same state machine, enabling you to leverage existing code in either language or mix implementations based on performance needs and developer expertise. The factory automatically handles the language-specific loading and instantiation mechanisms.
When mixing Python and C++ states in the same state machine, they can communicate through the blackboard, but only with primitive data types:
-
int- Integer numbers -
float- Floating-point numbers -
bool- Boolean values -
string- Text strings
Complex objects, custom classes, or ROS messages cannot be directly shared between Python and C++ states through the blackboard.
Python States
Python states are loaded dynamically from the specified module and
class. The type="py" attribute tells the factory to use
Python's import system to locate and instantiate the state class.
<State id="python_state" type="py" module="my_package.states" class="MyPythonState">
<Transition outcome="success" target="next_state"/>
</State>
C++ States
C++ states use ROS 2's pluginlib system for dynamic loading. The
type="cpp" attribute indicates that the state should be
loaded as a plugin, with the class name following the
package_name/ClassName convention used by pluginlib.
<State id="cpp_state" type="cpp" class="my_package/MyCppState">
<Transition outcome="success" target="next_state"/>
</State>
Loading XML State Machines
Once you've defined your state machine in XML, the YASMIN Factory API provides simple methods to load and execute it. The factory handles all the parsing, state instantiation, and wiring of transitions automatically.
Python
In Python, create a factory instance and use
create_sm_from_file() to load the XML definition. The
returned state machine can be executed directly by calling it as a
function.
from yasmin_factory import YasminFactory
factory = YasminFactory()
sm = factory.create_sm_from_file("path/to/state_machine.xml")
# Execute the state machine
outcome = sm()
print(outcome)
C++
The C++ API mirrors the Python interface. Include the factory header, create a factory instance, and load the XML file. The state machine is returned as a shared pointer that can be dereferenced and executed.
#include "yasmin_factory/yasmin_factory.hpp"
yasmin_factory::YasminFactory factory;
auto sm = factory.create_sm_from_file("path/to/state_machine.xml");
// Execute the state machine
std::string outcome = (*sm.get())();
YASMIN_LOG_INFO(outcome.c_str());
Advanced Features
YASMIN Factory supports advanced state machine patterns through XML configuration, enabling complex behaviors without modifying code. These features include data remapping, hierarchical composition, and parallel execution.
Blackboard Remapping
Remapping allows you to connect state inputs and outputs to different blackboard keys, making states more reusable. This is essential when using generic states in different contexts or when multiple instances of the same state need to access different data.
<State id="my_state" type="py" module="pkg.states" class="MyState">
<Remap from="input_data" to="sensor_reading"/>
<Remap from="output_data" to="processed_result"/>
<Transition outcome="success" target="next_state"/>
</State>
Nested State Machines
Hierarchical state machines allow you to organize complex behaviors into manageable sub-machines. A nested state machine appears as a single state to its parent, with its internal outcomes mapped to transitions in the outer machine. This promotes modularity and reusability of behavior patterns.
<StateMachine id="main_sm" default_outcome="done">
<StateMachine id="sub_sm" default_outcome="sub_done">
<State id="sub_state1" type="py" module="pkg" class="State1"/>
<State id="sub_state2" type="py" module="pkg" class="State2"/>
</StateMachine>
<Transition from="sub_sm" outcome="sub_done" target="done"/>
</StateMachine>
Concurrence States
Concurrence states execute multiple child states simultaneously in parallel threads. The outcome mapping defines which combination of child outcomes determines the overall concurrence outcome. This is useful for implementing behaviors that monitor multiple conditions simultaneously or execute independent tasks in parallel for efficiency.
<Concurrence id="concurrent_states" default_outcome="defaulted">
<State id="state_a" type="py" module="pkg" class="StateA"/>
<State id="state_b" type="py" module="pkg" class="StateB"/>
<OutcomeMap outcome="all_done">
<StateOutcome state="state_a" outcome="success"/>
<StateOutcome state="state_b" outcome="success"/>
</OutcomeMap>
</Concurrence>
Example: Mixed Language State Machine
This example demonstrates a state machine using both Python and C++ states:
<StateMachine outcomes="outcome4">
<State name="Foo" type="cpp" class="yasmin_demos/FooState">
<Transition from="outcome1" to="Bar"/>
<Transition from="outcome2" to="outcome4"/>
</State>
<State name="Bar" type="py" module="yasmin_demos.bar_state" class="BarState">
<Transition from="outcome3" to="Foo"/>
</State>
</StateMachine>
YASMIN