Lecture#

Prerequisites#

Ensure you have followed the simulation instructions and have everything set up before running any code in this lecture.

Before You Start
  • Remove the log/, build/, and install/ folders.

  • Do a git pull.

  • Compile lecture 12 packages only:

colcon build --symlink-install \
    --cmake-args -DCMAKE_BUILD_TYPE=Release \
    --packages-up-to lecture12_meta
  • Source your workspace.

  • Verify Gazebo launches cleanly:

ros2 launch rosbot_gazebo husarion_world.launch.py

Namespaces#

A namespace is a prefix applied to a ROS 2 node and all its associated entities (topics, services, parameters). It provides a hierarchical organization that prevents naming conflicts when running multiple instances of the same node.

Without namespaces, running two camera nodes produces a topic conflict. With namespaces, each instance lives in its own isolated prefix.

The Problem: Identical Topic Names#

Consider a robot with three cameras. Each runs the same camera_node and publishes on /image_raw. Running two instances immediately causes a conflict: both nodes compete for the same topic, and subscribers receive data from whichever node happened to publish last.

Note

A namespace solves this by prepending a unique prefix. camera_node under namespace /front publishes on /front/image_raw; under /rear it publishes on /rear/image_raw. The node code is identical in both cases.

What a Namespace Affects#

When you apply namespace /rear to a node:

  • The node name becomes /rear/camera_node.

  • Every relative topic or service (e.g., image_raw) becomes /rear/image_raw.

  • Parameters are stored under the fully-qualified node name /rear/camera_node.

  • Absolute topics (starting with /) are not affected by the namespace.

Important

If a topic is defined with a leading slash (e.g., /image_raw), the namespace has no effect on it. Always use relative topic names inside node code if you intend the namespace to apply.

Applying Namespaces#

Namespaces can be applied from the command line at launch time, or declared inside a Python launch file.

Namespace applied to node and topic names

Fig. 94 Namespace applied to node and topic names.#

Namespace applied to node and topic names

Fig. 95 Namespace applied to node and topic names.#

CLI: ``–ros-args -r __ns``

ros2 run namespace_demo camera_demo_exe --ros-args -r __ns:=/rear
  • __ns is a special remapping target for the namespace.

  • Verify with ros2 node list and ros2 topic list.

Demonstration: CLI Namespace

Command

What to observe

T1

ros2 run namespace_demo camera_demo_exe --ros-args -r __ns:=/front

Node starts as /front/camera_node

T2

ros2 run namespace_demo camera_demo_exe --ros-args -r __ns:=/rear

Second instance starts as /rear/camera_node

T3

ros2 node list

Both /front/camera_node and /rear/camera_node appear

T3

ros2 topic list

/front/image_raw and /rear/image_raw (isolated)

T3

rqt_graph

Two separate nodes, each publishing on its own namespaced topic

Observe

How many nodes appear in rqt_graph? What are their full names? What topics do each publish?

Launch File: ``namespace`` Argument

front_camera = Node(
    package='namespace_demo',
    executable='camera_demo_exe',
    namespace='front',
    output='screen',
)
rear_camera = Node(
    package='namespace_demo',
    executable='camera_demo_exe',
    namespace='rear',
    output='screen',
)
Demonstration: Launch File Namespace

Command

What to observe

T1

ros2 launch namespace_demo multi_camera.launch.py

Both camera nodes start in separate namespaces

T2

ros2 node list

/front/camera_node and /rear/camera_node appear

T2

ros2 topic list

/front/image_raw and /rear/image_raw (same result as CLI)

Remapping#

Remapping lets you change the name of any ROS 2 entity (node name, topic, service, parameter) at runtime without modifying the node’s source code. It is the primary mechanism for adapting existing nodes to new system architectures.

Note

Every node in a ROS 2 system must have a unique name. Node remapping lets you run the same executable under a different name without touching the code.

Node Remapping#

