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.
Nav2 (BehaviorTree.CPP 4.x, Groot2 protocol)¶
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:
Ports come in pairs.
Groot2Publisherbindsgroot_server_portfor request/reply andgroot_server_port + 1for its publish socket. That is why Nav2's own defaults are1667and1669and not1667/1668—1668belongs 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:
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:
| 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:
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));
}
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);
Defaults in btview already match BehaviorTree.CPP's, so btview --protocol v3 is
usually enough.
Only one
PublisherZMQmay 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:
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.
Checking the link¶
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.