4. Runtime SDK & Extensions
π Duration: 1-2 hours
πͺ Level: Advanced
Tutorial Overviewβ
In this tutorial you will create custom robot configuration packages, build new Behavior plugins, and learn how to interact with MoveIt Pro through the runtime SDK.
By the end of this tutorial, you should understand:
- How to create and customize robot configuration packages for your own robot (including inheriting from existing configurations).
- How to develop custom Behaviors (plugins)
- How to integrate external ROS 2 interfaces (like topics, services, and actions) into MoveIt Pro Behaviors.
- How to use the MoveIt Pro SDK Python interface and standard ROS 2 CLI tools to programmatically run Objectives (Behavior Trees) without the GUI.
Pre-reqsβ
You should have already installed MoveIt Pro. We will assume you have already completed Tutorial 3 and know how to build Behavior Trees.
MoveIt Pro Workspaceβ
In MoveIt Pro, all robot configurations, Behaviors, and external code are represented as
ROS 2 packages in a ROS workspace.
This is where the moveit_pro
launcher will look to build and run your robot configuration.
Below is an example of a typical MoveIt Pro workspace:
moveit_pro_example_ws/
βββ Dockerfile
βββ docker-compose.yaml
βββ colcon-defaults.yaml
βββ src/
βββ lab_sim/
β βββ config/
β βββ launch/
β βββ objectives/
β βββ src/
β βββ package.xml
β βββ CMakeLists.txt
βββ additional_configuration_packages/
βββ external_dependencies/
An empty workspace is provided as a starting point, called moveit_pro_empty_ws, which you can fork and add your own robot configuration package to. If there are changes or additions to our recommended configuration, your fork will nudge you to sync the upstream changes.
User Workspacesβ
In production systems, it is often desirable to have a customized Docker build and bring-up procedure specific to that hardware. The MoveIt Pro Runtime is distributed through Docker images. When the robot starts, Docker services will launch the MoveIt Pro runtime, drivers, developer platform user interface, and/or additional Docker containers using your user image. A user image is built using the base MoveIt Pro image, your ROS 2 workspace, and MoveIt Pro configuration packages according to the steps in your Dockerfile.
The following diagram illustrates how a production system would share and build packages in the MoveIt Pro runtime:
The ROS 2 workspace can be built into the image as part of the Docker build context, mounted at runtime, or some combination of both. See our Docker Containerization page for more detail.
Robot Configuration Packagesβ
A robot configuration package is a ROS 2 package that describes your robot and its capabilities.
It defines:
- Robot and environment (URDF and MJCF) models
- Planner settings (motion planners, inverse kinematics solvers, allowable collisions, etc.)
- Real-time Controller settings
- Camera settings
- Objectives (Applications)
- Saved waypoints
- Launch files: To start drivers/simulators, additional ROS nodes/processes, etc.
- Custom Behaviors (C++ source files)
Creating a New Robot Configurationβ
Creating a custom robot configuration package from scratch is a huge topic covered in this how to guide, but it is outside the scope of this tutorial. Instead we will "fork" an existing package through inheritance.
Inherited Robot Configuration Packagesβ
Package inheritance is a powerful tool that allows you to create base robot configuration packages that contain a generic configuration of your robot, then use inherited robot config packages to add or override certain aspects.
Some example use cases for inherited packages:
- Minor hardware differences
- Differences in end effectors (lacks a gripper, or the robot has multiple end-effectors to choose from)
- IP addresses, serial numbers, specific kinematic calibrations, or other unique details
- Additional Objectives, Behaviors, or sensors
- Mobile base and navigation Behaviors
- Perception and ML Behaviors
An example inheritance structure:
Create a new robot config based on an existing configβ
In the following we will create a robot configuration package (called example_config
) that will be built on an existing base robot config package (in this case, lab_sim
):
We will assume your user workspace is located at the standard ~/moveit_pro/moveit_pro_example_ws
location. You can modify the below paths for any location, however.
-
First we begin with a
config.yaml
. Lets create it now:mkdir -p ~/moveit_pro/moveit_pro_example_ws/src/example_config/config
touch ~/moveit_pro/moveit_pro_example_ws/src/example_config/config/config.yaml -
In the editor of your choice, add the following to
config.yaml
:based_on_package: lab_sim
-
Navigate to the top level of your package
cd ~/moveit_pro/moveit_pro_example_ws/src/example_config
-
Add three files:
CMakeLists.txt
,package.xml
, andlaunch/agent_bridge.launch.xml
with the following contents:CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(example_config)
find_package(ament_cmake REQUIRED)
# Add waypoints, Objectives, and other folders here if you want to override them
install(DIRECTORY config launch DESTINATION share/${PROJECT_NAME})
ament_package()package.xml
<?xml version="1.0"?>
<package format="3">
<name>example_config</name>
<version>0.1.0</version>
<description>Example derived MoveIt Pro config package</description>
<maintainer email="support@picknik.ai">MoveIt Pro Maintainer</maintainer>
<license>BSD-3-Clause</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<exec_depend>lab_sim</exec_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>launch/agent_bridge.launch.xml
<?xml version="1.0" encoding="UTF-8" ?>
<launch>
<include file="$(find-pkg-share moveit_studio_agent)/launch/studio_agent_bridge.launch.xml"/>
</launch>
After creating your new robot configuration package, you can rebuild and launch using:
moveit_pro build user_workspace
moveit_pro run -c example_config
When MoveIt Pro opens, you shouldn't really see any differences to the normal lab_sim
world, yet.
Override the Objectives folderβ
In order to create new Objectives specifically in this example_config
, we first need to override that folder:
- Create the
objectives
folder:
mkdir -p ~/moveit_pro/moveit_pro_example_ws/src/example_config/objectives
- Add to your
config.yaml
:
config.yaml
objectives:
objective_library_paths:
example_objective:
package_name: "example_config"
relative_path: "objectives"
- Update the
install
statement in yourCMakeLists.txt
to look like this:
CMakeLists.txt
install(DIRECTORY config launch objectives DESTINATION share/${PROJECT_NAME})
Make sure colcon builds your objectives
folder (it may not get symlinked to your install folder if it is empty)!
Developing Custom Behavior Pluginsβ
Next we will build some example Behaviors plugins in our example_config
.
In the Creating Custom Behaviors how to guide, more details for creating MoveIt Pro Behaviors are covered.
Using the graphical interface or command line, create a new Behavior and select the AsyncBehavior
type.
Open your new Behavior's .cpp
file and modify its doWork()
method with this implementation:
doWork()
tl::expected<bool,std::string> MyBehavior::doWork() {
auto ports = moveit_studio::behaviors::getRequiredInputs(getInput<int>("int_1"),
getInput<int>("int_2"));
if (!ports.has_value()) {
return tl::make_unexpected("Missing inputs: "+ports.error());
}
const auto& [a,b] = ports.value();
setOutput<int>("int_out", a + b);
return { true };
}
In the providePorts()
function add input and output ports to the Behavior:
providedPorts()
BT::PortsList MyBehavior::providedPorts(){
return {
BT::InputPort<int>("int_1",0,"The first number"),
BT::InputPort<int>("int_2",0,"The second number"),
BT::OutputPort<int>("int_out","The result")
};
}
Now, when we run our Behavior, it will add two blackboard integers for us.
Try out blackboard introspection to see your result
Adding Multiple Behavior Plugins in a Single Packageβ
By default, each new Behavior gets put into its own ROS package, but it is often more convenient to have a collection of associated Behavior plugins in the same ROS package. In this section we walk you through manually adding a new Behavior.
To add additional Behaviors to your package, we must create the relevant .cpp
and .hpp
files and register the Behavior in register_behaviors.cpp
.
Lets create a new Behavior to add two blackboard values using an external ROS service:
service_client.hpp
#pragma once
#include <example_interfaces/srv/add_two_ints.hpp>
#include <moveit_studio_behavior_interface/service_client_behavior_base.hpp>
using moveit_studio::behaviors::BehaviorContext;
using moveit_studio::behaviors::ServiceClientBehaviorBase;
using AddTwoInts = example_interfaces::srv::AddTwoInts;
namespace my_behavior
{
class MyAddTwoIntsServiceClient final : public ServiceClientBehaviorBase<AddTwoInts>
{
public:
MyAddTwoIntsServiceClient(const std::string& name, const BT::NodeConfiguration& config,
const std::shared_ptr<BehaviorContext>& shared_resources);
/** @brief Implementation of the required providedPorts() function for the hello_world Behavior. */
static BT::PortsList providedPorts();
/**
* @brief Implementation of the metadata() function for displaying metadata, such as Behavior description and
* subcategory, in the MoveIt Studio Developer Tool.
* @return A BT::KeyValueVector containing the Behavior metadata.
*/
static BT::KeyValueVector metadata();
private:
/** @brief User-provided function to get the name of the service when initializing the service client. */
tl::expected<std::string, std::string> getServiceName() override;
/**
* @brief User-provided function to create the service request.
* @return Returns a service request message. If not successful, returns an error message. Note that the criteria for
* success or failure is defined by the user's implementation of this function.
*/
tl::expected<AddTwoInts::Request, std::string> createRequest() override;
/** @brief Optional user-provided function to process the service response after the service has finished. */
tl::expected<bool, std::string> processResponse(const AddTwoInts::Response& response) override;
/** @brief Classes derived from AsyncBehaviorBase must implement getFuture() so that it returns a shared_future class member */
std::shared_future<tl::expected<bool, std::string>>& getFuture() override
{
return future_;
}
/** @brief Classes derived from AsyncBehaviorBase must have this shared_future as a class member */
std::shared_future<tl::expected<bool, std::string>> future_;
};
} // namespace my_behavior
service_client.cpp
#include <my_behavior/service_client.hpp>
// Include the template implementation for GetMessageFromTopicBehaviorBase<T>.
#include <moveit_studio_behavior_interface/impl/service_client_behavior_base_impl.hpp>
namespace my_behavior
{
MyAddTwoIntsServiceClient::MyAddTwoIntsServiceClient(
const std::string& name, const BT::NodeConfiguration& config,
const std::shared_ptr<moveit_studio::behaviors::BehaviorContext>& shared_resources)
: ServiceClientBehaviorBase<example_interfaces::srv::AddTwoInts>(name, config, shared_resources)
{
}
BT::PortsList MyAddTwoIntsServiceClient::providedPorts()
{
// This node has three input ports and one output port
return BT::PortsList({
BT::InputPort<std::string>("service_name", "/add_two_ints", "The name of the service to call."),
BT::InputPort<int>("addend1", "The first int to add to the other."),
BT::InputPort<int>("addend2", "The second int to add to the other."),
BT::OutputPort<int>("result", "{result}", "Result of the AddTwoInts service."),
});
}
BT::KeyValueVector MyAddTwoIntsServiceClient::metadata()
{
return { { "subcategory", "Example Behaviors" },
{ "description", "Calls a service to add two integers and makes the result available on an output port." } };
}
tl::expected<std::string, std::string> MyAddTwoIntsServiceClient::getServiceName()
{
const auto service_name = getInput<std::string>("service_name");
if (const auto error = moveit_studio::behaviors::maybe_error(service_name))
{
return tl::make_unexpected("Failed to get required value from input data port: " + error.value());
}
return service_name.value();
}
tl::expected<AddTwoInts::Request, std::string> MyAddTwoIntsServiceClient::createRequest()
{
const auto a = getInput<int>("addend1");
const auto b = getInput<int>("addend2");
if (const auto error = moveit_studio::behaviors::maybe_error(a, b))
{
return tl::make_unexpected("Failed to get required value from input data port: " + error.value());
}
return example_interfaces::build<AddTwoInts::Request>().a(a.value()).b(b.value());
}
tl::expected<bool, std::string> MyAddTwoIntsServiceClient::processResponse(const AddTwoInts::Response& response)
{
setOutput<int>("result", response.sum);
return { true };
}
} // namespace my_behavior
To publish a toast message with your result, add the following to your processResponse()
method:
shared_resources_->logger->publishInfoMessage("Sum:", std::to_string(response.sum));
Register the Behavior in register_behaviors.cpp
:
moveit_studio::behaviors::registerBehavior<MyAddTwoIntsServiceClient>(factory, "MyServiceClient", shared_resources);
Click to reference the full register_behaviors.cpp
#include <behaviortree_cpp/bt_factory.h>
#include <moveit_studio_behavior_interface/behavior_context.hpp>
#include <moveit_studio_behavior_interface/shared_resources_node_loader.hpp>
#include <my_behavior/my_behavior.hpp>
#include <my_behavior/service_client.hpp>
#include <pluginlib/class_list_macros.hpp>
namespace my_behavior
{
class MyBehaviorBehaviorsLoader : public moveit_studio::behaviors::SharedResourcesNodeLoaderBase
{
public:
void registerBehaviors(BT::BehaviorTreeFactory& factory,
[[maybe_unused]] const std::shared_ptr<moveit_studio::behaviors::BehaviorContext>& shared_resources) override
{
moveit_studio::behaviors::registerBehavior<MyBehavior>(factory, "MyBehavior", shared_resources);
moveit_studio::behaviors::registerBehavior<MyAddTwoIntsServiceClient>(factory, "MyServiceClient", shared_resources);
}
};
} // namespace my_behavior
PLUGINLIB_EXPORT_CLASS(my_behavior::MyBehaviorBehaviorsLoader,
moveit_studio::behaviors::SharedResourcesNodeLoaderBase);
Update CMakeLists.txt
to build your new Behavior:
set(THIS_PACKAGE_INCLUDE_DEPENDS moveit_studio_behavior_interface pluginlib example_interfaces)
add_library(
my_behavior
SHARED
src/my_behavior.cpp
src/service_client.cpp
src/register_behaviors.cpp)
Click to reference the full CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(my_behavior CXX)
find_package(moveit_studio_common REQUIRED)
moveit_studio_package()
set(THIS_PACKAGE_INCLUDE_DEPENDS moveit_studio_behavior_interface pluginlib example_interfaces)
foreach(package IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS})
find_package(${package} REQUIRED)
endforeach()
add_library(
my_behavior
SHARED
src/my_behavior.cpp
src/service_client.cpp
src/register_behaviors.cpp)
target_include_directories(
my_behavior
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
ament_target_dependencies(my_behavior
${THIS_PACKAGE_INCLUDE_DEPENDS})
# Install Libraries
install(
TARGETS my_behavior
EXPORT my_behaviorTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES
DESTINATION include)
if(BUILD_TESTING)
moveit_pro_behavior_test(my_behavior)
endif()
# Export the behavior plugins defined in this package so they are available to
# plugin loaders that load the behavior base class library from the
# moveit_studio_behavior package.
pluginlib_export_plugin_description_file(
moveit_studio_behavior_interface my_behavior_plugin_description.xml)
ament_export_targets(my_behaviorTargets HAS_LIBRARY_TARGET)
ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS})
ament_package()
Update package.xml
with our new dependency:
<depend>example_interfaces</depend>
Click to reference the full package.xml
<?xml version="1.0" encoding="utf-8" ?>
<package format="3">
<name>my_behavior</name>
<version>0.0.0</version>
<description>My description</description>
<maintainer email="support@picknik.ai">
MoveIt Pro User
</maintainer>
<author email="support@picknik.ai">
MoveIt Pro User
</author>
<license>TODO</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>moveit_studio_common</build_depend>
<depend>moveit_studio_behavior_interface</depend>
<depend>example_interfaces</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
<buildtool_depend>python3-colcon-common-extensions</buildtool_depend>
</package>
Communicating With External ROS Interfacesβ
Next we will test out our previously built custom Behavior.
You will need to install an example service server in your Docker container using the following command (while MoveIt Pro is running):
moveit_pro shell
sudo apt update && sudo apt install ros-humble-examples-rclcpp-minimal-service
Then run in that same shell:
ros2 run examples_rclcpp_minimal_service service_main
The above steps only install packages in the running instance of MoveIt Pro.
When you end the session (moveit_pro down
) the changes will not persist.
To make them permanent, install them in your production image by adding this to your Dockerfile:
Click to expand
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=bind,target=${USER_WS}/,source=. \
. /opt/overlay_ws/install/setup.sh && \
apt-get update && \
rosdep install -q -y \
--from-paths src \
--ignore-src && \
apt-get install -y --no-install-recommends \
ros-humble-examples-rclcpp-minimal-service ros-humble-ros2action
Check out the Behavior Specialization section for more examples of specialized Behaviors to communicate with ROS interfaces like topics, services, and actions.
Programmatic SDK Controlβ
While MoveIt Pro can run as a standalone application with its own UI, or as simple a runtime API. Many users integrate MoveIt Pro into their own framework with their application acting as the overarching coordinator.
While the MoveIt Pro UI is useful for building, editing, and executing Objectives, it is by no means required. MoveIt Pro can be run headless and its Objectives can be launched and communicated from external processes through ROS interfaces.
Using the MoveIt Pro SDK to Start Objectivesβ
In this section, we will use the SDK to remotely start an Objective without the UI. As an example, we will trigger the 3 Waypoints Pick and Place
Objective that ships in lab_sim
. While you can do this with Python, in this example we will use the command line.
First we need to add the MoveIt Pro SDK repository as a git submodule
to the moveit_pro_example_ws
that you already have downloaded:
cd ~/moveit_pro/moveit_pro_example_ws/src/external_dependencies
git submodule add https://github.com/PickNikRobotics/moveit_studio_sdk.git
Then we will build our user image and launch MoveIt Pro:
moveit_pro build
moveit_pro run -c lab_sim
And from a separate terminal window (shell) start the Objective using a ROS service:
moveit_pro shell
ros2 service call /execute_objective moveit_studio_sdk_msgs/srv/ExecuteObjective "{objective_name: '3 Waypoints Pick and Place'}"
To cancel a running Objective, for example an Objective that loops forever like 3 Waypoints Pick and Place
, run:
ros2 service call /cancel_objective moveit_studio_sdk_msgs/srv/CancelObjective
Using the MoveIt Pro SDK to Execute Behavior Tree XMLβ
MoveIt Pro also supports your external application programmatically generating your own Behavior Tree XML, and then sending it to the MoveIt Pro runtime to execute it. You can send the entire XML contents programatically via an action server.
While Python examples are provided here, for now we will use the command line.
First, start MoveIt Pro and in a separate terminal enter a shell (moveit_pro shell
):
Install the action client interface in the shell instance:
sudo apt update && sudo apt install ros-humble-ros2action
Send the action request with a trivial Behavior Tree XML example:
ros2 action send_goal /do_objective moveit_studio_sdk_msgs/action/DoObjectiveSequence '
objective_xml_string: |
<root BTCPP_format="4" main_tree_to_execute="test simple">
<BehaviorTree ID="test simple">
<Control ID="Sequence" name="TopLevelSequence">
<Action ID="AlwaysSuccess"/>
</Control>
</BehaviorTree>
</root>
'
You should get the following result from the action, indicating it was successful:
Goal finished with status: SUCCEEDED
Waiting for an action server to become available...
Sending goal:
objective_name: ''
objective_xml_string: "<root BTCPP_format=\"4\" main_tree_to_execute=\"test simple\">\n <BehaviorTree ID=\"test simple\">\n <Control ID=\"Sequence\" name=\"TopLevelSequence\">\n <Action ID=\"AlwaysSuccess\"/>\n </Control>\n </BehaviorTree>\n</root>\n"
parameter_overrides: []
Goal accepted with ID: d5f48811588240f19a3ab303ccdc78ce
Result:
error_code:
val: 1
message: ''
source: ''
error_message: ''
Goal finished with status: SUCCEEDED
Nice work!
Summaryβ
This tutorial introduced advanced usage of MoveIt Pro for customizing and deploying robotic applications. You learned how to use the moveit_pro CLI, create and extend robot configuration packages, develop custom Behaviors, and integrate with external ROS interfaces. You also saw how to run Objectives programmatically using the MoveIt Pro SDKβequipping you with the tools to build and deploy production-grade robotic systems.