Node remapping assigns a new name to a running node without modifying its source code. This is essential when launching multiple instances of the same executable.

CLI: ``–ros-args -r __node``

ros2 run remapping_demo camera_demo_exe --ros-args -r __node:=front_camera
  • __node is the special remapping target for the node name.

  • The node that would normally be named /camera_node is now /front_camera.

  • All parameters are stored under the remapped name.

  • All CLI commands must use the new name: ros2 node info /front_camera.

Demonstration: CLI Node Remapping

Command

What to observe

T1

ros2 run remapping_demo camera_demo_exe --ros-args -r __node:=front_camera

Node starts as /front_camera instead of /camera_node

T2

ros2 run remapping_demo camera_demo_exe --ros-args -r __node:=rear_camera

Second instance starts as /rear_camera (no name conflict)

T3

ros2 node list

Both /front_camera and /rear_camera appear

Launch File: ``name`` Argument

front_camera = Node(
    package="remapping_demo",
    executable="camera_demo_exe",
    name="front_camera",
    output="screen",
)

rear_camera = Node(
    package="remapping_demo",
    executable="camera_demo_exe",
    name="rear_camera",
    output="screen",
)
Demonstration: Launch File Node Remapping

Command

What to observe

T1

ros2 launch remapping_demo node_remap.launch.py

Both nodes start with remapped names

T2

ros2 node list

/front_camera and /rear_camera appear (not /camera_node)

Topic Remapping#

Topic remapping redirects a publisher or subscriber to use a different topic name, allowing nodes with incompatible naming conventions to communicate without modifying either node.

CLI: ``-r original:=new``

ros2 run remapping_demo camera_demo_exe --ros-args -r image_raw:=/sensors/front/image
  • The format is -r original_name:=new_name.

  • The topic /image_raw is now published as /sensors/front/image.

  • Subscribers must use the remapped name.

Launch File: ``remappings`` Argument

front_camera = Node(
    package='remapping_demo',
    executable='camera_demo_exe',
    name='front_camera',
    remappings=[
        ('image_raw', '/sensors/front/image'),
    ],
    output='screen',
)

Tip

Use relative names (without a leading /) in the left side of the remapping tuple so that namespaces continue to compose correctly. Absolute names on the right side are fine when you want a fixed destination regardless of namespace.

Demonstration: Connecting Two Nodes with Incompatible Names

A camera_node publishes on image_raw. An image_processor subscribes on camera/image. Without remapping, they never connect.

Command

What to observe

T1

ros2 launch remapping_demo topic_remap.launch.py

Both camera and processor nodes start

T2

ros2 topic list

Remapped topic /sensors/front/image appears

T2

ros2 topic echo /sensors/front/image --once

A message is received (publisher and subscriber are connected)

T2

rqt_graph

Arrow from camera node through remapped topic to processor node

Parameter Remapping#

Parameters can be overridden at launch time using -p on the CLI or the parameters argument in a launch file. This was introduced in L10.

Namespaces vs. Remapping#

Situation

Tool

Why

Running the same node multiple times (e.g., three cameras)

Namespace

All topics, services, and parameters are isolated automatically under one prefix.

Two nodes use different names for the same data (e.g., one publishes /image, another subscribes to /camera/image)

Topic remapping

Bridges the naming gap without touching either node’s code.

Running the same executable twice in the same namespace

Node remapping

Each instance must have a unique node name; __node changes it.

Overriding a parameter value at launch time

Parameter remapping (-p)

Faster than editing a YAML file; no source change needed.

Both isolation and renaming in one launch

Namespace + remapping together

Namespace isolates the group; remapping fine-tunes individual names within it.

A namespace is a bulk operation that moves everything under a new prefix at once. Remapping is a surgical operation that changes one specific name. Use namespaces first for isolation, then remapping to fix anything the namespace alone does not handle correctly.

Behavior Trees#

