Skip to content

Wire protocols

btview implements two protocols. Everything here is decode/encode only — see src/btview/protocol/, which has no socket code in it at all, and tests/test_protocol.py, which pins every layout below.

BehaviorTree.CPP 4.x BehaviorTree.CPP 3.x
Publisher BT::Groot2Publisher BT::PublisherZMQ
Sockets REP on port, PUB on port + 1 REP on server_port, PUB on publisher_port
Defaults 1667 (and 1668) REP 1667, PUB 1666
Tree encoding XML FlatBuffers
Status delivery client polls server pushes
Transitions derived by diffing sent explicitly
btview flag --protocol v4 (default) --protocol v3

Groot2 (BehaviorTree.CPP 4.x)

Reference: include/behaviortree_cpp/loggers/groot2_protocol.h and src/loggers/groot2_publisher.cpp.

Request header — 6 bytes

Offset Field Type Notes
0 protocol uint8 kProtocolID = 2
1 type uint8 ASCII request tag
2–5 unique_id uint32 little-endian

Request tags (BT::Monitor::RequestType):

Tag Name Implemented by btview
T FULLTREE yes
S STATUS yes
B BLACKBOARD yes
I R A D H hook management no
N U breakpoints no
O P recording / transitions no

Reply — multipart

  1. Header, 22 bytes: the 6-byte request header echoed back, then a 16-byte tree UUID.
  2. Payload: depends on the request.

An error reply is a two-frame message whose first frame is the literal error; btview raises the second frame as the message.

FULLTREE

Payload is the whole tree as XML — the output of WriteTreeToXML(tree, true, true), which means:

  • every node carries _uid (the runtime id the status buffer refers to) and _fullpath;
  • each instantiated subtree is written as its own <BehaviorTree ID="..."> block;
  • a <TreeNodesModel> section declares each registration as Action, Condition, Control, Decorator or SubTree.

btview inlines SubTree nodes from the matching block, so the view matches execution rather than the file layout. Repeated instantiations consume successive blocks with the same ID. Hand-written XML without _uid still renders; those nodes just never get a status.

STATUS

Payload is 3 bytes per node, in tree order:

uint16 uid (little-endian) | uint8 status

Status values are BT::NodeStatus: 0 IDLE, 1 RUNNING, 2 SUCCESS, 3 FAILURE, 4 SKIPPED.

A node that is idle now but produced a result earlier is reported as 10 + previous — so 12 means "idle, and it succeeded". btview decodes that into a status plus a remembered result and dims the row instead of showing a bare IDLE, which is what lets you see which branch already finished. Bytes outside 0..4 and 10..14 degrade to IDLE.

BLACKBOARD

Request is two frames: the header, then blackboard names joined with ; (empty means all). The reply payload is a JSON object serialised with MessagePack (nlohmann::json::to_msgpack). Not every build answers this; btview surfaces the error rather than hanging.

Implementation note: btview's blackboard support is written to the upstream source and verified against its own mock, not against every BehaviorTree.CPP build in the wild. If your publisher answers differently, please open an issue with the raw payload.


BehaviorTree.CPP 3.x (PublisherZMQ)

Reference: include/behaviortree_cpp_v3/loggers/bt_zmq_publisher.h.

Tree — REP socket

Send any message; the reply is a FlatBuffers buffer whose root table is Serialization.BehaviorTree (schema: fbs/BT_logger.fbs, the same file BehaviorTree.CPP generates its C++ from). btview reads it with the FlatBuffers runtime and hand-written accessors — the field slots live in bt_logger_fb.py and must stay in sync with the schema.

Relevant fields: root_uid, nodes[] (uid, children_uid[], status, instance_name, registration_name, port_remaps[]) and node_models[] (registration_name, type).

Status — PUB socket

One message per flush():

Bytes Field
4 uint32 LE — size of the status region
n status region: 3 bytes per node, same layout as Groot2
4 uint32 LE — number of transitions
12 each transition records

A transition record is int32 t_sec, int32 t_usec, uint16 uid, int8 prev_status, int8 status — 12 bytes, little-endian (BT::SerializeTransition).

v3 has no "idle, remembering" encoding: status bytes are plain NodeStatus values, and there is no SKIPPED.

btview drains the SUB queue on each poll, keeps the newest snapshot, and reports every transition seen — so on v3 nothing is lost between polls.

Constraints

Upstream allows only one PublisherZMQ per process (a global guard), and the two ports must differ. btview rejects equal ports up front rather than failing obscurely later.


Testing against btview

btview mock implements the publisher side of both protocols with a real ticking tree, so you can develop a client, verify a network path, or point Groot2 itself at something while the robot is in pieces:

btview mock                 # Groot2 v4 REP on 1667
btview mock --protocol v3   # v3 REP 1667 / PUB 1666

The mock implements FULLTREE, STATUS and BLACKBOARD, and answers anything else with a protocol error. It does not bind the v4 PUB port (port + 1), which only carries breakpoint notifications that btview does not consume.