Creating Behavior Plugins — Best Practices
Reference docs: src/docs/docs/concepts/creating_behaviors/creating_behaviors.md, src/docs/docs/how_to/custom_behaviors/adding_ports/adding_ports.md, src/behavior_interface/README_PUBLIC.md.
Choosing a Base Class
- SharedResourcesNode<BT::SyncActionNode> — Synchronous, completes in a single tick. Use when work is fast (<1ms). Override tick().
- AsyncBehaviorBase — For long-running tasks (planning, vision inference). Returns RUNNING until complete. Override doWork() returning tl::expected<bool, std::string>.
- GetMessageFromTopicBehaviorBase<MsgT> — Subscribes to a ROS topic and outputs the latest message.
- ServiceClientBehaviorBase<SrvT> — Calls a ROS service. Override getServiceName(), createRequest(), processResponse().
- ActionClientBehaviorBase<ActionT> — Calls a ROS action server.
- For Behaviors that don't need ROS at all, inherit directly from BT::SyncActionNode or BT::StatefulActionNode.
Class Naming
- Use PascalCase: ComputeTrayPlacePositionsUsingAprilTags, not compute_tray_place_positions.
- Be descriptive and specific. Prefer GetCenterMostAprilTag over GetTag. Include the method/sensor when relevant (e.g., UsingAprilTags, FromPointCloud).
- File names use snake_case matching the class: compute_tray_place_positions_using_apriltags.hpp/.cpp.
Header File (include/.../my_behavior.hpp)
- Include the PickNik copyright header (use the current year).
- Use #pragma once.
- Mark the class final unless designed for inheritance.
- Add a @brief one-liner and @details block with a markdown port table listing every port:
/*** @brief Brief description of what this Behavior does.** @details Longer explanation of the algorithm, inputs, and outputs.** | Data Port Name | Port Type | Object Type |* | --------------- | --------- | --------------------------------- |* | input_pose | input | geometry_msgs::msg::PoseStamped |* | output_pose | output | geometry_msgs::msg::PoseStamped |*/
- Port Type column values: input, output, or bidirectional.
- Document the constructor parameters with @param tags.
Source File (src/.../my_behavior.cpp)
Port Constants
Define port name constants as constexpr auto in an anonymous namespace. Use the kPortID prefix:
Port Definitions (providedPorts())
- BT::InputPort<T> — Data flowing into the Behavior (read-only). This is the most common port type.
- Required inputs use a blackboard placeholder: BT::InputPort<T>("name", "{placeholder}", "Description.")
- Optional inputs with defaults: BT::InputPort<T>("name", default_value, "Description.")
- BT::OutputPort<T> — Data written by the Behavior. Always use a blackboard placeholder: BT::OutputPort<T>("name", "{placeholder}", "Description.")
- BT::BidirectionalPort<T> — Reads and writes the same port (e.g., accumulating into a vector). Use sparingly.
- Most ports should be InputPort. Only use OutputPort for data the Behavior produces. Only use BidirectionalPort when the Behavior must modify an existing blackboard value in-place.
Port Descriptions
Every port must have a description string. Descriptions should:
- Be a complete sentence ending with a period.
- Explain the purpose, not just restate the type (e.g., "Distance in meters between adjacent place positions." not "A double.").
- Mention units where applicable (meters, seconds, radians).
- Note constraints or special values (e.g., "0 means forever.", "Negative indexes start backwards from the last element.").
- For optional ports, state what happens when not provided (e.g., "If not provided, visualization is skipped.").
Port Naming Conventions
- Use snake_case for port names: input_pose, camera_info, tray_apriltag_id.
- Prefix input ports with context when ambiguous: input_image, input_pose, target_frame_id.
- Suffix output ports descriptively: place_positions, detection_pose, annotated_image.
- For topic name ports, use _topic suffix: visualization_topic.
- Match existing conventions — check similar Behaviors before inventing new names.
Metadata
Provide both subcategory and description metadata:
Subcategory examples: "Vision", "Pose Handling", "Vector Handling", "Motion - Planning", "Perception - 2D Image".
The description is HTML wrapped in <p> tags, used in the Behavior Hub UI:
Input Validation
Use getRequiredInputs() for ports that must be set:
For optional ports with defaults, use .value() or .value_or():
Registration
- Add #include in register_vision_behaviors.cpp (or the appropriate register_*_behaviors.cpp), keeping alphabetical order.
- Add registerBehavior<MyBehavior>(factory, "MyBehavior", shared_resources); in the registration function.
- Add "MyBehavior" to the corresponding kXxxBehaviorsScenario list in test/test_load_behavior_loader_plugin.cpp, alphabetically.
CMake
Add a static library block in the appropriate CMakeLists.txt:
Then add it to the core_behaviors_vision (or core_behaviors) PRIVATE link list in the shared library target.
Testing
- At minimum, every Behavior must be listed in test_load_behavior_loader_plugin.cpp — this verifies it can be loaded and instantiated.
- For thorough testing, use the WithBehavior<T> test fixture from moveit_pro_behavior_interface:
- Define a port setter map with BEGIN_BEHAVIOR_PORT_SETTER_MAP / DEFINE_BEHAVIOR_PORT_SETTER / DEFINE_OPTIONAL_BEHAVIOR_PORT_SETTER.
- Use INSTANTIATE_SYNC_BEHAVIOR_PORT_NOT_SET_TESTS (or ASYNC variant) to auto-generate tests for each required port.
- Always set tf2_spin_thread=false in BehaviorContext for tests.
Common Patterns
- TF lookups: Use getBehaviorContext()->transform_buffer_ptr->canTransform() before lookupTransform(). Include <tf2_eigen/tf2_eigen.hpp> for tf2::transformToEigen().
- Image annotation: Use cv_bridge::toCvCopy() + OpenCV drawing + ROSPublisherHandle to publish annotated images.
- Camera intrinsics: Extract from CameraInfo.k[] — fx=k[0], fy=k[4], cx=k[2], cy=k[5].
- Pinhole projection: u = fx * (x/z) + cx, v = fy * (y/z) + cy.
- Publishing one-shot data to the UI: use latched QoS — rclcpp::QoS(rclcpp::KeepLast(1)).reliable().transient_local(). foxglove_bridge subscribes on the UI's behalf and the frontend adapter requests no QoS, so durability must come from the publisher: latching lets DDS deliver the most recent message to a UI that subscribes or reconnects later. Continuous-stream topics (joint states, jog commands) don't need this. Where the publisher lives depends on how long the sample must outlive the publish:
- If the latched sample only needs to reach a UI that connects during the same Objective run, a publisher held as a Behavior member (kept alive across ticks) is fine. See switch_ui_primary_view.cpp.
- If it must survive past the run — e.g. a snapshot the UI re-fetches after a page reload — a Behavior-owned publisher is destroyed when the tree is torn down at the end of the Objective, retracting the latched sample. Store it on the long-lived BehaviorContext via persistent_publishers.getOrCreate(topic, factory) instead, and clear it when the snapshot is invalidated via persistent_publishers.clear(). See send_point_cloud_to_ui.cpp (stores) and clear_snapshot.cpp (clears).
Design Tips
- .value() vs .value_or(): For ports with defaults defined in providedPorts(), use .value() — getInput() is guaranteed to succeed because the port always has a value. Reserve .value_or() for truly optional ports that may not be wired in the behavior tree at all.
- Keep tick() focused: If tick() grows beyond ~40 lines, extract logical blocks into private helper methods (e.g., publishVisualizationMarkers()). This makes tick() read like an outline and keeps each method testable.
- Name utilities broadly: Name utility files/classes by what they do, not by the first use case. For example, marker_utils (handles lines, poses, text, delete-all) is better than pose_marker_utils.
Generated via doxygen2docusaurus 2.2.2 by Doxygen 1.9.8.