Tutorial: watch a real Nav2 tree¶
Everything in Install can be checked with btview demo, which is
synthetic. This tutorial connects to the real thing: an actual bt_navigator from the
Nav2 project, ticking Nav2's own behaviour tree, publishing through the same
Groot2Publisher that runs on a robot.
No robot, no simulator and no ROS installation on your machine — just Docker.
Time: about five minutes, most of it pulling a 1.2 GB image.
What you are about to run¶
integration/nav2/ brings up
the smallest slice of Nav2 that ticks a tree:
flowchart LR
subgraph container["nav2_docker container"]
TF[static_transform_publisher] -.->|map → odom → base_link| BN
GL[goal loop] -->|NavigateToPose| BN[bt_navigator]
BN <-->|compute_path_to_pose, follow_path,<br/>spin, wait, backup| ST[stub servers]
BN --> P["Groot2Publisher<br/>:1667 + :1668"]
end
P -->|ZMQ| B[btview on your machine]
There is no planner, no controller, no costmap and no simulator. In their place is a script that answers the five actions and two services Nav2's tree calls, on a fixed schedule.
Those stubs are not optional. Nav2's BT action nodes resolve their action server when the
tree is constructed, so without compute_path_to_pose the XML fails to load,
bt_navigator refuses to configure, and there is no publisher to connect to at all.
Stubbing them rather than running the real thing keeps the image at 1.2 GB instead of
7.8 GB, and makes the run deterministic — the same sequence of statuses every time,
rather than whatever a simulated robot happened to do.
follow_path succeeds twice and then fails on purpose, which sends the tree down its
recovery branch, so you get to watch the interesting half too.
Everything btview reads is genuine: the publisher is the real BT::Groot2Publisher
compiled into nav2_bt_navigator, and the tree is Nav2's own
navigate_to_pose_w_replanning_and_recovery.xml. The stubs only decide what the leaves
return.
1. Start Nav2¶
git clone https://github.com/guilyx/btview && cd btview
docker compose -f integration/nav2/docker-compose.yml up nav2 -d --wait
--wait blocks until the container's healthcheck passes, which is a TCP connect to the
monitoring port. When it returns, bt_navigator is ACTIVE and serving.
Pick a different distro with NAV2_TAG — any -production or -standard tag from
nav2_docker works:
NAV2_TAG=lyrical-1.5.1-production docker compose -f integration/nav2/docker-compose.yml up nav2 -d --wait
2. Look at it¶
You should see Nav2's real tree — NavigateRecovery at the root, a PipelineSequence
under it, ComputePathToPose and FollowPath each wrapped in a RecoveryNode, and a
RoundRobin of spin / wait / back-up in the recovery branch.
Watch what happens as goals arrive. ComputePathToPose goes RUNNING then SUCCESS, then
FollowPath holds RUNNING for a few seconds — and on every third goal it fails, the
costmap-clearing branch runs, and the spin / wait / back-up round robin takes a turn.
Press g for the drawn graph to see which branch the robot went down.
Prefer one shot?
btview snapshot --port 1667 --active # only what is running or failing
btview echo --port 1667 # one line per transition
btview info --port 1667 # endpoint, tree size, round-trip latency
Or run the monitor in a container too, if you would rather not install anything:
3. Capture it¶
This is the workflow the tool exists for — record on the robot, read it back anywhere:
4. Stop it¶
The same stack as a test¶
The tutorial is also btview's integration suite. Twelve assertions run against this
container in CI:
docker compose -f integration/nav2/docker-compose.yml up nav2 -d --wait
pytest -m integration tests/integration
They check the things a mock structurally cannot. Our mock and our client were written by the same person from the same reading of the protocol — if that reading is wrong in both directions, every unit test still passes. So these assert that:
- the tree is recognisably Nav2's, with
PipelineSequenceandRecoveryNodesurviving the round trip; - every node has a runtime uid, and the uids in the status buffer are the uids in the
XML. This is the regression test for a bug that produced a perfect-looking tree in which
no node ever changed status, because BehaviorTree.CPP writes
_uidand we readuid; - conditions are identified as conditions, which is only possible if the publisher's node model was applied rather than guessed from tree shape;
- statuses actually change while goals are being fed;
- a real publisher answers a blackboard request — the one piece of the protocol that was previously only ever exercised against our own mock.
Without a container they skip with instructions. In CI BTVIEW_IT_REQUIRE=1 turns that
skip into a failure, because a green run that silently skipped everything is worse than no
test at all.
Going further: a robot that actually drives¶
This stack proves the monitoring path, not navigation. For a robot that really moves, use
the -standard image and Nav2's own simulation launch, which brings up Gazebo, a map,
localisation and the full stack:
docker run --rm -it --network host \
ghcr.io/ros-navigation/nav2_docker:jazzy-1.3.12-standard \
ros2 launch nav2_bringup tb3_simulation_launch.py headless:=True
You will need to enable monitoring in that stack's parameters — see Nav2 and BehaviorTree.CPP — then set an initial pose and a goal in RViz. It is a much heavier image (7.8 GB) and wants a GPU to be pleasant, which is exactly why the harness above does not use it.
Troubleshooting¶
| Symptom | Cause |
|---|---|
up --wait times out |
The image is still pulling. Otherwise docker compose -f integration/nav2/docker-compose.yml logs nav2 |
configure was refused in the log |
The tree failed to load. Usually a stub server that did not come up, so an action node could not resolve its server |
| Tree loads, nothing ever changes | The goal loop died. Look for feeding goals in the container log |
| Connection refused on 1667 | Monitoring is off — check the log line listing the groot parameters this build honoured |
| Tree appears but statuses are all IDLE | Normal between goals; the loop pauses two seconds after each one |