A behavior tree (BT) is a hierarchical structure for organizing robot behaviors. Unlike a state machine, where transitions are encoded as edges between states, a BT separates what the robot does (leaf nodes) from how decisions are made (composite nodes). This makes behaviors modular, reusable, and easy to extend.

Core Concepts#

Every node in a behavior tree returns one of three statuses when ticked: SUCCESS, FAILURE, or RUNNING. The tree is ticked at a fixed rate; the root node propagates ticks downward and collects statuses upward.

Table 23 Node Types#

Node type

Symbol

Behavior

Sequence

-> (arrow)

Ticks children left to right. Fails on first FAILURE. Succeeds only if all children succeed. Like logical AND.

Fallback

? (question)

Ticks children left to right. Succeeds on first SUCCESS. Fails only if all children fail. Like logical OR.

Action

Rectangle

Leaf node that does real work (e.g., publish cmd_vel). Returns RUNNING while working, SUCCESS when done.

Condition

Diamond

Leaf node that checks a condition. Returns SUCCESS or FAILURE instantly, never RUNNING.

Note

A condition node never modifies world state. An action node always does. This separation is the key design principle of behavior trees.

Drive to Goal with Recovery#

Drive-to-goal BT with Fallback recovery

Fig. 96 Drive-to-goal BT with Fallback recovery.#

Drive-to-goal BT with Fallback recovery

Fig. 97 Drive-to-goal BT with Fallback recovery.#

  • The root Sequence ticks GoalNotReached? first.

  • If the robot has not reached the goal (SUCCESS), the Sequence ticks the Fallback.

  • The Fallback tries DriveForward (wrapped in a Timeout decorator) first. While driving, it returns RUNNING and the robot moves.

  • If the Timeout expires (e.g., the robot is stuck), the decorator forces FAILURE and the Fallback ticks Spin as a recovery.

  • Once the robot reaches the goal, GoalNotReached? returns FAILURE and the Sequence stops. No velocity is published and the robot halts.

The Tick#

The tick is the fundamental mechanism of a behavior tree. At each tick:

  1. The root node is ticked.

  2. The root passes the tick to its children according to its own logic.

  3. Each leaf node executes its behavior and returns SUCCESS, FAILURE, or RUNNING.

  4. Statuses propagate back up to the root.

    • RUNNING: the action is still in progress.

    • SUCCESS: the action or condition completed successfully.

    • FAILURE: the action or condition failed. The parent composite decides what to do next.

py_trees and py_trees_ros#

py_trees is a pure Python behavior tree library. py_trees_ros adds ROS 2 integration: a tick loop driven by a ROS 2 timer, and pre-built behaviors for subscribing to topics, calling services, and sending action goals.

Installation:

sudo apt install ros-jazzy-py-trees ros-jazzy-py-trees-ros
Table 24 Key Classes#

Class

Purpose

py_trees.behaviour.Behaviour

Base class for all custom leaf nodes.

py_trees.composites.Sequence

Composite: ticks children in order (AND).

py_trees.composites.Selector

Composite: ticks children until one succeeds (OR).

py_trees_ros.trees.BehaviourTree

Wraps a py_trees tree in a ROS 2 node with a timer-driven tick loop.

py_trees.common.Status

Enum: SUCCESS, FAILURE, RUNNING.

Note

py_trees.composites.Selector is the py_trees name for a Fallback node. The terms are interchangeable.

Writing a Custom Leaf Node#

Every custom behavior inherits from py_trees.behaviour.Behaviour and overrides four methods:

import py_trees

class MyBehaviour(py_trees.behaviour.Behaviour):
    def __init__(self, name: str):
        super().__init__(name=name)

    def setup(self, **kwargs):
        # Called once before the first tick.
        # Acquire ROS 2 resources here (node, publishers, etc.)
        pass

    def initialise(self):
        # Called each time the node transitions from idle to running.
        # Reset internal state here.
        pass

    def update(self) -> py_trees.common.Status:
        # Called every tick while this node is active.
        # Return SUCCESS, FAILURE, or RUNNING.
        return py_trees.common.Status.SUCCESS

    def terminate(self, new_status: py_trees.common.Status):
        # Called when the node exits (succeeds, fails, or is preempted).
        pass

