Skip to content

Connecting to a real robot

btview speaks the two ZMQ protocols BehaviorTree.CPP ships with. Nothing ROS-specific is involved: the publisher binds a TCP port and btview connects to it. That is the whole integration.


Turn monitoring on

Monitoring is off by default, and a disabled publisher looks exactly like a firewall problem: nothing listening, no log line explaining why. Turn it on first, always.

The setting is per navigator, not per node — each navigator plugin owns its own tree, so each gets its own switch and its own port. In your nav2_params.yaml:

bt_navigator:
  ros__parameters:
    navigators: ["navigate_to_pose", "navigate_through_poses"]
    navigate_to_pose:
      plugin: "nav2_bt_navigator::NavigateToPoseNavigator"
      enable_groot_monitoring: true
      groot_server_port: 1667
    navigate_through_poses:
      plugin: "nav2_bt_navigator::NavigateThroughPosesNavigator"
      enable_groot_monitoring: true
      groot_server_port: 1669

Restart the stack, then:

btview                        # on the robot
btview --host 192.168.1.42    # from anywhere that can reach it

Ports come in pairs. Groot2Publisher binds groot_server_port for request/reply and groot_server_port + 1 for its publish socket. That is why Nav2's own defaults are 1667 and 1669 and not 1667/1668 — 1668 belongs to the first navigator. Leave the odd gaps alone.

Two trees, two monitors

Those are two different behaviour trees, live at the same time. Watch whichever one the robot is currently executing:

btview --port 1667    # navigate_to_pose
btview --port 1669    # navigate_through_poses

A navigator only ticks while it holds a goal, so the idle one shows a static tree — that is the tree being IDLE, not a broken connection.

Older releases put these at the top level

The parameters moved into the navigator block when Nav2 gained pluggable navigators. On Humble-era stacks they sit directly under ros__parameters, and BehaviorTree.CPP 3.x spells them differently again. Rather than memorising release history, ask the robot:

ros2 param list /bt_navigator | grep -i groot
What you see What it means
navigate_to_pose.groot_server_port Current layout — the YAML above, --protocol v4
groot_server_port Older BehaviorTree.CPP 4.x — same keys, one level up, --protocol v4
groot_zmq_publisher_port / groot_zmq_server_port BehaviorTree.CPP 3.x — --protocol v3 with those two ports

Or skip the publisher entirely

If grep finds nothing, or you cannot change parameters, or the port is unreachable — Nav2 publishes nav2_msgs/BehaviorTreeLog on behavior_tree_log regardless of any of this, and btview can read it:

btview --source ros2://behavior_tree_log

Nothing to enable, no port to reach, and being push-based it cannot miss a transition between polls. It needs rclpy (so run it from a sourced workspace), and the topic carries no tree structure — pass ?tree=your_bt.xml for the real hierarchy, or accept a flat list of nodes as they tick. See transports.


Your own BehaviorTree.CPP 4.x application

Create the publisher after you create the tree, and keep it alive as long as the tree is:

#include "behaviortree_cpp/loggers/groot2_publisher.h"

auto tree = factory.createTree("MainTree");
BT::Groot2Publisher publisher(tree, 1667);   // must outlive the tick loop

while (rclcpp::ok()) {
  tree.tickOnce();
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
btview --port 1667

BehaviorTree.CPP 3.x (PublisherZMQ)

3.x uses two separate sockets and a different encoding:

#include "behaviortree_cpp_v3/loggers/bt_zmq_publisher.h"

// PublisherZMQ(tree, max_msg_per_second = 25, publisher_port = 1666, server_port = 1667)
BT::PublisherZMQ publisher(tree, 25, 1666, 1667);
btview --protocol v3 --server-port 1667 --publisher-port 1666

Defaults in btview already match BehaviorTree.CPP's, so btview --protocol v3 is usually enough.

Only one PublisherZMQ may exist per process — upstream enforces this with a global guard. If construction throws, something else already made one.


Reaching the port

Over SSH

The simplest safe option — no ports exposed on the network:

ssh -L 1667:localhost:1667 robot@192.168.1.42
btview                                          # in another local shell

For v3 forward both ports (-L 1667:localhost:1667 -L 1666:localhost:1666).

Or just run btview on the robot

It is a terminal tool with a ~10 MB dependency footprint. Installing it on the robot and running it inside your SSH session avoids the port question entirely, and is the mode it was designed for.

In containers

The publisher binds inside its network namespace. Either share the host network (network_mode: host), or publish the port pair and connect to the container's address — see docker.md.


btview info --host 192.168.1.42
endpoint       192.168.1.42:1667
protocol       Groot2 / BT.CPP 4.x
tree uuid      642d2fd37b804713ad6e7175f1fa7ba8
tree           MainTree
nodes          61
by kind        action 34  condition 9  control 15  decorator 3
tree fetch     8.4 ms
status poll    min 1.1 ms · avg 2.3 ms · max 9.8 ms  (20 samples)

If status poll averages tens of milliseconds, lower --poll-hz; you are competing with the robot's own work for that socket.

Troubleshooting

Symptom Likely cause
timed out … waiting for FULLTREE Monitoring is off, or the port is wrong or blocked
Connects, but every node is IDLE The tree is not being ticked — send a goal, or you are on the other navigator's port
not a valid BehaviorTree FlatBuffer v3 client against a v4 publisher — drop --protocol v3
expected a <root> element v4 client against a v3 publisher — add --protocol v3
Tree appears, statuses never change Polling paused (p), or the tree really is idle
Node types all look like actions The publisher advertises no node model; glyphs fall back to tree shape

Load

A status poll is one request/reply carrying three bytes per node — about 200 bytes for a 60-node tree. At the default 10 Hz that is a couple of kB/s, and BehaviorTree.CPP answers it from a cached buffer. Still, it shares a socket with the tree that is driving your robot: on a busy machine, --poll-hz 2 is plenty for watching a tree you are eyeballing.

Be aware of what polling costs you in resolution. The Groot2 protocol only exposes snapshots, so btview derives transitions by diffing consecutive polls: a node that goes RUNNING and SUCCESS between two polls shows up as one change, not two. Poll faster when you are hunting a flicker, slower when you just want to watch. BehaviorTree.CPP 3.x is different — its publisher pushes transition records itself, so --protocol v3 misses nothing regardless of poll rate.