Transports and the bridge¶
btview separates what the bytes mean (the codec) from how they arrive (the
transport). Above that line nothing cares: the TUI, snapshot, echo and record all work
against anything that can hand over a tree and, when asked, a bag of statuses.
That is what makes a recording on disk, a live robot, and a future ROS 2 topic interchangeable — and what makes the bridge a five-line loop rather than a special case.
Naming: it is ZeroMQ, not MQTT¶
The two get confused constantly, so to be explicit: BehaviorTree.CPP monitoring is
ZeroMQ. groot2_publisher.cpp includes zmq_addon.hpp and opens
ZMQ_REP and ZMQ_PUB sockets. There is no broker, no CONNECT/PUBLISH packets, and
nothing listening on 1883. Pointing an MQTT client at port 1667 will not work.
"Groot2" in btview names the wire format (the request/reply framing that
Groot2Publisher speaks), not the transport and not the GUI. --protocol v4 is a shorthand
for it; groot2://host:port says the same thing more precisely.
Source URIs¶
Every command that connects accepts --source URI as an alternative to
--host/--port/--protocol:
btview --source groot2://robot.local:1667
btview snapshot --source btcpp3://robot:1667?publisher_port=1666
btview --source replay://run.btv.jsonl?speed=4&loop=1
btview --source mock:// # a synthetic robot, no hardware
btview transports lists what the installed build can read and write.
| Scheme | Reads |
|---|---|
groot2://host:port |
BehaviorTree.CPP 4.x Groot2Publisher (default port 1667) |
btcpp3://host:port |
BehaviorTree.CPP 3.x PublisherZMQ (REP 1667, ?publisher_port=1666) |
ros2://topic |
Nav2's behavior_tree_log — no publisher needed (?tree=, needs rclpy) |
replay://path / file://path |
a recording from btview record (?speed=, ?loop=) |
mock://[host:port] |
an in-process synthetic Nav2 publisher (?tick_hz=) |
Options are query parameters, so a URI is copy-pasteable into a script or a systemd unit:
groot2://robot:1667?timeout_ms=2000.
The bridge¶
It reads one transport and republishes on another. The sinks:
| Scheme | Writes |
|---|---|
groot2://bind:port |
re-serves the stream as a Groot2 publisher |
file://path |
a .btv.jsonl recording (same format as btview record) |
Let Groot2 watch a BehaviorTree.CPP 3.x robot¶
The Groot2 GUI speaks v4 only. A robot on BehaviorTree.CPP 3.x cannot talk to it — unless something translates:
Now point Groot2 (or another btview) at the bridge. This path is covered by a test that
drives a v3 publisher through the bridge and reads it back with a v4 client.
Replay a recording as if it were a live robot¶
Useful for demoing, for teaching, and for pointing a GUI at a failure that already happened.
Capture from anywhere¶
Which is exactly what btview record does — record is the ergonomic spelling, the bridge
is the general one.
Adding a transport¶
A source is any class implementing MonitorSource — two methods:
from btview.client import MonitorSource, PollResult
class MyTopicSource(MonitorSource):
label = "my-robot"
protocol = "custom"
reports_transitions = True # True if you push transitions instead of us diffing
def fetch_tree(self) -> TreeModel: ...
def poll(self) -> Optional[PollResult]: ...
Register it against a scheme and every command can use it:
from btview.sources import register_source
@register_source("mytopic", "my fleet's status topic")
def _open(uri, **options):
return MyTopicSource(host=uri.host, port=uri.port, **options)
Sinks are the mirror image — open(tree), publish(statuses, transitions, timestamp),
close() — registered with register_sink. btview.sinks.RecordingSink is a short example;
Groot2Sink shows how to re-serve a protocol.
ros2:// — no publisher required¶
Nav2 already puts everything on the ROS graph. nav2_behavior_tree's RosTopicLogger
creates a publisher on behavior_tree_log carrying nav2_msgs/BehaviorTreeLog, and it
does so next to the tree, regardless of enable_groot_monitoring. Each
BehaviorTreeStatusChange has:
builtin_interfaces/Time timestamp
string node_name
uint16 uid # the same uid the ZMQ protocols use
string previous_status # IDLE, RUNNING, SUCCESS or FAILURE
string current_status
btview --source ros2://behavior_tree_log
btview --source ros2://behavior_tree_log?tree=/opt/ros/bt/navigate_to_pose.xml
btview --source ros2:///robot1/behavior_tree_log # namespaced
Two things this does better than the ZMQ path:
- There is nothing to switch on. It works against stock Nav2 with Groot monitoring off and no port to reach. On a robot where you cannot edit parameters or open a port, this is the way in.
- It cannot miss a transition. Groot2 only exposes snapshots, so
btviewderives transitions by diffing polls, and a node that goes RUNNING then SUCCESS between two polls shows up once. The log reports every change at any poll rate.
It needs rclpy, which btview does not depend on¶
The whole premise of the tool is that it installs on a laptop with no ROS. So rclpy is
imported lazily, only by this transport, and only when you use it. Run btview from a
sourced workspace; in a virtualenv, create it with --system-site-packages so rclpy stays
visible:
source /opt/ros/jazzy/setup.bash
python3 -m venv --system-site-packages ~/.venvs/btview-ros
~/.venvs/btview-ros/bin/pip install btview
~/.venvs/btview-ros/bin/btview --source ros2://behavior_tree_log
Without it you get an error saying exactly that, rather than a stack trace.
The topic carries events, not a tree¶
This is the transport's real limitation. behavior_tree_log says what changed, never what
the tree looks like, so there are two modes:
| What you get | |
|---|---|
ros2://behavior_tree_log |
A flat list of nodes, discovered as they first tick. Statuses, sparklines and the transition log all work; there is no hierarchy to draw, and nodes appear as the tree reaches them |
ros2://behavior_tree_log?tree=file.xml |
The real hierarchy from your behaviour tree XML |
?timeout= (default 10s) is how long fetch_tree waits for the first message in discovered
mode. A navigator only ticks while it holds a goal, so an idle robot legitimately says
nothing — if it times out, send a goal and try again.
About tree= and uids¶
Statuses are keyed by uid, and a behaviour tree file as written by a human has none —
_uid only appears in WriteTreeToXML output. So btview numbers the file itself,
depth-first from 1, which is the order BehaviorTree.CPP hands uids out as it builds the tree.
That is an assumption about someone else's implementation detail, so it is checked rather
than trusted, and it holds: CI strips _uid from a live Nav2 tree, re-infers the
numbering and asserts it matches what the publisher said, and separately drives the whole
transport against Nav2's own 28-node navigate_to_pose tree inside a container. At runtime
any event whose logged node name disagrees with the node it lands on falls back to the
logged name and is counted, so a distro that numbers differently reports itself rather than
lighting up the wrong node.
If you ever suspect trouble, a tree exported from a Groot-enabled run carries the publisher's real uids and is used as-is:
btview export --format xml -o tree-with-uids.xml # once, with monitoring on
btview --source ros2://behavior_tree_log?tree=tree-with-uids.xml
Still to come: an MQTT sink¶
For fleet dashboards. A broker cannot be exercised in this project's CI, so it needs its own
testing story rather than being bolted on quietly — the same standard ros2:// was held to,
which is why that one is verified against a real bt_navigator in a container.