Scenario#

We build a behavior tree that drives a ROSbot toward a goal position using a proportional controller, monitors the robot’s odometry, and stops when the goal is reached. If DriveForward times out, a SpinInPlace recovery rotates the robot toward a fixed target heading (goal_yaw, default 0.0 rad) before retrying.

Drive-to-goal BT with Fallback recovery

Fig. 98 Drive-to-goal BT with Fallback recovery.#

Drive-to-goal BT with Fallback recovery

Fig. 99 Drive-to-goal BT with Fallback recovery.#

Review#

Table 25 bt_demo Package Files#

File

Description

drive_forward.py

Action node: steers the robot toward a goal using a P-controller (k_rho, k_alpha). Always returns RUNNING.

spin_in_place.py

Action node: rotates toward a target heading using a P-controller (k_yaw). Returns SUCCESS when within tolerance.

goal_not_reached.py

Condition node: subscribes to odometry and returns SUCCESS while far from the goal, FAILURE when within tolerance.

main_drive_to_goal.py

Entry point: assembles a Sequence with a condition gate and a Selector fallback (Timeout + Spin recovery), reads ROS 2 parameters, and runs the tree.

drive_to_goal.launch.py

Launch file: exposes all tuneable values (goal, gains, timeout) as launch arguments.

Decorators#

A decorator wraps a single child node and modifies how its return status is interpreted or how long it is allowed to run.

Table 26 Common Decorators#

Decorator

Effect

Inverter

Flips SUCCESS to FAILURE and vice versa. Leaves RUNNING unchanged.

Timeout

Returns FAILURE if the child is still RUNNING after a set duration.

Retry

Re-ticks the child up to N times on FAILURE before propagating.

SuccessIsRunning

Converts SUCCESS to RUNNING, useful for continuous monitoring.

Example: Timeout

drive = DriveForward(name='DriveForward', goal_x=goal_x, goal_y=goal_y,
                     k_rho=k_rho, k_alpha=k_alpha)

drive_with_timeout = py_trees.decorators.Timeout(
    child=drive,
    name=f'DriveForward ({timeout_duration} s)',
    duration=timeout_duration,
)

Decorators in py_trees are found in py_trees.decorators. They wrap any single node; they cannot have more than one child.

Reading the Terminal Output#

When unicode_tree_debug=True, py_trees_ros prints the tree on every tick. Here is an example:

[-] DriveToGoal [*]
    --> GoalNotReached? [✓]
    {o} DriveOrRecover [*]
        -^- DriveForward (10.0 s) [*] -- remaining: 6.8s
            --> DriveForward [*]
        --> Spin
Table 27 Terminal Symbols#

Symbol

Node type

Meaning

[-]

Sequence

Ticks children left-to-right (AND).

{o}

Selector

Tries children until one succeeds (OR / Fallback).

-^-

Decorator

Wraps a single child and modifies its result.

-->

Leaf

A behaviour node (action or condition) with no children.

[*]

RUNNING

The node is currently active.

[✓]

SUCCESS

The node completed successfully.

[✗]

FAILURE

The node failed.

(blank)

INVALID

The node has not been ticked yet.

Demonstration#

bt_demo Demonstration

Command

T1

ros2 launch rosbot_gazebo empty_world.launch.py rviz:=False

T2

ros2 launch bt_demo drive_to_goal.py

T2

ros2 launch bt_demo drive_to_goal.py goal_x:=5.0 goal_y:=3.0 k_rho:=0.6

T2

ros2 launch bt_demo drive_to_goal.py goal_x:=10.0 timeout_duration:=3.0 goal_yaw:=1.57

T3

ros2 topic echo /odometry/filtered --field pose.pose.position

T3

ros2 topic echo /cmd_vel