Skip to content

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

btview --port 1667

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:

docker compose -f integration/nav2/docker-compose.yml run --rm monitor

3. Capture it

This is the workflow the tool exists for — record on the robot, read it back anywhere:

btview record --port 1667 -o nav2.btv.jsonl --duration 60
btview play nav2.btv.jsonl --speed 4

4. Stop it

docker compose -f integration/nav2/docker-compose.yml down

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 PipelineSequence and RecoveryNode surviving 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 _uid and we read uid;
  • 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